Skip to content

perf: cut redundant work from the steady cross-session rebuild - #90

Merged
bhavsarpratik merged 5 commits into
mainfrom
perf/xsession-rebuild-cost
Oct 26, 2026
Merged

bhavsarpratik merged 5 commits into
mainfrom
perf/xsession-rebuild-cost

Conversation

@bhavsarpratik

Copy link
Copy Markdown
Member

Summary

When an agent is writing its transcript, the server runs source discovery every 10 s and a forced cross-session rebuild every 15 s. This PR removes work that produced the same results on every cycle. Dashboard, menu bar and MCP output do not change.

The PR builds on main and does not depend on #88. #88 removes the cost of re-encoding the snapshot on each poll; this PR reduces the cost of building it.

  • Claude local-agent discovery (token_meter/runtimes/claude.py)
    • When an agent's own folder has no trace, the old code globbed the shared parent folder again for that agent. Here that was 38 agents re-listing one 119-entry folder.
    • The parent is now listed once per scan, uncached, and the literal <id>.jsonl name is checked with lexists in each folder.
    • That matches what glob does for a literal final component. I checked this against the CPython 3.9, 3.11 and 3.14 sources.
    • Names that contain glob characters or path separators still use the per-agent glob.
  • Claude Desktop metadata
    • Each file is parsed once per (mtime, size) signature, the same pattern as the existing cwd and activity caches.
    • Only the 8 fields the index reads are kept. The rest of the file can hold MCP configuration and permission state.
    • A full scan evicts files that have been removed.
  • Coverage and provenance (domain/aggregates.py, domain/usage.py): these no longer copy ID sets they already own or only read.
  • Matched pace (app.py): the cached windows object is returned without a deepcopy (2 MB, 35 ms). Snapshots are read-only. That rule is now documented, and I traced every consumer.
  • Agent usage (domain/agents.py): each distinct scope selection is computed once per aggregation. There were 736 _usage_body calls but only 75 distinct selections. The top-level body is still built on its own, because it is changed afterwards.

Why the aggregates are not updated incrementally: they fold sessions in order using float sums. Subtracting a session and adding it back changes the float association, so the totals would no longer match exactly.

Measurements

Setup: Python 3.9, 2,259 sources. Inputs are frozen as APFS clones of the local traces (no credential files, and no Cursor state.vscdb). Base is a git archive of main, run with the same driver.

In-process, warm, mean of two runs each

main this PR
all_session_sources() 250 ms 175 ms (−30%)
forced cross_session() 621 ms 452 ms (−27%)
capability_inventory 151 ms 50 ms
aggregate_model_stats 168 ms 140 ms

Isolated server CPU, ABAB order

  • Each run: a server on a spare port, with software_update_watcher not started.
  • A writer appends one turn to a 5 MB Claude transcript every 3 s.
  • 60 s warm-up, then 240 s measured.
Load main this PR
Dashboard tab (/state 1 s, /session 2 s) + menu bar (/menubar 2 s) 56.2% (59.5, 52.9) 49.7% (50.6, 48.9)
No clients (discovery + rebuild + current session only) 22.4% (21.6, 23.1) 20.5% (19.9, 21.0)

Notes on these numbers:

  • On main, the polled number is dominated by re-encoding the snapshot on every /state poll, which is what perf: stop re-encoding the cross-session snapshot on every poll #88 fixes.
  • With no clients, most of the remaining CPU is the whole-trace rebuild of the large active transcript. This PR does not change that.
  • The rest of aggregate_model_stats is the ordered float fold itself.

Equivalence evidence

  • Parity golden (tests/domain/test_aggregate_parity.py):
    • Covers per-section SHA-256 digests of the cross-session, daily, monthly, model-stats, provenance and agent-usage aggregates, computed on deterministic synthetic rows.
    • The digests were generated from main (fd08706).
    • Python 3.12 made float sum() compensated, so the golden stores one digest set per sum() behavior. 3.9 and 3.11 share one set; 3.12, 3.13 and 3.14 share the other.
    • The test passes on all five interpreters.
    • Mutation checks confirm it catches a provenance change, a reordering of float sums, and a leak of the shared body.
  • Provenance: the new code is compared with the old copying implementation over 400 random input shapes, and the test checks that inputs are not mutated.
  • Claude discovery: compared with a per-agent glob reference on a tree that includes sibling traces, multiple projects, hidden folders, a file where a project folder should be, a glob-magic id, and missing traces. Tests also cover:
    • listing the shared parent only once
    • freshness when a path cache is configured
    • rooted scans and eviction
    • trimming the cached fields
  • Real data: with the clock pinned, all_session_sources() and cross_session() were compared between main and this branch, once in the same process and once in back-to-back processes. Only two kinds of field differed:
    • the action token, which is random per process
    • Hermes request_revision strings, which embed a SQLite -shm mtime that changes whenever the database is opened
  • Isolated servers: on two servers run side by side, /state (8 MB) and /menubar were byte-identical apart from token and generated_at.
  • One intended difference: with a path cache configured, the shared sibling listing is never staler than main. It can find a new sibling trace sooner than main would.

Gates

  • Full suite: 1553 tests OK (17 skipped).
  • Compile and syntax: py_compile, the embedded JS parse, bash -n and git diff --check all pass.
  • Independent tester: passed. Two project reviewers (correctness and test quality; consumers, aliasing and privacy) passed after their findings were fixed in the second commit.

Test plan

  • python3 -m unittest discover -s tests
  • Parity tests on Python 3.9, 3.11, 3.12, 3.13 and 3.14
  • Real-data and isolated-server A/B against main
  • Not verified: ./scripts/install and installed-runtime parity. The installed runtime on the measuring machine belongs to another branch.
  • Not verified: behavior on Linux and Windows.

🤖 Generated with Claude Code

bhavsarpratik and others added 2 commits October 23, 2026 13:32
While an agent writes its transcript, discovery runs every 10 s and the
cross-session snapshot rebuilds every 15 s. Remove work that produced the
same results:

- Claude local-agent discovery expanded the shared sibling directory once per
  agent (38 x 119 entries); expand it once per scan and test the literal
  trace name, as glob does. Names with glob magic keep the per-agent glob.
- Parse Claude Desktop metadata once per file signature.
- Stop copying owned ID sets in model coverage and usage provenance.
- Return the matched-pace windows cache without a deepcopy; snapshots are
  read-only.
- Compute each distinct agent-usage scope selection once per aggregation.

Aggregates are not folded incrementally because float sums are order
dependent. A parity golden generated from main pins the outputs.

Co-Authored-By: Claude Code <noreply@anthropic.com>
- Keep only the indexed fields of Claude Desktop metadata in the cache; the
  rest of a file can carry MCP configuration and permission state.
- List the shared sibling directory uncached so it is never staler than the
  per-agent globs it replaced.
- Key agent scope bodies by record identity alone.
- Store parity digests per float sum() behavior: Python 3.12 made sum()
  compensated, so CI's 3.12 produces different (equally reference) digests.
- Test path-cache freshness, rooted scans, eviction, and field trimming.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@bhavsarpratik

Copy link
Copy Markdown
Member Author

Status at 1532799: one minor fix needed, then ready after an update from main.

  • CI is green on all nine checks. The PR has passed an independent tester and two reviewers.
  • Fix before merge (cross-platform): the Claude Desktop metadata cache saves a failed read (OSError) under the file's unchanged mtime and size. A temporary read error, such as a Windows sharing violation or an antivirus lock, then hides that Desktop session until the file changes or the server restarts. Don't cache OSError results. Parse errors can stay cached.
  • Integration gate: totals update exactly after a trace append with this PR's memoization, and the revision contract holds. All eight runtimes produce stable opaque keys, and the PR never changes a published snapshot.
  • Accepted, no change needed: the parity golden covers 4 of 8 runtimes, but the changed aggregate code does not branch by runtime. The dashboard edits its held snapshot in place after a save, which is harmless because the server issues a new revision.
  • Next: the fix and an update from main, then the gates rerun on the new head.
  • Not verified: real Linux and Windows hosts beyond CI.

— Pratik's agent

bhavsarpratik and others added 3 commits October 26, 2026 15:33
The metadata cache stored a failed read under the file's unchanged mtime
and size, so a locked or briefly unreadable file (a Windows sharing
violation or an antivirus scan) hid that Desktop session until the file
changed or the server restarted. Read errors are no longer cached; parse
errors still are, since they repeat until the file changes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
@bhavsarpratik
bhavsarpratik merged commit 49286a8 into main Oct 26, 2026
9 checks passed
bhavsarpratik added a commit that referenced this pull request Oct 26, 2026
Brings in the Splunk by Cisco branding (#93), the trace-path privacy fix
(#89), and the cross-session perf work (#88, #90).

- All Sessions rows now key on the server-sent trace key, since #89 removed
  the path the branch used to hash.
- Regenerate the agent_usage parity digests for the new model_runtimes and
  model_days fields; every pre-existing field still matches main's golden on
  Python 3.9 and 3.14.

Co-Authored-By: Claude Code <noreply@anthropic.com>
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