perf: reduce per-repaint CPU under concurrent sessions - #2
Merged
Merged
Conversation
With several concurrent Claude Code sessions every statusline invocation is a fresh process, so per-render fixed costs multiply by session count. - terminal: probe each ancestor with a single `ps -o ppid=,tty=` call instead of two spawns per ancestor; the walk runs per render on macOS where numeric widths are deliberately not persisted - usage-fetch, claude-service-status: load https-proxy-agent (and its transitive graph) on demand, only when a proxy is actually configured Benchmark (13MB transcript, 4 parallel workers x 20 renders via stdin): 27.2 -> 20.6 CPU-seconds total, 0.34 -> 0.26 CPU-s per render (-24%), wall 7.5s -> 5.6s. node --cpu-prof shows the undici/proxy graph gone from startup (compileForInternalLoader 40.7ms -> 7.5ms per render). Co-Authored-By: Claude Code <noreply@anthropic.com>
axisrow
commented
Sep 25, 2026
axisrow
left a comment
Owner
Author
There was a problem hiding this comment.
Verdict: Approve — not requesting changes.
The two per-repaint cost reductions are correct and well scoped; CI (build, lint + type check, tests) is green on the head commit.
Checked:
- terminal.ts: the merged
ps -o ppid=,tty=output parses cleanly (trim + whitespace split handles the right-aligned ppid column;?,??and empty tty map to null;ppid=0breaks the walk without extra spawns). StillexecFileSyncwith an args array — no shell, no new injection surface. - Error handling survives the async refactor: both options builders wrap the dynamic import in try/catch and cannot reject (the proxy-url getters are pure env reads), so the promise chain in
fetchStatusPagePathand the pre-awaited options infetchFromUsageApihave no unhandled-rejection path, and null options still short-circuit to the same results as before. - Node 14 dual-runtime: the
import typeis fully erased,await importfrom CJS works on Node >= 12.17, and the dependency stays declared in package.json (no manifest change). - The base-options +
pathoverride plumbing infetchStatusPagePathis behaviorally equivalent to the old inline construction.
Two minor non-blocking notes as inline comments.
…wiring - terminal: check the ancestor TTY before reassigning pid so each loop iteration reads as "one ps answer, one pid"; no call-order change - claude-service-status: cover the lazy proxy-agent branch with tests (agent attached when HTTPS_PROXY is set, absent otherwise), matching the usage-fetch proxy coverage Co-Authored-By: Claude Code <noreply@anthropic.com>
…ation - ps: use two -o flags instead of the comma list; FreeBSD's parser treats everything after the first '=' as one header, which collapsed the combined form into a single bogus column and dropped the probe to the tput fallback. Still one spawn per ancestor. - walk: start at process.ppid and read each generation's TTY before terminating on its PPID, restoring the old two-spawn walk's generation coverage (1..8) and keeping a dead-end PPID from hiding a valid width on the same ps row. Known narrowing vs the old sequence: if the combined lookup fails entirely, the walk stops (the old two-call flow could still continue on the separately obtained PPID). Co-Authored-By: Claude Code <noreply@anthropic.com>
axisrow
commented
Sep 25, 2026
axisrow
left a comment
Owner
Author
There was a problem hiding this comment.
Verdict: Approve — not requesting changes. Follow-up on the earlier review (id 5314558675): both minor findings are addressed, and the extra hardening is correct.
- Walk parity restored: starting from
process.ppid(a free read) covers ancestor generations 1-8 exactly like the original two-spawn walk, and reading the current generation TTY before breaking on a terminal PPID is a strict improvement over main — a PPID of 0 no longer hides a width sitting on the same ps row. Covered by a dedicated test. - The two
-oflags form is the right portable choice: on FreeBSD the header-replacement=consumes the rest of the-oargument, so the comma list would collapse into one bogus column; two flags produce the same whitespace-split row on macOS and Linux, so the parsing path is unchanged. - New proxy tests in claude-service-status.test.ts lock in both branches of the lazy-import wiring (agent attached with HTTPS_PROXY set, absent without) with proper env restore in afterEach.
- CI green (build, lint + type check, tests) on head 0fc829c; the PR description was updated to match the final ps format and walk behavior.
Nothing further blocking.
axisrow
added a commit
that referenced
this pull request
Sep 25, 2026
The fork's main had PR #2's pre-review revision (177686f); this brings in the final revision from ao/ccstatusline-2/root: per-repaint subprocess and module-graph cost cut (sirmalloc#397), the width-walk invariant clarification, and the BSD-safe two-flag ps format with restored walk parity (0fc829c). Conflicted on src/utils/terminal.ts: kept the TTY-probe-before-break ordering from the final revision and dropped the obsolete duplicate check after the parentPid break. Co-Authored-By: Claude Code <noreply@anthropic.com>
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the per-repaint CPU cost that multiplies under concurrent Claude Code sessions (context: upstream issue sirmalloc#397 — every statusline repaint spawns a fresh node process, so fixed per-process costs scale with session count). Two shared-path fixes, no new dependencies:
src/utils/terminal.ts— the macOS/BSD terminal-width ancestor walk probes each ancestor with a singleps -o ppid= -o tty= -p PIDspawn instead of two spawns (ps -o ppid=+ps -o tty=). The walk still covers ancestor generations 1–8 (process.ppidstarts it for free) and reads each generation's TTY before terminating on its PPID, matching the old walk's coverage and precedence. Two separate-oflags rather than a comma list: FreeBSD'spsparser treats everything after the first=as a single header, which would collapse the comma form into one bogus column.src/utils/usage-fetch.ts,src/utils/claude-service-status.ts—https-proxy-agent(and its proxy-chunk module graph) is now loaded via dynamic import only when a proxy is actually configured. Previously it was compiled into every startup of every render invocation.Behavior notes (deliberate, reviewed in-thread)
pslookup fails outright, the walk stops there. The old two-call flow could still continue upward using the separately obtained PPID. This required synthetic failure fixtures to observe.Benchmark
Synthetic 13 MB transcript, 4 parallel workers × 20 renders via stdin, isolated HOME, built dist on Node. Initial measurements on the author's host: CPU per render 0.34 s → 0.26 s (−24%), p50 373 ms → 224 ms. These were not reproduced by independent verification on a shared development host, which measured CPU 268.8 → 247.9 ms per render (−7.8%) and p50 283.7 → 239.9 ms on the same workload.
What independent verification confirms structurally:
psspawn per ancestor instead of two (2 → 1 with an inherited caller TTY; 16 → 8 when no TTY is found through the chain).HTTPS_PROXYset the request resolves throughHttpsProxyAgent, and an invalid proxy URL degrades safely. These failure/agent paths were confirmed on real Node 14.21.3 as well as Node 25 and Bun.compileForInternalLoaderper render: 42.45 ms → 35.91 ms mean (8 × 500 µs-sampled profiles) — improved, but the initially claimed 40.7 → 7.5 ms was not reproduced.undicistartup load comes from the eager Yoga/widget-editor route (an upstream perf: keep the TUI off the status line render path sirmalloc/ccstatusline#575 artifact), not from the proxy change; deferring that path is a separate follow-up.Tests
bun test: 2361 pass / 0 fail (terminal width mocks updated to the single-pstwo-flag format; a regression test pins that a terminal PPID does not hide the same row's width; new proxy-agent wiring tests for the status page fetch: agent attached whenHTTPS_PROXYis set, absent otherwise)bun run lint: clean (tsc --noEmit + eslint, no rule suppressions)Risks / follow-ups
??=syntax, so the bundled dist does not actually run on Node 14 despite the target version; unrelated to this PR's changes.Related: sirmalloc#397 (upstream issue only — no upstream changes in this PR).
🤖 Generated with Claude Code