Skip to content

perf(indexing): faster cold index that yields to foreground work - #78

Merged
bhavsarpratik merged 13 commits into
mainfrom
perf/cold-index-speedup
Oct 16, 2026
Merged

bhavsarpratik merged 13 commits into
mainfrom
perf/cold-index-speedup

Conversation

@bhavsarpratik

@bhavsarpratik bhavsarpratik commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Platform scope: the parallel cold index runs on macOS only. Linux and Windows keep the existing sequential cold index (identical results) until worker processes are verified on those hosts; enable via PlatformCapabilities.parallel_index. All platforms get the matched-pace speedup. Linux keeps its tested /proc/stat/power_supply probes for later enablement; the untested Windows ctypes probes were removed.
  • Parallel cold index. The first build after install or restart used to parse every trace on one core. It now prefetches session summaries in a spawn-context worker pool, then reuses the existing sequential loop unchanged. Anything the pool doesn't store goes through the old path: too few sources, a single CPU, a worker error, or a pool that breaks or won't start. Results match the sequential path exactly.
  • Doesn't slow the machine. token_meter/services/indexing.py caps the pool at half the logical CPUs (at most 6, and 2 on battery or Low Power Mode). Every second it adjusts the number of workers from measured idle CPU (additive increase, multiplicative decrease): it halves them when less than one CPU is idle and adds one while two or more are idle. When the host is saturated, workers move to macOS Utility QoS. Measured here: lowering priority with nice alone slowed a CPU-bound foreground job by 30–60%, while Utility QoS had no measurable impact. Workers run niced with utility disk I/O, exit with the server (they watch its process handle), and run from the temp directory.
  • Platform hooks. PlatformServices gains lower_background_priority, set_background_mode, power_constrained, and cpu_activity for macOS (QoS, pmset, host_statistics), Linux (/proc/stat, power_supply), and Windows (SetPriorityClass, GetSystemTimes, GetSystemPowerStatus). Each one is best-effort: it returns unknown and never raises.
  • Memory-safe. Each worker can peak at several hundred MB on a large trace, so the pool allows one worker per 4 GB of RAM (8 GB → 2, 16 GB → 4, 24 GB+ → 6; 2 if unknown). The macOS kernel memory-pressure level (the one behind Activity Monitor's memory graph) caps the workers at half the pool on warn and one worker on critical. Because a Python worker keeps its peak memory until it exits, pressure also restarts the pool at that smaller size. Measured: forcing critical mid-build cut worker memory from about 1.5 GB to one worker (≤ ~750 MB). If the OS refuses to start a worker, a worker is killed, or a restart fails, indexing falls back to the sequential path with identical results (all covered by tests).
  • Faster, prompt-free install. When the Swift toolchain is already installed (checked with xcode-select -p, which never prompts), the macOS installer compiles the menu bar companion in the background while the server indexes, then waits only for what is left. Without a toolchain it builds after the server is ready, exactly as main does, so the installer never opens Apple's developer-tools dialog unexpectedly. Output, failure messages, smoke check and LaunchAgent order are unchanged; a failed or interrupted install stops the build and removes its temp files. Live, with a cold build: Server ready 20s, menu bar companion ready 0s after that.
  • Codex warm start. Workers receive Codex's signature-checked trace-head metadata, so they don't each reread all 1,324 rollouts to resolve lineage (2.6s → 0.06s per worker).
  • Matched-pace speedup, results unchanged. Per-turn features are computed once, a log-output band skips pairs that can't match, and the bootstrap draws are inlined using the same random stream. Output is byte-identical to main.

Results (this Mac, 2,240 sessions, heavily shared machine)

main this branch
Cold session summaries 45.1s 11.5s (2,237 settled sessions identical)
Matched-pace CPU 17.0s 8.9s (JSON byte-identical)
LaunchAgent restart → state_ready 56.5s (a second restart took 234s, likely machine load) 31–46s

Discovery (~16s) is unchanged and is the next target.

Test plan

  • python3 -m unittest discover -s tests: 1,514 tests, 0 failures (main merged in, including the test_goals fix)
  • New tests:
    • tests/integration/test_cold_index.py: fixture parity, real spawned workers including a forked Codex child, every fallback path, AIMD bound, single-flight, worker exit on parent kill.
    • tests/domain/test_matched_pace_parity.py: frozen main reference, checked against an injected change.
    • tests/contracts/test_platform_background_work.py.
    • Codex worker-state round trip.
  • py_compile, embedded JS parse, bash -n, git diff --check
  • macOS install: /health and /menubar OK, both LaunchAgents running, source/runtime parity checked, 6 workers observed. A restart with workers running left 0 behind.
  • Independent tester plus two reviewers (correctness/regressions and platform/packaging/privacy), all passed on 5990bf0.
  • Linux and Windows stay sequential: tests assert cross_session rows equal the sequential path with no executor created; suite also passes with the server platform simulated as Linux
  • Before enabling Linux: run on a Linux host (systemd user unit, containers, worker exit, priority)
  • Before enabling Windows: re-add priority handling and verify on a Windows host (hidden console, reinstall during a cold build)

🤖 Generated with Claude Code

bhavsarpratik and others added 13 commits October 15, 2026 21:05
The first index after install or restart parsed every trace on one core
(~50s here) and then spent ~17s CPU on matched-pace statistics. Cold
summaries now fill from a spawn-context worker pool that is capped at half
the CPUs (two on battery), follows measured idle CPU with AIMD, and moves
workers to macOS Utility QoS when the machine saturates so foreground work
is not slowed. Any pool failure falls back to the unchanged sequential
path. Pace matching precomputes per-turn features with identical float
operations and random draws, so results stay bit-identical.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Idle pool workers never see EOF when the server is killed, so they
outlived it and, on Windows, pinned the runtime directory a reinstall
moves. Workers now watch the parent's sentinel and run from the temp
directory. Linux ignores peripheral batteries and idle USB-C ports,
macOS keeps any low-power flag, malformed probe readings degrade to
unknown, and the in-process OpenCode exclusion is an adapter capability.

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

Worker processes on Windows (hidden-console inheritance, reinstall moves
while workers run, parent-exit handles) are unverified on a real host, so
a new PlatformCapabilities.parallel_index keeps Windows on the existing
sequential path and drops the untested ctypes probes. Windows still gets
the matched-pace speedup. Linux sizes the pool from sched_getaffinity so
containers and pinned services are not oversubscribed, and the pool is
skipped on Pythons without multiprocessing.parent_process.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…s verified

Linux enablement rested on code reading alone. Nice applies only within a
scheduler group and CPU quotas are invisible to the idle-CPU probe, so
Linux now keeps the sequential cold index like Windows until worker
processes are proven on a Linux host; its probes stay for that. Tests
exercise the parallel path on any host and assert per-platform enablement.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Each worker can peak at several hundred MB on a large trace, and the
idle-CPU throttle cannot see swapping, so six workers on an 8 GB Mac could
push it into swap. The pool now allows one worker per 4 GiB of physical
memory (two when unknown), and the kernel memory-pressure level caps the
in-flight limit at half the pool on warn and one worker on critical. Spawn
failure and a killed worker are covered by tests that fall back to
identical sequential rows.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A worker keeps its peak memory (about 500 MB after the largest trace)
until it exits, and neither gc nor allocator pressure relief returns it,
so lowering the dispatch limit alone did not relieve memory. Pressure now
drains in-flight work and restarts the pool at the capped size; a failed
restart hands the rest to the sequential path. Measured: forcing critical
mid-build dropped worker memory from about 1.5 GB to one worker.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Shrinking used the combined limit, so a CPU dip during warn pressure could
restart the pool at one worker for the rest of the build. The pool now
shrinks to the memory cap (half on warn, one on critical) while the CPU
limit still governs dispatch. Adds an end-to-end test that a failed
restart falls back to identical sequential rows.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The Swift build needs only the staged source and toolchain, yet it waited
for indexing to finish. It now starts in the background right after the
server LaunchAgent, and the installer waits only for what is left of it
before the same output replay, failure check, smoke test, and menu bar
registration. Any failure or interruption stops the build's process tree
and removes its captured output.

Co-Authored-By: Claude Code <noreply@anthropic.com>
On a Mac without the Command Line Tools, swiftc opens Apple's install
dialog; starting it in the background would show that dialog unannounced
while the installer reports indexing. The early build now runs only when
`xcode-select -p` (which never prompts) reports a developer directory with
swiftc; otherwise the build runs after the server is ready, as before. The
wait message no longer promises a minute-long compile when the build
already finished.

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

The installer tests now provide a deterministic `xcode-select -p`
developer directory so the background build does not depend on the test
host's toolchain, and the failed-build test expects the background-build
wait line after Server ready.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Keeps the background-build cleanup test from racing the 2s readiness
window on slow hosts.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@bhavsarpratik
bhavsarpratik merged commit 9a513a7 into main Oct 16, 2026
2 checks passed
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