diff --git a/.jules/bolt.md b/.jules/bolt.md index 231af188b..d5fcbd53e 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -10,3 +10,8 @@ ## 2024-05-24 - Memoizing inline array maps **Learning:** Inline mapping of arrays inside JSX in large React components causes O(N) recalculation on every render. **Action:** Wrap inline JSX elements that map over arrays (e.g., lists of tasks) in a `useMemo` hook with specific dependencies. + +## 2025-02-12 - Avoided unused setdefault list allocations in grouping loops + +**Learning:** `dict.setdefault(key, []).append(value)` evaluates the empty-list default on every iteration, including when the key already exists. In grouping loops, `defaultdict(list)` avoids those transient unused list allocations while preserving insertion order. +**Action:** Use `defaultdict(list)` when missing keys are intentionally initialized with lists. Keep `setdefault` when its eager-default behavior or an ordinary `dict` is part of the required contract, and benchmark before claiming a material end-to-end improvement. diff --git a/CHANGELOG.md b/CHANGELOG.md index dcfc6f787..c3c96302f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ ### 코드 건강성 개선 (Code Health) +- 백엔드 그룹화 루프에 `defaultdict(list)`를 적용해 기존 순서와 응답을 보존하면서 `setdefault`가 매 반복마다 만들던 미사용 빈 리스트 할당을 피했습니다. - `WorkspaceHome`의 작업 완료 토글과 Reply SLA 팔로업 생성 로직을 `useTasks` hook으로 분리하고, 화면 쪽 formatter를 주입해 작업 제목 정규화 로직 중복을 방지했습니다. - `backend/api/security.py`에서 사용하지 않는 `from __future__ import annotations` 구문을 제거하고 조건 표현식을 정리했습니다. - `backend/alembic/env.py`에서 사용하지 않는 `from __future__ import annotations` 구문을 제거해 Alembic 환경 설정 코드를 간결하게 정리했습니다. diff --git a/backend/api/data.py b/backend/api/data.py index c470d69c0..dccd85890 100644 --- a/backend/api/data.py +++ b/backend/api/data.py @@ -1,5 +1,6 @@ import base64 import binascii +from collections import defaultdict from datetime import datetime, timezone import hashlib import json @@ -1695,14 +1696,14 @@ def _diligence_risk_matrix( groups: dict[ tuple[RemediationPriority, str, str], list[DataDiligenceExceptionRegisterEntry], - ] = {} + ] = defaultdict(list) for exception in snapshot.diligence_exception_register: key = ( exception.severity_code, exception.owner_area, exception.related_artifact, ) - groups.setdefault(key, []).append(exception) + groups[key].append(exception) entries: list[DataDiligenceRiskMatrixEntry] = [] for (severity, owner_area, related_artifact), exceptions in sorted( @@ -1844,9 +1845,9 @@ def _diligence_close_decision_summary( def _diligence_close_artifact_review_queue( snapshot: DataEvidenceSnapshotResponse, ) -> list[DataDiligenceCloseArtifactReviewQueueEntry]: - groups: dict[str, list[DataDiligenceCloseProofPlanEntry]] = {} + groups: dict[str, list[DataDiligenceCloseProofPlanEntry]] = defaultdict(list) for proof in snapshot.diligence_close_proof_plan: - groups.setdefault(proof.required_proof_artifact, []).append(proof) + groups[proof.required_proof_artifact].append(proof) entries: list[DataDiligenceCloseArtifactReviewQueueEntry] = [] for artifact, proofs in sorted(groups.items()): @@ -1889,9 +1890,9 @@ def _diligence_close_artifact_review_queue( def _diligence_close_owner_handoff_queue( snapshot: DataEvidenceSnapshotResponse, ) -> list[DataDiligenceCloseOwnerHandoffQueueEntry]: - groups: dict[str, list[DataDiligenceCloseProofPlanEntry]] = {} + groups: dict[str, list[DataDiligenceCloseProofPlanEntry]] = defaultdict(list) for proof in snapshot.diligence_close_proof_plan: - groups.setdefault(proof.owner_area, []).append(proof) + groups[proof.owner_area].append(proof) entries: list[DataDiligenceCloseOwnerHandoffQueueEntry] = [] for owner_area, proofs in sorted(groups.items()): diff --git a/backend/services/email_import_service.py b/backend/services/email_import_service.py index a91038f73..1ff9a2bb3 100644 --- a/backend/services/email_import_service.py +++ b/backend/services/email_import_service.py @@ -1,5 +1,6 @@ import asyncio import datetime +from collections import defaultdict from email import policy as email_policy import hashlib import logging @@ -575,7 +576,7 @@ def add_edge( segments_by_source: dict[ tuple[str, str], list[ContentSegmentRecord], - ] = {} + ] = defaultdict(list) for segment in sorted( email_obj.content_segments, key=lambda item: ( @@ -585,10 +586,9 @@ def add_edge( item.segment_path, ), ): - segments_by_source.setdefault( - (segment.source_kind, segment.source_record_uid), - [], - ).append(segment) + segments_by_source[ + (segment.source_kind, segment.source_record_uid) + ].append(segment) add_edge( edge_kind="node_has_segment", edge_path=f"{segment.content_node.node_path}/has/{segment.segment_path}", diff --git a/backend/services/project_graph/project_registration.py b/backend/services/project_graph/project_registration.py index d4d0932bf..57cfd5633 100644 --- a/backend/services/project_graph/project_registration.py +++ b/backend/services/project_graph/project_registration.py @@ -2,7 +2,7 @@ import datetime import hashlib -from collections import Counter +from collections import Counter, defaultdict from dataclasses import dataclass from typing import Any, Iterable, Mapping @@ -612,9 +612,9 @@ def _candidate_groups( *, scope: ProjectGraphQueryScope, ) -> tuple[_CandidateGroup, ...]: - records_by_email: dict[int, list[ProjectGraphObjectRecord]] = {} + records_by_email: dict[int, list[ProjectGraphObjectRecord]] = defaultdict(list) for record in records: - records_by_email.setdefault(record.email_id, []).append(record) + records_by_email[record.email_id].append(record) groups: list[_CandidateGroup] = [] for group_records in records_by_email.values(): @@ -632,7 +632,9 @@ def _candidate_groups( if explicit_candidate is not None else _synthetic_project_uid(group_records, scope=scope) ) - groups.append(_CandidateGroup(project_uid=project_uid, records=tuple(group_records))) + groups.append( + _CandidateGroup(project_uid=project_uid, records=tuple(group_records)) + ) return tuple(groups) @@ -842,9 +844,9 @@ def _relation_summary( ``relation_count`` descending with a ``relation_type``-ascending tie-break so the result is deterministic regardless of relation iteration order. """ - grouped: dict[str, list[ProjectTraceRelation]] = {} + grouped: dict[str, list[ProjectTraceRelation]] = defaultdict(list) for relation in relations: - grouped.setdefault(relation.relation_type, []).append(relation) + grouped[relation.relation_type].append(relation) type_summaries = [ ProjectRelationTypeSummary( relation_type=relation_type,