From 607df818509ddfbc6415e824289a0b731b622c06 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:50:08 +0000 Subject: [PATCH] =?UTF-8?q?perf(network-graph):=20O(N)=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=20=EC=97=B0=EC=82=B0=EC=9D=84=20=EC=A0=9C=ED=95=9C?= =?UTF-8?q?=EB=90=9C=20=EB=A3=A8=ED=94=84=EB=A1=9C=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NetworkGraph 컴포넌트의 useMemo 훅 내부에서 사용되던 비효율적인 O(N) 체인 연산(`Array.from().slice().map()` 및 `.map().filter().slice()`)을 크기가 제한된 O(1) `for...of` 루프로 대체했습니다. 이를 통해 대규모 노드 및 관계 데이터 렌더링 시 불필요한 중간 배열 할당을 방지하고 성능을 개선했습니다. --- .jules/bolt.md | 3 ++ CHANGELOG.md | 8 +++++ frontend/src/components/NetworkGraph.tsx | 46 ++++++++++++++++-------- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index fa2deda3f..5dc54027d 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,3 +26,6 @@ ## 2024-05-24 - [React Component Memoization] **Learning:** In React components like `WorkspaceHome`, when layout state or polling changes trigger parent re-renders, expensive child components like `EmailDetail` will also re-render unnecessarily if not memoized. **Action:** Always consider `React.memo` for heavy child components that rely on stable props (like IDs) when the parent component has frequent unrelated state updates. +## 2024-11-20 - O(N) Array Operations in React Render Hooks +**Learning:** Chaining array methods like `Array.from().slice(0, N).map()` or `.map().filter().slice(0, N)` inside React `useMemo` hooks is highly inefficient for large sets, as it allocates full intermediate O(N) arrays before truncating them. +**Action:** Replace these operations with bounded `for...of` loops and early `break` statements when the desired element limit (N) is reached to preserve O(1) memory allocation and processing time. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec84c36f..abb105ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ ## [Unreleased] + +### 성능 개선 +- **NetworkGraph**: `useMemo` 내부의 O(N) 배열 연산(`Array.from().slice().map()` 등)을 O(1) 크기의 제한된 `for...of` 루프로 최적화하여 메모리 할당 및 렌더링 성능을 개선했습니다. + - 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다. - EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다. @@ -2736,6 +2740,10 @@ - `docker compose down` ## [Unreleased] + +### 성능 개선 +- **NetworkGraph**: `useMemo` 내부의 O(N) 배열 연산(`Array.from().slice().map()` 등)을 O(1) 크기의 제한된 `for...of` 루프로 최적화하여 메모리 할당 및 렌더링 성능을 개선했습니다. + ### Added - `backend/api/tools.py` 내의 임시 `mock_handler`를 구체적인 기능을 수행하는 5개의 실제 도구 핸들러로 대체했습니다. - `thread_summarizer_handler`: 이메일 스레드 요약 정보 반환 diff --git a/frontend/src/components/NetworkGraph.tsx b/frontend/src/components/NetworkGraph.tsx index f9eb61c71..2963abd68 100644 --- a/frontend/src/components/NetworkGraph.tsx +++ b/frontend/src/components/NetworkGraph.tsx @@ -278,27 +278,45 @@ export default function NetworkGraph() { }, [nodes, edges, nodeMap, edgeMap]); const nodeLabels = useMemo(() => { - return nodes - .map((node) => String(node.label ?? node.id)) - .filter(Boolean) - .slice(0, 5); + // Bolt Optimization: Replace O(N) intermediate array map/filter/slice with a bounded O(1) loop + const labels: string[] = []; + for (const node of nodes) { + if (labels.length >= 5) break; + const label = String(node.label ?? node.id); + if (label) labels.push(label); + } + return labels; }, [nodes]); const firstEdge = edges[0] ?? null; const relationshipOptions = useMemo(() => { - return Array.from(edgeMap.values()).slice(0, 5).map((edge, index) => ({ - edge, - id: String(edge.id), - label: `관계 ${index + 1}: ${describeEdge(edge, nodeMap)}`, - })); + // Bolt Optimization: Replace O(N) Array.from().slice().map() with bounded O(1) loop + const options = []; + let index = 0; + for (const edge of edgeMap.values()) { + if (options.length >= 5) break; + options.push({ + edge, + id: String(edge.id), + label: `관계 ${index + 1}: ${describeEdge(edge, nodeMap)}`, + }); + index++; + } + return options; }, [edgeMap, nodeMap]); const nodeOptions = useMemo(() => { - return Array.from(nodeInstanceMap.values()).slice(0, 8).map((node) => ({ - id: String(node.id), - label: `노드: ${String(node.label ?? node.id)}`, - node, - })); + // Bolt Optimization: Replace O(N) Array.from().slice().map() with bounded O(1) loop + const options = []; + for (const node of nodeInstanceMap.values()) { + if (options.length >= 8) break; + options.push({ + id: String(node.id), + label: `노드: ${String(node.label ?? node.id)}`, + node, + }); + } + return options; }, [nodeInstanceMap]); const selectRelationship = (edge: Edge, status: string) => {