Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
40dcbc9
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 13, 2026
599c353
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 13, 2026
39a9265
build(security): update pdfjs-dist to 6.2.108 to resolve CVE-2026-16633
seonghobae Aug 13, 2026
53e054c
build(security): update pdfjs-dist to 6.2.108 to resolve CVE-2026-16633
seonghobae Aug 14, 2026
36db87e
chore(perf): isolate chart dedup optimization
seonghobae Aug 14, 2026
2d92b26
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 14, 2026
9f58b96
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 14, 2026
86ccbf2
chore(perf): keep chart optimization dependency-neutral
seonghobae Aug 14, 2026
5fc5e99
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 14, 2026
70893b7
perf(exports): restore chart deduplication to atomic scope
seonghobae Aug 14, 2026
982a367
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 14, 2026
f357941
chore(scope): restore chart optimization isolation
seonghobae Aug 14, 2026
45a3020
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 14, 2026
1a3d674
test(chart): keep optimization notes in Unreleased
seonghobae Aug 15, 2026
88aa81b
fix(changelog): keep chart optimization unreleased
seonghobae Aug 15, 2026
fe7c49d
test(changelog): freeze released audio-intake history
seonghobae Aug 15, 2026
ee9d0d4
fix(changelog): restore immutable released wording
seonghobae Aug 15, 2026
e0f87a9
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 15, 2026
26b6614
fix(chart): restore Unreleased changelog ownership
seonghobae Aug 15, 2026
6073935
test(chart): restore ordered dedup regression contract
seonghobae Aug 15, 2026
6674b56
test(chart): exercise distinct active duplicate roles
seonghobae Aug 15, 2026
e3268c8
test(chart): preserve linear dedup complexity contract
seonghobae Aug 23, 2026
f1d7067
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 25, 2026
352d954
perf: 차트 내보내기에서 배열 조회(not in list)를 딕셔너리 키 조회로 변경하여 O(N^2) 성능 저하 방지
seonghobae Aug 26, 2026
3b7717d
perf: 차트 내보내기에서 O(N^2) 성능 저하 방지 및 npm/Trivy CI 실패 수정
seonghobae Aug 26, 2026
a868964
Merge branch 'develop' into perf-chart-dedup-7079628762098570838
seonghobae Aug 26, 2026
9c27a94
perf: 차트 내보내기에서 O(N^2) 성능 저하 방지
seonghobae Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### 성능 최적화 (Performance)

- 차트 내보내기(chart.py)에서 `not in list` 대신 삽입 순서가 보장되는 딕셔너리(`dict.keys()`)를 활용하여 역할, 큐 및 우선순위 데이터의 중복 제거 방식을 $O(N^2)$에서 $O(N)$으로 최적화했습니다.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
31 changes: 15 additions & 16 deletions services/analysis-engine/src/bandscope_analysis/exports/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ def _active_role_ids(section: Mapping[str, object]) -> list[str] | None:
part_graph = section.get("partGraph")
if not isinstance(part_graph, list):
return None
active: list[str] = []
active: dict[str, None] = {}
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:
active.append(role_id)
return active
if isinstance(role_id, str) and role_id:
active[role_id] = None
return list(active.keys())
Comment on lines +81 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: chart.py dedup change preserves output

Replacing the not in list checks with insertion-ordered dict keys in _active_role_ids, _active_role_names, _section_cue, and _footer_lines keeps first-occurrence order and identical dedup results, so the emitted lists and joined strings are unchanged while complexity drops to O(N).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.



def _active_roles(section: Mapping[str, object]) -> list[Mapping[str, object]]:
Expand Down Expand Up @@ -121,25 +121,25 @@ 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] = []
names: dict[str, None] = {}
for role in _active_roles(section):
name = _role_display_name(role)
if name is not None and name not in names:
names.append(name)
return names
if name is not None:
names[name] = None
return list(names.keys())


def _section_cue(section: Mapping[str, object]) -> str:
"""Join the active roles' cue values into a single cue string."""
cues: list[str] = []
cues: 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:
cues.append(value)
return "; ".join(cues)
if isinstance(value, str) and value:
cues[value] = None
return "; ".join(cues.keys())


def _confidence_level(section: Mapping[str, object]) -> str | None:
Expand Down Expand Up @@ -188,19 +188,18 @@ def _section_lines(sections: list[Mapping[str, object]]) -> list[str]:
def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object]]) -> list[str]:
"""Build the footer: per-role rehearsal priorities and the export focus."""
lines: list[str] = []
priorities: list[str] = []
priorities: dict[str, None] = {}
for section in sections:
for role in _section_roles(section):
name = _role_display_name(role)
priority = role.get("rehearsalPriority")
if name is None or not isinstance(priority, str) or not priority:
continue
entry = f" - {name}: {priority}"
if entry not in priorities:
priorities.append(entry)
priorities[entry] = None
if priorities:
lines.append("Priorities:")
lines.extend(priorities)
lines.extend(priorities.keys())
summary = song.get("exportSummary")
if isinstance(summary, Mapping):
headline = summary.get("headline")
Expand Down
39 changes: 39 additions & 0 deletions services/analysis-engine/tests/test_chart_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,42 @@ def test_path_like_fields_never_reach_output(self) -> None:
assert "secret-demo" not in text
assert "/Users" not in rows_json
assert "secret-demo" not in rows_json

def test_deduplication_preserves_first_occurrence_order(self) -> None:
"""Deduplication uses dictionary keys to preserve insertion order exactly."""
song = _demo_song()
section = song["sections"][0]
# Set up a sequence that will break an unordered set implementation
# and test deduplication across disjoint identical values.
section["roles"] = [
_role("bass", "Bass", "Walk up"),
_role("drums", "Drums", "Hit on 1"),
_role("keys", "Keys", "Hold chord"),
_role("bass-copy", "Bass", "Walk up"),
_role("drums-copy", "Drums", "Hit on 1"),
_role("keys-copy", "Keys", "Hold chord"),
]
for role in section["roles"]:
role["rehearsalPriority"] = "medium"
section["roles"][0]["rehearsalPriority"] = "high"
section["roles"][2]["rehearsalPriority"] = "low"

section["partGraph"] = [
{"role_id": "bass", "is_active": True, "handoff_to": [], "handoff_from": []},
{"role_id": "drums", "is_active": True, "handoff_to": [], "handoff_from": []},
{"role_id": "keys", "is_active": True, "handoff_to": [], "handoff_from": []},
{"role_id": "bass", "is_active": True, "handoff_to": [], "handoff_from": []},
{"role_id": "drums", "is_active": True, "handoff_to": [], "handoff_from": []},
{"role_id": "keys", "is_active": True, "handoff_to": [], "handoff_from": []},
]
Comment thread
seonghobae marked this conversation as resolved.
Resolved

rows = build_cue_sheet_rows(song)
# Order should be exact and duplicates removed
assert rows[0]["roles"] == ["Bass", "Drums", "Keys"]
assert rows[0]["cue"] == "Walk up; Hit on 1; Hold chord"

text = build_chart_text(song)
# Priorities deduplication
assert " - Bass: high" in text
assert " - Drums: medium" in text
assert " - Keys: low" in text
Loading