#4914·gsd-core

statusline: per-render Node cold-start dominates CPU cost (~300ms/call, ~96% Node bootstrap not script logic)

Author: exceptdevCreated Sep 21, 2026Updated Sep 21, 2026
Labelsneeds-triage

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.074s

One 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 statusLine command talks to (socket/pipe) instead of a cold process per call.
  • A lighter entry point that avoids requiring the full ../gsd-core/bin/lib/*.cjs tree (semver-compare, package-identity, state-document, active-workstream-store, planning-workspace) for the common render path.

Environment

  • @opengsd/gsd-core v1.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.