Performance bucket from audit (#531): O(n²) model build, SSE rebuild thrash, log buffer#540
Conversation
…sist api key (#514) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #514 fix declared two local copies of the '********' sentinel; import the existing exported REDACTED_SENTINEL from models/config instead, so the backend-coupled value lives in one place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-pair toggle errors Review found the #516 commit only covered the SSE-handler JSON.parse; the issue's other acceptance criteria were unaddressed. Complete them: - autoqueue.service: guard the patterns JSON.parse (log + empty fallback, no throw) - logs page: distinguish a history-fetch failure from an empty result via a historyLoadFailed flag + a distinct "failed to load" template state - path-pairs: enable/auto-queue toggle failures now set an error banner and refresh() to reconcile the one-way-bound checkbox with the server Tests added for each. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeQL flagged the #514 sessionStorage persistence of web.api_key as high-severity clear-text storage of sensitive information. There is no safe way to auto-recover the key after a reload (the auth-exempt /server/config/get redacts it; an authenticated endpoint recreates the deadlock), so drop the client-side persistence entirely. Kept: the core deadlock fix — config is still bootstrapped independent of the SSE stream (UI renders instead of hanging), the redacted sentinel is never sent as a credential, and reconnect backoff stays. After a reload on an auth-enabled instance the user re-enters the key in Settings (which re-pushes it to the stream). Removed the sessionStorage read/write, StorageKeys.API_KEY, and the interceptor's persisted-key lookup, plus the persistence tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #516 "failed to load" banner reused class="record error", which the Playwright log-record selector (#logs .record) matched — so when the history fetch errored in CI, the banner was read as a log record (and an ERROR-level one), breaking logs E2E tests. Give it a dedicated history-load-error class so it never collides with actual log-record queries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…move (#521) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review found the #522 trackBy keyed by time+logger+message, which collides for identical repeated log lines in the same millisecond (common under DEBUG sync) -> Angular NG0955 duplicate-key reconcile drops/mis-associates rows, an observable regression vs the old track $index. Replace with a monotonic sequence id per record/entry *object* (WeakMap, assigned lazily in trackBy): unique even for identical content, stable per object, and still enables DOM reuse on front-trim. Adds a duplicate-identical-lines render guard. The ring-buffer cap (the primary unbounded-memory fix) is unchanged. The other two #522 acceptance criteria (CDK virtual scroll, batched change detection) are deferred to a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR introduces structured error-handling via file-action event payloads, defensive JSON parsing across SSE services, view-file emission coalescing with object-identity optimization, logs-page buffer management and stable tracking, exponential-backoff stream reconnection, credential-redaction guards in config/interceptor, improved settings toggle error recovery, and a Python iterator API for efficient model traversal. ChangesAngular Frontend Resilience & Performance
Python Backend Iterator API
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- config.service: a redacted /server/config/get refresh no longer clobbers the real api_key in the snapshot, which the apiKeyInterceptor reads for REST auth (would break authed REST calls mid-session). Preserve the real key when the refresh returns the sentinel. + regression test. - model-file.service: the SSE guard wrapped only JSON.parse; a valid-JSON but wrong-shape event (model-init not an array, model-added missing new_file) still threw in the mapping. Guard the dispatch too (log + skip). + wrong-shape tests. - logs page: role="alert" on the history-load error banner (a11y announcement). - logs spec: mock LoggerService in TestBed; add a real debounced-pipe fetch-failure test (fake timers) that fails if catchError stops setting the flag. - config spec: use the REDACTED_SENTINEL constant instead of literal "********". - stream-dispatch spec: Math.random() mocked to 0.999 (it's [0,1), never 1). Skipped (already resolved by the earlier persistence removal): the CodeQL clear-text-storage alert, "don't persist the API key", and the sessionStorage descriptor restore — that code path no longer exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ket-531 # Conflicts: # src/angular/src/app/pages/logs/logs-page.component.spec.ts
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/angular/src/app/pages/logs/logs-page.component.ts (1)
89-99:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSet
pendingScrollToBottombeforedetectChanges()so live tail auto-scroll isn’t missed
src/angular/src/app/pages/logs/logs-page.component.tscallsthis.changeDetector.detectChanges()beforethis.pendingScrollToBottom = true. SincengAfterViewChecked()only scrolls whenpendingScrollToBottomis already true, the “tail visible” case can miss scrolling for that record update.🔧 Proposed fix
this.records = next.length > LogsPageComponent.MAX_LIVE_RECORDS ? next.slice(next.length - LogsPageComponent.MAX_LIVE_RECORDS) : next; - this.changeDetector.detectChanges(); - if (shouldScroll) { this.pendingScrollToBottom = true; } + this.changeDetector.detectChanges(); this.refreshScrollButtonVisibility();Add a regression spec in
src/angular/src/app/pages/logs/logs-page.component.spec.ts(under the existing#522live-log describe) that:
- stubs
component.logTail.nativeElement.getBoundingClientRect()to be inside the viewport- forces the host
offsetParentto be non-null- spies on
window.scrollTo- emits one live record via the existing
logs$subject- asserts
scrollTois called as a result of that emission (no extra manualfixture.detectChanges()needed).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/angular/src/app/pages/logs/logs-page.component.ts` around lines 89 - 99, Move the assignment to pendingScrollToBottom so it happens before changeDetector.detectChanges(): when appending the new record in LogsPageComponent set this.pendingScrollToBottom = true (if shouldScroll) and only then call this.changeDetector.detectChanges(), ensuring ngAfterViewChecked() will see the flag and trigger scrolling; also add a regression spec in the LogsPageComponent test: stub component.logTail.nativeElement.getBoundingClientRect() to return an in-viewport rect, ensure the host element reports a non-null offsetParent, spy on window.scrollTo, emit one live record on the existing logs$ subject, and assert window.scrollTo was called.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/angular/src/app/pages/files/file.component.ts`:
- Around line 150-152: actionEvent currently returns a clearActiveAction
callback that closes over the component instance and can clear a different row
when the component is recycled under *cdkVirtualFor; fix by making the callback
verify the original file identity before mutating activeAction. Modify
actionEvent(FileComponent::actionEvent) to capture a stable identifier from the
passed ViewFile (e.g. file.id or the file object reference) and return
clearActiveAction that checks this.activeAction?.fileId === capturedId (or
this.activeAction?.file === capturedFile) before calling
this.clearActiveAction() or clearing activeAction; update clearActiveAction if
needed to accept/compare an id instead of unconditionally clearing. Ensure
references to FileActionEvent, ViewFile, actionEvent and activeAction are used
so the check is performed on the original file identity.
In `@src/angular/src/app/services/files/view-file.service.ts`:
- Around line 70-85: When rebuilding pairName on existing view files, also
reapply the current sort so ordering stays consistent: in the branch where you
compute nextFiles and detect changed, sort nextFiles with the component's sort
comparator (e.g. call nextFiles.sort(this.sortComparator) or use the existing
sort routine) before assigning this.files = nextFiles and calling
this.pushViewFiles(); this ensures updates to pairName immediately reflect any
sort-by-pairName ordering.
In `@src/angular/src/app/services/settings/config.service.ts`:
- Around line 133-139: The current check uses a truthy test on currentKey so
cleared values ('' or null) are treated as absent and get overwritten by
REDACTED_SENTINEL; change the condition to detect presence of an api_key field
instead of truthiness. In config.service.ts, where currentKey is retrieved from
this.configSubject.getValue()?.web?.api_key and compared against
REDACTED_SENTINEL, replace the truthy check (currentKey && ...) with an
existence check (e.g.,
Object.prototype.hasOwnProperty.call(this.configSubject.getValue()?.web ?? {},
'api_key') or checking for !== undefined) so that empty-string or null stored
keys are preserved when incoming.web?.api_key === REDACTED_SENTINEL.
---
Outside diff comments:
In `@src/angular/src/app/pages/logs/logs-page.component.ts`:
- Around line 89-99: Move the assignment to pendingScrollToBottom so it happens
before changeDetector.detectChanges(): when appending the new record in
LogsPageComponent set this.pendingScrollToBottom = true (if shouldScroll) and
only then call this.changeDetector.detectChanges(), ensuring
ngAfterViewChecked() will see the flag and trigger scrolling; also add a
regression spec in the LogsPageComponent test: stub
component.logTail.nativeElement.getBoundingClientRect() to return an in-viewport
rect, ensure the host element reports a non-null offsetParent, spy on
window.scrollTo, emit one live record on the existing logs$ subject, and assert
window.scrollTo was called.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b35bae78-b10d-45dd-84d8-cddea87c0d9b
📒 Files selected for processing (33)
src/angular/src/app/app.config.tssrc/angular/src/app/pages/files/bulk-action-bar.component.spec.tssrc/angular/src/app/pages/files/bulk-action-bar.component.tssrc/angular/src/app/pages/files/file-list.component.spec.tssrc/angular/src/app/pages/files/file-list.component.tssrc/angular/src/app/pages/files/file.component.spec.tssrc/angular/src/app/pages/files/file.component.tssrc/angular/src/app/pages/logs/logs-page.component.htmlsrc/angular/src/app/pages/logs/logs-page.component.scsssrc/angular/src/app/pages/logs/logs-page.component.spec.tssrc/angular/src/app/pages/logs/logs-page.component.tssrc/angular/src/app/pages/settings/path-pairs.component.spec.tssrc/angular/src/app/pages/settings/path-pairs.component.tssrc/angular/src/app/services/autoqueue/autoqueue.service.spec.tssrc/angular/src/app/services/autoqueue/autoqueue.service.tssrc/angular/src/app/services/base/stream-dispatch.service.spec.tssrc/angular/src/app/services/base/stream-dispatch.service.tssrc/angular/src/app/services/files/model-file.service.spec.tssrc/angular/src/app/services/files/model-file.service.tssrc/angular/src/app/services/files/view-file.service.spec.tssrc/angular/src/app/services/files/view-file.service.tssrc/angular/src/app/services/logs/log.service.spec.tssrc/angular/src/app/services/logs/log.service.tssrc/angular/src/app/services/server/server-status.service.spec.tssrc/angular/src/app/services/server/server-status.service.tssrc/angular/src/app/services/settings/config.service.spec.tssrc/angular/src/app/services/settings/config.service.tssrc/angular/src/app/services/utils/api-key.interceptor.spec.tssrc/angular/src/app/services/utils/api-key.interceptor.tssrc/python/controller/model_builder.pysrc/python/model/file.pysrc/python/tests/unittests/test_controller/test_model_builder.pysrc/python/tests/unittests/test_model/test_file.py
- file.component: clearActiveAction is now file/action-aware. <app-file> is recycled by *cdkVirtualFor, so a late failure callback could clear the wrong row once the instance was rebound to another file/action. Guard it to clear only when the instance still represents the same file and in-flight action. - view-file.service: re-apply the sort comparator after a pairName change. The #521 pairName-update branch updated rows but never re-sorted, so a pairName sort went stale until the next model diff. - config.service: preserveRealApiKey now also preserves a *cleared* (empty) key. The backend redacts api_key unconditionally, so the previous truthy guard let a refresh turn a cleared key back into a phantom "********" secret. Tests added for each. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the ⚡ Performance bucket of tracking issue #531 (3 issues) — behavior-preserving optimizations with tests.
Note
Stacked on #538 (frontend bucket): #521/#522 touch the logs/file-list code #538 also changed, so this branch builds on it. GitHub will auto-retarget this PR to
developonce #538 merges, at which point the diff shows only these performance commits. Intentional multi-issue grouping (per the maintainer's request), one commit per issue.How this was built
A multi-agent workflow: parallel investigation → sequential implementation (one tested commit per issue) → parallel adversarial review, with the review explicitly checking each optimization is strictly behavior-preserving (identical output, only faster), not just that tests pass.
Issues fixed
build_model()(every 0.5s on the controller thread) usedlist.pop(0)in two per-file BFS traversals → O(n²) on large trees, plus acopy.copyof each node's child list. Nowcollections.deque+popleft()(O(1)) and a non-copyingiter_children()on the hot read-only path. Byte-identical model output; big asymptotic win on deep/wide seedbox trees.ViewFileServicerebuilt the whole file list on every SSE update. Now coalesces SSE-driven rebuilds, memoizes the filter, and preservesViewFileobject identity for unchanged rows so OnPush/trackBycan skip them; single-pass bulk remove. Same rendered view (files, order, checked/selected state).trackBy.Review findings → resolution
behaviorPreserved=true, all acceptance criteria met.trackBykeyed bytime+logger+message, which collides for identical repeated log lines in the same millisecond (common under DEBUG sync) → Angular NG0955 duplicate-key reconcile drops/mis-associates rows. Fixed with a per-object monotonic seq key (WeakMap) — unique even for identical content, stable per object. Added a duplicate-identical-lines render guard.Test plan
ng lint --max-warnings 0clean.test_scan_file_with_latin_chars, is a pre-existing macOS-onlyErrno 92that passes on CI's Linux runner);ruff check+C901(complexity ≤ 12) clean.Closes #520. Closes #521. Closes #522.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Style