Skip to content

perf(coding-agent): cache materialized session views behind mutation counter - #257

Merged
code-yeongyu merged 3 commits into
mainfrom
perf/render-hotpath-cache
Jul 21, 2026
Merged

perf(coding-agent): cache materialized session views behind mutation counter#257
code-yeongyu merged 3 commits into
mainfrom
perf/render-hotpath-cache

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Jul 21, 2026

Copy link
Copy Markdown
Owner

What

Bug A from the render-hotpath plan: the interactive footer re-materialized the entire session 3x per frame at ~60fps (getEntries/getBranch/getSessionName each deep-copying a 10 MB / 724-entry session via ResidentStringStore.materialize). Profiled at 33.4% self-time in transformJsonValue on a 17h session burning 50-86% CPU idle.

Fix

  • SessionManager mutation-counter caching: memoized getEntries()/getBranch() (keyed on leafId+mutationCount), O(1) getSessionName()
  • Incremental getUsageTotals() maintained on assistant-message append; footer consumes it (legacy loop deleted)
  • Robustness: usage-less assistant entries (aborted/error turns in older session files) contribute nothing

Measured evidence (real CLI, 10 MB session, tmux)

  • Idle CPU: 50-86% -> 0.0% x5 samples (t3-perf-qa.txt)
  • sample(1) top-25 self-time: transformJsonValue ABSENT (was 33.4%)
  • footer-width.test.ts byte-identical output guard green
  • Cross-fix live QA (F2): first footer render 2.12s, prompt round-trip 0.29s, idle CPU 0.0% x10 over 10 min (f2-live-qa.txt)

Evidence

Plan: .omo/plans/senpi-render-hotpath-and-startup-fixes.md

  • t1-vitest.txt / t1-vitest-red.txt (RED->GREEN mutation-counter cache)
  • t2-vitest.txt / t2-vitest-red.txt (RED->GREEN usage totals equality)
  • t3-perf-qa.txt (manual perf QA, PASS)
  • pr1-full-gate.txt (gates + environmental flake characterization: origin/main itself fails 74/284 mcp tests under local parallel load; CI is the authoritative full gate)

Plan: .omo/plans/senpi-render-hotpath-and-startup-fixes.md

SessionManager maintains running usage totals (input/output/cacheRead/
cacheWrite/cost/latestCacheHitRate) incrementally on assistant-message
append and rebuilds them in _buildIndex()/newSession, exposed via
getUsageTotals(). The footer no longer scans getEntries() every frame;
rendering output is byte-identical (footer-width.test.ts green, and a
new totals-equality test proves deep equality with the legacy loop,
including latestCacheHitRate semantics and all-entries-not-branch-scoped
behavior).

Evidence: .omo/evidence/senpi-render-hotpath-and-startup-fixes/t2-vitest-red.txt, t2-vitest.txt
@code-yeongyu

Copy link
Copy Markdown
Owner Author

Todo 2 (incremental usage totals) landed in 40d98ec: SessionManager.getUsageTotals() maintained incrementally on assistant-message append, rebuilt in _buildIndex()/newSession; footer hot path no longer scans getEntries() per frame. TDD: totals-equality test (legacy loop vs getUsageTotals(), deep equality incl. latestCacheHitRate + all-entries semantics) captured RED then GREEN; footer-width.test.ts green with zero output diffs (125/125 tests passing across footer-width + session-manager suites). Evidence: .omo/evidence/senpi-render-hotpath-and-startup-fixes/t2-vitest-red.txt and t2-vitest.txt.

@code-yeongyu

Copy link
Copy Markdown
Owner Author

CI failure root cause + fix (commit 9bc68d9)

The 3 CI failures (footer-token-format, startup-session-name x2) traced to two gaps from the incremental-usage-totals change:

  1. session-manager.ts _accumulateUsage dereferenced entry.message.usage unconditionally. Older session files contain assistant entries with no usage (aborted/error turns), which crashed on load and broke session-start paths (startup-session-name). Fixed: early-return when usage is undefined — those entries contribute nothing to the running totals.
  2. footer-token-format.test.ts mocked sessionManager without the new getUsageTotals method the footer hot path now calls. Fixed: mock returns totals matching the fixture transcript.

Verification

  • Targeted vitest (green, 16 files / 133 tests): test/footer-token-format.test.ts test/startup-session-name.test.ts test/session-manager test/footer-width.test.ts test/suite/app-server-daemon.test.ts
  • Pre-commit gate (npm run check: biome, tsgo, shrinkwrap/install-lock, neo build+vet+test) green on the commit hook.

Environmental note (local full-suite flakiness, pre-existing)

Local full-suite runs on a loaded parallel machine flake in spawn-heavy suites unrelated to this PR (app-server daemon/stdio CLI spawns, resource-loader, mcp). Confirmed pre-existing: plain origin/main itself fails 74/284 test/mcp tests under the same local parallel load. One earlier local failure was also caused by a leaked daemon from a killed run holding port 18999. Per vitest.config.ts, the serialized CI pool is the deterministic configuration, so GitHub CI is the authoritative full gate for this PR.

Evidence: /Users/yeongyu/local-workspaces/.omo/evidence/senpi-render-hotpath-and-startup-fixes/pr1-full-gate.txt

@code-yeongyu

Copy link
Copy Markdown
Owner Author

CI failures fixed (commit 9bc68d9)

Root causes — two distinct issues behind the 3 failing tests:

  1. test/footer-token-format.test.ts — its sessionManager mock was still built around getEntries() and lacked getUsageTotals() after Todo 2 moved footer.ts to sessionManager.getUsageTotals(). Fixed by adding a getUsageTotals mock (same legacy-formula pattern as test/footer-width.test.ts).

  2. test/startup-session-name.test.ts (both tests)not a mock problem; a real production regression. SessionManager._accumulateUsage() assumed every persisted assistant message has usage, but session files can contain usage-less assistant entries (e.g. aborted/error turns — the test fixture writes one). The legacy footer loop only ran in interactive mode, so these files loaded fine before; the new load-path accumulation crashed with "Cannot read properties of undefined (reading 'input')" during print-mode startup, so --name never got appended. Fixed with a minimal guard in _accumulateUsage (skip usage-less entries), matching legacy semantics.

Gate result: full gate green from worktree root — npm run check && CI=true npm run test exits 0. coding-agent: 462 passed / 4 skipped (466 files), 4006 passed / 31 skipped (4037 tests); all other workspace packages pass. (CI=true selects the repo's serialized forks pool per vitest.config.ts guidance for subprocess-heavy suites.)

Note: local reruns were hampered by extreme machine load (loadavg ~180) which flaked pre-existing subprocess tests (app-server-daemon, app-server-stdio, skills-sidecar) — those pass standalone and are unrelated to this change.

@code-yeongyu
code-yeongyu merged commit 23f560c into main Jul 21, 2026
12 of 14 checks passed
@code-yeongyu
code-yeongyu deleted the perf/render-hotpath-cache branch July 21, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant