From 98bbc7c722916c411b3daab4cb7f685ad924814d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 11 Sep 2026 20:56:34 +0000 Subject: [PATCH 1/7] perf(frontend): replace O(N) array chain with bounded loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `NetworkGraph` 컴포넌트의 `useMemo` 내 관계 및 노드 옵션 매핑 과정에서 `.map().filter().slice()` 등 체인형 배열 메서드 사용으로 인한 O(N) 중간 배열 할당 비용을 제거했습니다. 이를 단일 `for...of` 루프와 조기 `break`로 교체하여 메모리 오버헤드를 개선했습니다. --- .jules/bolt.md | 5 +++ CHANGELOG.md | 1 + frontend/src/components/NetworkGraph.tsx | 48 +++++++++++++++++------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index fa2deda3f..2cd84c255 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,3 +26,8 @@ ## 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. +## 2025-02-12 - Replaced Chained Array Allocations with Bounded Loops in React Hooks + +**Learning:** Using chained array iteration methods like `.map().filter().slice(0, N)` inside React `useMemo` hooks is an anti-pattern when working with potentially large datasets. Each step in the chain produces an intermediate array that occupies memory and triggers garbage collection. For simple mapping and filtering where only a small number of elements are needed, this leads to unnecessary `O(N)` memory allocation and computational overhead. + +**Action:** Whenever a React hook transforms an array and only requires a bounded subset (e.g., the first 5 elements), replace the chained array methods (`.map().filter().slice()`) with a single bounded `for...of` loop. Allocate a target array, perform the transformation and condition checks inside the loop, push matching elements, and use an early `break` once the limit is reached. This reduces the time and space complexity to `O(1)` (relative to the bound) and prevents `O(N)` bottlenecks. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec84c36f..14db3fc37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased] +- **Performance (Bolt)**: `NetworkGraph` 컴포넌트 내의 관계 및 노드 선택 옵션 생성 시 `.map().filter().slice()` 등 체인된 배열 메서드 사용을 단일 루프(bounded `for...of`)로 대체하여 불필요한 O(N) 메모리 할당 및 병목 현상을 방지했습니다. - 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다. - EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다. diff --git a/frontend/src/components/NetworkGraph.tsx b/frontend/src/components/NetworkGraph.tsx index f9eb61c71..f5b79ba1b 100644 --- a/frontend/src/components/NetworkGraph.tsx +++ b/frontend/src/components/NetworkGraph.tsx @@ -278,27 +278,47 @@ 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 chained array allocations with bounded loops to prevent O(N) memory overhead + const result: string[] = []; + for (const node of nodes) { + const label = String(node.label ?? node.id); + if (label) { + result.push(label); + if (result.length >= 5) break; + } + } + return result; }, [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 chained array allocations with bounded loops to prevent O(N) memory overhead + const result = []; + let index = 0; + for (const edge of edgeMap.values()) { + result.push({ + edge, + id: String(edge.id), + label: `관계 ${index + 1}: ${describeEdge(edge, nodeMap)}`, + }); + index++; + if (result.length >= 5) break; + } + return result; }, [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 chained array allocations with bounded loops to prevent O(N) memory overhead + const result = []; + for (const node of nodeInstanceMap.values()) { + result.push({ + id: String(node.id), + label: `노드: ${String(node.label ?? node.id)}`, + node, + }); + if (result.length >= 8) break; + } + return result; }, [nodeInstanceMap]); const selectRelationship = (edge: Edge, status: string) => { From 328a52ab1190a01a39ce7fbf3adef2b992a5e1f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:44:10 +0900 Subject: [PATCH 2/7] repair(bolt): remove unmeasured global array rewrite heuristic --- .jules/bolt.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 2cd84c255..fa2deda3f 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,8 +26,3 @@ ## 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. -## 2025-02-12 - Replaced Chained Array Allocations with Bounded Loops in React Hooks - -**Learning:** Using chained array iteration methods like `.map().filter().slice(0, N)` inside React `useMemo` hooks is an anti-pattern when working with potentially large datasets. Each step in the chain produces an intermediate array that occupies memory and triggers garbage collection. For simple mapping and filtering where only a small number of elements are needed, this leads to unnecessary `O(N)` memory allocation and computational overhead. - -**Action:** Whenever a React hook transforms an array and only requires a bounded subset (e.g., the first 5 elements), replace the chained array methods (`.map().filter().slice()`) with a single bounded `for...of` loop. Allocate a target array, perform the transformation and condition checks inside the loop, push matching elements, and use an early `break` once the limit is reached. This reduces the time and space complexity to `O(1)` (relative to the bound) and prevents `O(N)` bottlenecks. From 097e4cd602df9b12c1c1339c12e9014ce5b28635 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:44:25 +0900 Subject: [PATCH 3/7] test(network): lock bounded graph option traversal --- .../NetworkGraph.map-lookup.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/frontend/src/components/NetworkGraph.map-lookup.test.ts b/frontend/src/components/NetworkGraph.map-lookup.test.ts index 3ba76c75c..bbd1f7735 100644 --- a/frontend/src/components/NetworkGraph.map-lookup.test.ts +++ b/frontend/src/components/NetworkGraph.map-lookup.test.ts @@ -63,4 +63,29 @@ describe("NetworkGraph constant-time selection lookup contract", () => { expect(networkGraphSource).toContain("firstGraphEntryById(nodes"); expect(networkGraphSource).not.toMatch(/new Map\((edges|nodes)\.map\(/); }); + + it("bounds option construction before traversing complete graph collections", () => { + const nodeLabels = sourceBetween("const nodeLabels = useMemo", "const firstEdge ="); + const relationshipOptions = sourceBetween( + "const relationshipOptions = useMemo", + "const nodeOptions = useMemo", + ); + const nodeOptions = sourceBetween( + "const nodeOptions = useMemo", + "const selectRelationship =", + ); + + expect(nodeLabels).toContain("for (const node of nodes)"); + expect(nodeLabels).toContain("if (result.length >= 5) break;"); + expect(nodeLabels).not.toContain(".map("); + expect(nodeLabels).not.toContain(".filter("); + + expect(relationshipOptions).toContain("for (const edge of edgeMap.values())"); + expect(relationshipOptions).toContain("if (result.length >= 5) break;"); + expect(relationshipOptions).not.toContain("Array.from("); + + expect(nodeOptions).toContain("for (const node of nodeInstanceMap.values())"); + expect(nodeOptions).toContain("if (result.length >= 8) break;"); + expect(nodeOptions).not.toContain("Array.from("); + }); }); From 9f4baf27efb8f06d39f37c786d6d918b40bfbded Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:42:57 +0900 Subject: [PATCH 4/7] test(network): harden bounded option traversal regression --- frontend/src/components/NetworkGraph.map-lookup.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/NetworkGraph.map-lookup.test.ts b/frontend/src/components/NetworkGraph.map-lookup.test.ts index bbd1f7735..6d2d3b133 100644 --- a/frontend/src/components/NetworkGraph.map-lookup.test.ts +++ b/frontend/src/components/NetworkGraph.map-lookup.test.ts @@ -82,10 +82,14 @@ describe("NetworkGraph constant-time selection lookup contract", () => { expect(relationshipOptions).toContain("for (const edge of edgeMap.values())"); expect(relationshipOptions).toContain("if (result.length >= 5) break;"); + expect(relationshipOptions).not.toContain(".map("); + expect(relationshipOptions).not.toContain(".filter("); expect(relationshipOptions).not.toContain("Array.from("); expect(nodeOptions).toContain("for (const node of nodeInstanceMap.values())"); expect(nodeOptions).toContain("if (result.length >= 8) break;"); + expect(nodeOptions).not.toContain(".map("); + expect(nodeOptions).not.toContain(".filter("); expect(nodeOptions).not.toContain("Array.from("); }); }); From 5c50e176632e95ba1cff95fdb803a0ce884db7a9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:55:09 +0000 Subject: [PATCH 5/7] docs(changelog): update performance optimization factual wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드리뷰어 요청에 따라 NetworkGraph 성능 향상 내역의 설명을 "병목 현상 방지" / "단일 루프" 등의 모호한 문구에서 bounded for...of 순회를 사용해 조기 종료하여 중간 컬렉션 생성을 줄였다는 검증된 사실 기반의 문장으로 수정했습니다. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14db3fc37..1027e6939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## [Unreleased] -- **Performance (Bolt)**: `NetworkGraph` 컴포넌트 내의 관계 및 노드 선택 옵션 생성 시 `.map().filter().slice()` 등 체인된 배열 메서드 사용을 단일 루프(bounded `for...of`)로 대체하여 불필요한 O(N) 메모리 할당 및 병목 현상을 방지했습니다. +- **Performance (Bolt)**: `NetworkGraph` 컴포넌트의 세 옵션 생성 경로를 bounded `for...of` 순회로 바꿔 제품 상한에서 조기에 멈추고 불필요한 중간 컬렉션 생성을 줄였습니다. - 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다. - EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다. From 31543499c86c733ee1db9925b65eb6132234322d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:58:29 +0000 Subject: [PATCH 6/7] style(frontend): remove redundant Bolt optimization comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드 리뷰 피드백에 따라 `NetworkGraph.tsx` 파일 내에서 불필요하게 반복되던 `// Bolt Optimization: ...` 주석 3곳을 삭제했습니다. 최적화 로직 자체나 루프의 한계값 등 다른 코드는 변경하지 않았습니다. --- frontend/src/components/NetworkGraph.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/components/NetworkGraph.tsx b/frontend/src/components/NetworkGraph.tsx index f5b79ba1b..0fbca4332 100644 --- a/frontend/src/components/NetworkGraph.tsx +++ b/frontend/src/components/NetworkGraph.tsx @@ -278,7 +278,6 @@ export default function NetworkGraph() { }, [nodes, edges, nodeMap, edgeMap]); const nodeLabels = useMemo(() => { - // Bolt Optimization: Replace chained array allocations with bounded loops to prevent O(N) memory overhead const result: string[] = []; for (const node of nodes) { const label = String(node.label ?? node.id); @@ -292,7 +291,6 @@ export default function NetworkGraph() { const firstEdge = edges[0] ?? null; const relationshipOptions = useMemo(() => { - // Bolt Optimization: Replace chained array allocations with bounded loops to prevent O(N) memory overhead const result = []; let index = 0; for (const edge of edgeMap.values()) { @@ -308,7 +306,6 @@ export default function NetworkGraph() { }, [edgeMap, nodeMap]); const nodeOptions = useMemo(() => { - // Bolt Optimization: Replace chained array allocations with bounded loops to prevent O(N) memory overhead const result = []; for (const node of nodeInstanceMap.values()) { result.push({ From d82e722dbf1b8b485d533056409bd91689a03b5f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:55:28 +0000 Subject: [PATCH 7/7] =?UTF-8?q?style(frontend):=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20Bolt=20=EC=84=B1=EB=8A=A5=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 리뷰어의 피드백을 반영하여 `NetworkGraph.tsx` 내의 불필요한 주석인 `// Bolt Optimization: Replace chained array allocations with bounded loops to prevent O(N) memory overhead` 3곳을 삭제했습니다. 기존 루프, 상한(limits), 로직은 일절 수정하지 않았습니다.