Skip to content

Sample the benchmark under a real workload via the macterm CLI#141

Merged
thdxg merged 6 commits into
mainfrom
claude/mystifying-colden-103489-pr3
Jul 5, 2026
Merged

Sample the benchmark under a real workload via the macterm CLI#141
thdxg merged 6 commits into
mainfrom
claude/mystifying-colden-103489-pr3

Conversation

@thdxg

@thdxg thdxg commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Third of three PRs for #107, stacked on #140 (will retarget as the stack lands). Closes the loop with the CI benchmark (#129): the window-state benchmark measured an empty app — one idle shell — so regressions that only show under load (render churn, poll pressure, per-surface overhead) were invisible.

What changes

  • benchmark.py run gains --workload TABS (default off; bench.sh passes BENCH_WORKLOAD, default 2). After the three idle states are sampled exactly as before, the harness spawns TABS busy tabs through the bundled macterm CLI (tab new --run + grid 2x2 --run against <data-dir>/control.sock) and samples the same three states again as workload-focused/unfocused/minimized.
  • Workload command: /bin/sh -c "while :; do date; sleep 1; done" per pane — a real external child emitting a line a second ("logs trickling in"), negligible CPU of its own, and parses in POSIX shells and nushell (it's typed into the pane's shell verbatim).
  • Fail-hard verification: the spawn is paced (0.5s/tab — a mass simultaneous spawn is its own pathology, not the steady state this measures), then pane list --json must show exactly 1 + 4×TABS panes or the run aborts — silently benchmarking a partial workload against a full one would poison comparisons.
  • The readiness poll (macterm status until exit 0) exercises the socket's starting state; CLI failures dump the existing diagnostics (screenshot + app log).

Baseline migration — seamless by construction

The idle state keys (focused/unfocused/minimized) are unchanged, so their baseline history keeps comparing across this PR. A baseline that lacks the workload-* states renders them with absolute values, deltas, no labels, and a footnote saying deltas appear once main's baseline includes a workload run. Verified against a synthesized pre-workload baseline: no spurious benchmark:regression/improvement verdict, note renders. The first post-merge main push fills the baseline in.

Verified locally

Full run --workload 2 against the built app (short windows): all six states sample, and the workload signal is exactly what was missing — focused RSS 160 → 230 MB, wakeups 801 → 1404/s, CPU 8.0 → 12.3% with 8 busy panes on screen. Results JSON records the workload config (tabs, panes_per_tab, command) for future comparability checks. Time cost in CI: roughly doubles the sampling phase (~2 extra minutes), well inside the workflow's 30-minute timeout.

🤖 Generated with Claude Code

thdxg added 3 commits July 5, 2026 00:44
External apps and AI agents need to drive Macterm programmatically
(#107), and the CI benchmark needs to spawn realistic workloads
headlessly. Darwin notifications (BenchmarkControl) carry no payload or
response, so this adds a Unix-socket control plane: newline-delimited
JSON requests dispatched on the main actor into the same AppState paths
the UI uses, and a bundled ArgumentParser CLI at
Contents/Resources/bin/macterm (PATH-injected into every pane along
with MACTERM_SOCKET).

This first slice ships the protocol, server, and read-only verbs —
status, project/tab/pane list, session list/info. Mutations (create,
select, split, run) and the benchmark workload follow in separate PRs.

Design notes drawn from cmux/Zentty/Supacode source: socket-ownership
check before connect, env var as discovery hint (never a hard pin, so a
stale export can't brick shells after an app restart), non-blocking
connect with a short poll, error responses carrying recovery hints, and
a safe-fail CLI contract (stdout only on success; exit 2 when the app
is unreachable).
With the control plane in place, this teaches it to actually drive the
app: project create/select (idempotent by canonical path, ProjectPath-
validated, remote specs rejected until #104), tab new/select/close,
pane split/focus/close/run, an equal-cells grid verb (the benchmark
workhorse), session kill, and layout apply/save.

Semantics worth reviewing:
- New panes/tabs take a command via the layout run: path (spawn-time
  initial_input); pane run types into an EXISTING live shell through a
  new GhosttyTerminalNSView.sendText — the paste path, guarded so a
  missing surface is a typed no_surface error, never a silent drop.
- Headless callers can't answer dialogs, so close/apply verbs return a
  typed busy error where the UI would stage a confirmation; --force
  takes the destructive path deliberately.
- Every pane's env now carries MACTERM_SESSION (its restart-stable zmx
  session name), so macterm run inside a pane self-targets. pane close
  deliberately ignores that fallback: destroying "whatever pane I'm
  in" by omission is a footgun.

docs/cli.md documents the grammar, targeting rules, wire protocol for
non-CLI clients, and the same-user security posture.
The window-state benchmark measured an empty app — one idle shell — so
regressions that only show under load (render churn, poll pressure,
per-surface overhead) were invisible. Now each state is sampled twice:
idle as before, then again as workload-* states after spawning
BENCH_WORKLOAD (default 2) busy tabs through the bundled CLI
(tab new --run + grid 2x2 --run, a /bin/sh date loop per pane — a real
external child emitting a line a second, portable across POSIX shells
and nushell).

Baseline migration is seamless by construction: the idle state keys are
unchanged, so their history keeps comparing, and a baseline that lacks
the workload-* states renders them with absolute values, no deltas, no
labels, plus an explanatory footnote — the first post-merge main run
fills them in.

The spawn is paced (0.5s/tab) and verified: a pane-count mismatch
aborts the run rather than silently benchmarking a partial workload
against a full one.
@github-actions github-actions Bot added area:ci CI workflows, dev tooling area:docs Documentation labels Jul 4, 2026
Base automatically changed from claude/mystifying-colden-103489-pr2 to main July 5, 2026 01:37
thdxg added 3 commits July 5, 2026 10:38
First CI run of the workload failed with 'control socket never became
ready': the throwaway HOME under the runner's $TMPDIR
(/var/folders/…/T/…) pushes <data-dir>/control.sock past the ~104-byte
sun_path limit, so the app refuses to bind and the CLI polls a socket
that never appears. /tmp keeps the whole path around 50 bytes. The
readiness-poll failure now also surfaces the CLI's stderr instead of
timing out silently.
…ce launch

The CI workload run exposed a launch-ordering race: SwiftUI can fire
the window's onAppear (which runs installResponders and attaches the
control handler) BEFORE applicationDidFinishLaunching. When the server
was created inside didFinishLaunching, that ordering skipped the attach
— the socket then answered 'starting' forever and the benchmark
workload timed out. The server object now exists from delegate init
(attach-before-start is harmless); didFinishLaunching still owns
start() and the env exports.
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Window-state benchmark

State Metric main@3e6caa76c this branch Δ
focused CPU % 1.23 1.10 -11%
Memory (RSS MB) 108.4 107.8 -1%
CPU ms/s (powermetrics) 11.9 10.8 -9%
Wakeups/s (powermetrics) 187.0 174.4 -7%
unfocused CPU % 1.07 1.07 +0%
Memory (RSS MB) 112.6 112.1 -0%
CPU ms/s (powermetrics) 10.2 10.7 +5%
Wakeups/s (powermetrics) 178.4 191.0 +7%
minimized CPU % 0.17 0.20 +20%
Memory (RSS MB) 112.8 112.3 -0%
CPU ms/s (powermetrics) 1.7 1.9 +12%
Wakeups/s (powermetrics) 73.9 71.1 -4%
workload-focused CPU % 2.73
Memory (RSS MB) 170.2
CPU ms/s (powermetrics) 27.2
Wakeups/s (powermetrics) 222.4
workload-unfocused CPU % 2.27
Memory (RSS MB) 170.5
CPU ms/s (powermetrics) 22.3
Wakeups/s (powermetrics) 210.7
workload-minimized CPU % 0.37
Memory (RSS MB) 170.5
CPU ms/s (powermetrics) 3.6
Wakeups/s (powermetrics) 115.9

30s sampling window per state; CPU % is the process CPU-time delta over the window. Runs land on different shared runners, so treat small deltas as noise — 🔺/🔻 marks changes ≥25% that also clear the metric's absolute noise floor (CPU % ≥0.5, Memory (RSS MB) ≥25, CPU ms/s ≥5, Wakeups/s ≥50), and those add the benchmark:regression / benchmark:improvement label.

The workload-* states (busy tabs spawned via the macterm CLI) have no baseline yet — deltas for them appear once main's baseline includes a workload run.

@thdxg thdxg merged commit 8f027f3 into main Jul 5, 2026
7 checks passed
@thdxg thdxg deleted the claude/mystifying-colden-103489-pr3 branch July 5, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci CI workflows, dev tooling area:docs Documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant