Skip to content

soak: what the long-running modes cost after the first minute, measured - #300

Merged
sanlee-ys merged 2 commits into
mainfrom
soak-instrument
Aug 18, 2026
Merged

soak: what the long-running modes cost after the first minute, measured#300
sanlee-ys merged 2 commits into
mainfrom
soak-instrument

Conversation

@sanlee-ys

Copy link
Copy Markdown
Owner

Everything the harness measured before this PR was one call. ci.yml times 15
statusline samples, and every eval fixture asserts a single render. The listener
modes and the council room do not stop after one call, so no gate answered the
question they raise: does an idle process hold a constant amount of memory across
a day? This PR adds the instrument and records the first measurement.

The instrument

tools/soak.ps1 has three modes. Residency mode samples one process tree at an
interval, and it records the working set, the private bytes, the handle count,
the live CPU and the child count. Per-fire mode starts a short-lived process many
times, because telltale statusline has no residency to sample. Summary mode
renders a finished JSONL file again, so the samples stay the artifact and a later
reader needs no second soak.

Three decisions in the script are load-bearing:

  • It queries Win32_Process rather than Get-Counter. The counter set names of
    Get-Counter are localized, and its instances carry process names rather than
    pids, so it cannot identify a tree.
  • It prints a median-based drift beside the least-squares slope, because one
    outlier moves a slope and no outlier moves a median.
  • It prints an absent figure as -- and never as 0, which is the zero-vs-absent
    rule from ADR-001 applied to the instrument itself.

Conditions

Intel i7-7700K, Windows 11, Windows PowerShell 5.1, Go 1.26.5. Both listeners
bound non-default loopback ports (14519 and 14318), and every arm ran with
USERPROFILE redirected to a scratch directory. The isolation is verified in both
directions: the listeners wrote their stores under the scratch home, and
~/.telltale held no file newer than the previous day when the arms ended.

What the arms found

Both idle listeners held 128 handles and 1 process across 210 samples over
34.8 min. The robust drift is 0.00 MiB on every metric, and idle CPU sat under the
measurement floor at 0.02 s and 0.00 s of total CPU. Neither listener leaks.

The statusline ran 1000 fires at a median of 20.9 ms, all exiting 0 and all
rendering the Opus marker, with no drift in cost. Its tail is the finding:
6 fires of 1000 blocked for about 2.03 s while their CPU stayed at the usual
15-31 ms, so the process waited and did not compute. The arm records that result
and deliberately does not name a cause, because two residency arms polled the same
machine throughout.

The full tables, and an explicit list of what the measurement does not assert,
are in the docs/design.md §5 amendment.

Owed

The council arm cannot run headlessly. The room is a TUI that needs a real
terminal, and its seats are live vendor CLIs that read credentials from the real
home directory, so it cannot use the redirected USERPROFILE that isolated the
other three arms. The amendment names it as owed and states the exact two-terminal
operator command.

Verification

go vet ./... passes on the merged tree. go test ./internal/hud/... ./internal/council/... passes, which is what covers the doc/code sync assertions
over docs/design.md. The soak is an operator instrument and not a CI gate; no
workflow runs it.

🤖 Generated with Claude Code

sanlee-ys and others added 2 commits August 18, 2026 08:57
Everything the harness measured before this commit was one call. The listener
modes and the council room do not stop after one call, and no gate measured
whether an idle process holds a constant amount of memory across a day.

tools/soak.ps1 is the instrument. Residency mode samples one process tree at an
interval and records the working set, the private bytes, the handle count, the
live CPU and the child count. Per-fire mode starts a short-lived process many
times, because the statusline has no residency to sample. Summary mode renders a
finished JSONL file again, so the samples stay the artifact.

The script uses Win32_Process rather than Get-Counter, because the counter set
names of Get-Counter are localized and its instances carry process names instead
of pids. It guards against pid reuse on every sample. It prints a median-based
drift beside the least-squares slope, and it prints an absent figure as `--`
rather than as 0.

The first measurement is recorded in design.md, in the section 5 area. Both idle
listeners held 128 handles and 1 process across 210 samples over 34.8 min, with
a robust drift of 0.00 MiB and idle CPU under the measurement floor. The
statusline ran 1000 fires at a median of 20.9 ms with no drift. Its tail is the
finding: 6 fires of 1000 blocked for about 2.03 s while their CPU stayed normal,
so the process waited and did not compute. The arm records that result and does
not name a cause.

The council arm is owed. A room needs a real terminal, and its seats read
credentials from the real home directory, so it cannot run headlessly. The
amendment states the operator command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sanlee-ys
sanlee-ys merged commit 4fe3545 into main Aug 18, 2026
5 checks passed
@sanlee-ys
sanlee-ys deleted the soak-instrument branch August 18, 2026 13:03
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