From d30b1f5fbf035a4ae5ecb895bb485dfacbdbad25 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:19:31 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0:=20?= =?UTF-8?q?chart=20export=20=EB=82=B4=20O(N^2)=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20O(1)=20=EB=94=95=EC=85=94=EB=84=88?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 3 +++ .../src/bandscope_analysis/exports/chart.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index d54cf10fc..469dd8c61 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -61,3 +61,6 @@ ## 2026-07-13 - Array.from mapping optimization **Learning:** Using `Array.from({ length: N }).map(...)` creates an intermediate array of `undefined` values which requires memory allocation and garbage collection, adding O(N) unnecessary overhead in frequently re-rendered UI components. **Action:** Use `Array.from({ length: N }, (_, index) => ...)` to map elements directly during array creation, avoiding intermediate allocations. +## 2024-05-14 - Python O(N^2) list dedup bottleneck +**Learning:** Found O(N^2) deduplication using `if item not in list:` inside nested loops in `services/analysis-engine/src/bandscope_analysis/exports/chart.py`. Given that memory instructions suggest using dictionary keys for deduplication (`dict.fromkeys(items)` or `seen[item] = None`) to maintain O(1) lookups and preserve insertion order, this is a measurable performance anti-pattern. +**Action:** Replace `item not in list` with dict key lookups (`dict.fromkeys()` where applicable, or a `seen` dict) in hot paths like export and formatting routines. diff --git a/services/analysis-engine/src/bandscope_analysis/exports/chart.py b/services/analysis-engine/src/bandscope_analysis/exports/chart.py index 3a84b59c8..1ad51d2a2 100644 --- a/services/analysis-engine/src/bandscope_analysis/exports/chart.py +++ b/services/analysis-engine/src/bandscope_analysis/exports/chart.py @@ -122,9 +122,11 @@ def _role_display_name(role: Mapping[str, object]) -> str | None: def _active_role_names(section: Mapping[str, object]) -> list[str]: """Return de-duplicated display names for the section's active roles.""" names: list[str] = [] + seen: dict[str, None] = {} for role in _active_roles(section): name = _role_display_name(role) - if name is not None and name not in names: + if name is not None and name not in seen: + seen[name] = None names.append(name) return names @@ -132,12 +134,14 @@ def _active_role_names(section: Mapping[str, object]) -> list[str]: def _section_cue(section: Mapping[str, object]) -> str: """Join the active roles' cue values into a single cue string.""" cues: list[str] = [] + seen: dict[str, None] = {} for role in _active_roles(section): cue = role.get("cue") if not isinstance(cue, Mapping): continue value = cue.get("value") - if isinstance(value, str) and value and value not in cues: + if isinstance(value, str) and value and value not in seen: + seen[value] = None cues.append(value) return "; ".join(cues) @@ -189,6 +193,7 @@ def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object """Build the footer: per-role rehearsal priorities and the export focus.""" lines: list[str] = [] priorities: list[str] = [] + seen_priorities: dict[str, None] = {} for section in sections: for role in _section_roles(section): name = _role_display_name(role) @@ -196,7 +201,8 @@ def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object if name is None or not isinstance(priority, str) or not priority: continue entry = f" - {name}: {priority}" - if entry not in priorities: + if entry not in seen_priorities: + seen_priorities[entry] = None priorities.append(entry) if priorities: lines.append("Priorities:") From c298ec82c31717e13ca30e37980f14aa5faad7b6 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:34:27 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0:=20?= =?UTF-8?q?chart=20export=20=EB=82=B4=20O(N^2)=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20O(1)=20=EB=94=95=EC=85=94=EB=84=88?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From bc779a076fbfae176ff88f276f1642d419aa8cd7 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:48:39 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0:=20?= =?UTF-8?q?chart=20export=20=EB=82=B4=20O(N^2)=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20O(1)=20=EB=94=95=EC=85=94=EB=84=88?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analysis-engine/src/bandscope_analysis/exports/chart.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/analysis-engine/src/bandscope_analysis/exports/chart.py b/services/analysis-engine/src/bandscope_analysis/exports/chart.py index 1ad51d2a2..d5c74f719 100644 --- a/services/analysis-engine/src/bandscope_analysis/exports/chart.py +++ b/services/analysis-engine/src/bandscope_analysis/exports/chart.py @@ -122,6 +122,7 @@ def _role_display_name(role: Mapping[str, object]) -> str | None: def _active_role_names(section: Mapping[str, object]) -> list[str]: """Return de-duplicated display names for the section's active roles.""" names: list[str] = [] + # ⚡ Bolt: O(1) deduplication cache to prevent O(N^2) list.includes() bottleneck seen: dict[str, None] = {} for role in _active_roles(section): name = _role_display_name(role) @@ -134,6 +135,7 @@ def _active_role_names(section: Mapping[str, object]) -> list[str]: def _section_cue(section: Mapping[str, object]) -> str: """Join the active roles' cue values into a single cue string.""" cues: list[str] = [] + # ⚡ Bolt: O(1) deduplication cache to prevent O(N^2) list.includes() bottleneck seen: dict[str, None] = {} for role in _active_roles(section): cue = role.get("cue") @@ -193,6 +195,7 @@ def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object """Build the footer: per-role rehearsal priorities and the export focus.""" lines: list[str] = [] priorities: list[str] = [] + # ⚡ Bolt: O(1) deduplication cache to prevent O(N^2) list.includes() bottleneck seen_priorities: dict[str, None] = {} for section in sections: for role in _section_roles(section): From 80008ad0fdc523d7a5082fec96c0ac7162a147fb Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:53:59 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0:=20?= =?UTF-8?q?chart=20export=20=EB=82=B4=20O(N^2)=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20O(1)=20=EB=94=95=EC=85=94=EB=84=88?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From e6e54ed78b4e500238c3da27e36e8de301973a67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 22:11:07 -0700 Subject: [PATCH 5/7] chore(chart): correct Python deduplication comments --- .../analysis-engine/src/bandscope_analysis/exports/chart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/analysis-engine/src/bandscope_analysis/exports/chart.py b/services/analysis-engine/src/bandscope_analysis/exports/chart.py index d5c74f719..8753c6751 100644 --- a/services/analysis-engine/src/bandscope_analysis/exports/chart.py +++ b/services/analysis-engine/src/bandscope_analysis/exports/chart.py @@ -122,7 +122,7 @@ def _role_display_name(role: Mapping[str, object]) -> str | None: def _active_role_names(section: Mapping[str, object]) -> list[str]: """Return de-duplicated display names for the section's active roles.""" names: list[str] = [] - # ⚡ Bolt: O(1) deduplication cache to prevent O(N^2) list.includes() bottleneck + # Use expected O(1) dictionary membership instead of O(N) list membership. seen: dict[str, None] = {} for role in _active_roles(section): name = _role_display_name(role) @@ -135,7 +135,7 @@ def _active_role_names(section: Mapping[str, object]) -> list[str]: def _section_cue(section: Mapping[str, object]) -> str: """Join the active roles' cue values into a single cue string.""" cues: list[str] = [] - # ⚡ Bolt: O(1) deduplication cache to prevent O(N^2) list.includes() bottleneck + # Use expected O(1) dictionary membership instead of O(N) list membership. seen: dict[str, None] = {} for role in _active_roles(section): cue = role.get("cue") @@ -195,7 +195,7 @@ def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object """Build the footer: per-role rehearsal priorities and the export focus.""" lines: list[str] = [] priorities: list[str] = [] - # ⚡ Bolt: O(1) deduplication cache to prevent O(N^2) list.includes() bottleneck + # Use expected O(1) dictionary membership instead of O(N) list membership. seen_priorities: dict[str, None] = {} for section in sections: for role in _section_roles(section): From 8d0a80deecd90e3cad0df4ae49685b3ffa479dae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 22:12:59 -0700 Subject: [PATCH 6/7] test(chart): expose upstream quadratic role-id dedup --- .../test_chart_export_dedup_complexity.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 services/analysis-engine/tests/test_chart_export_dedup_complexity.py diff --git a/services/analysis-engine/tests/test_chart_export_dedup_complexity.py b/services/analysis-engine/tests/test_chart_export_dedup_complexity.py new file mode 100644 index 000000000..b47ee1459 --- /dev/null +++ b/services/analysis-engine/tests/test_chart_export_dedup_complexity.py @@ -0,0 +1,37 @@ +"""Deterministic complexity regression for chart-export role de-duplication.""" + +from typing import ClassVar + +from bandscope_analysis.exports.chart import _active_role_ids + + +class _CountingRoleId(str): + """String role id that counts equality work without wall-clock timing.""" + + comparisons: ClassVar[int] = 0 + + def __eq__(self, other: object) -> bool: + """Count one equality comparison and preserve normal string semantics.""" + type(self).comparisons += 1 + return super().__eq__(other) + + def __hash__(self) -> int: + """Preserve normal string hashing for representative hash membership.""" + return super().__hash__() + + +def test_active_role_id_deduplication_avoids_quadratic_equality_work() -> None: + """Unique active ids must not require pairwise list-membership comparisons.""" + role_ids = [_CountingRoleId(f"role-{index}") for index in range(64)] + section = { + "partGraph": [ + {"role_id": role_id, "is_active": True} + for role_id in [*role_ids, _CountingRoleId("role-0")] + ] + } + + _CountingRoleId.comparisons = 0 + active = _active_role_ids(section) + + assert active == role_ids + assert _CountingRoleId.comparisons < 256 From fce7651cbeb37edcaa0ad6cec2bafff54acc0d61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 22:13:40 -0700 Subject: [PATCH 7/7] perf(chart): remove upstream quadratic active-id scan --- .../analysis-engine/src/bandscope_analysis/exports/chart.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/analysis-engine/src/bandscope_analysis/exports/chart.py b/services/analysis-engine/src/bandscope_analysis/exports/chart.py index 8753c6751..6d853bf0b 100644 --- a/services/analysis-engine/src/bandscope_analysis/exports/chart.py +++ b/services/analysis-engine/src/bandscope_analysis/exports/chart.py @@ -79,11 +79,13 @@ def _active_role_ids(section: Mapping[str, object]) -> list[str] | None: if not isinstance(part_graph, list): return None active: list[str] = [] + seen: set[str] = set() for node in part_graph: if not isinstance(node, Mapping) or node.get("is_active") is not True: continue role_id = node.get("role_id") - if isinstance(role_id, str) and role_id and role_id not in active: + if isinstance(role_id, str) and role_id and role_id not in seen: + seen.add(role_id) active.append(role_id) return active