statusline: per-render Node cold-start dominates CPU cost (~300ms/call, ~96% Node bootstrap not script logic)
Summary
gsd-statusline.js spawns a brand-new Node process on every statusline render (Claude Code calls the configured statusLine command continuously per active session). Profiled with node --prof against a real install (v1.14.0): each invocation costs ~300ms wall time, but only ~3.4% of that is the script's own JS logic. The rest is Node process cold-start — V8 isolate init plus compiling built-in modules (child_process, fs/promises, the internal module loader) from scratch on every call.
With 10-15 concurrent long-running Claude Code sessions (a real workload on a shared dev box), this adds up to a continuous, non-trivial aggregate CPU tax that shows up in top/load average even though no GSD workflow is active — easy to mistake for a runaway process.
Evidence
node --prof-process bottom-up profile of a single invocation (88 ticks total):
44 50.0% UNKNOWN
20 45.5% JS: ^compileForInternalLoader node:internal/bootstrap/realm:383:27
20 100.0% JS: ^requireBuiltin node:internal/bootstrap/realm:420:24
6 30.0% JS: ~<anonymous> node:internal/child_process:1:1
6 100.0% JS: ^compileForInternalLoader ...
4 20.0% JS: ~<anonymous> node:internal/fs/promises:1:1
4 100.0% JS: ^compileForInternalLoader ...Wall-clock for one call:
$ time (echo '{...}' | node hooks/gsd-statusline.js)
real 0m0.327s
user 0m0.111s
sys 0m0.074sOne concrete, low-risk lever
gsd-statusline.js does const childProcess = require('child_process'); unconditionally at the top of the file, but the only consumers of childProcess (readGitStatus, readStateHeadCommits) are gated behind opt-in config flags (statusline.show_git, statusline.show_state_freshness) that are off by default. Lazy-requiring child_process only inside those two functions would skip compiling that built-in on the common path (default config, most projects). It's a small trim, not a fix for the dominant cost.
The actual fix is architectural
The bulk of the cost is inherent to "spawn a fresh Node process per render." Options that would move the needle:
- A persistent statusline daemon/server that Claude Code's
statusLinecommand talks to (socket/pipe) instead of a cold process per call. - A lighter entry point that avoids requiring the full
../gsd-core/bin/lib/*.cjstree (semver-compare, package-identity, state-document, active-workstream-store, planning-workspace) for the common render path.
Environment
@opengsd/gsd-corev1.14.0- Node (system default,
/usr/bin/node) - Global install, Claude Code runtime
- Linux, 8-core VPS, 10-15 concurrent Claude Code sessions typical
Happy to share the full --prof-process output if useful.
Source: open-gsd/gsd-core