Skip to content

Benchmark window-state resource usage in CI and compare PRs against main#129

Merged
thdxg merged 5 commits into
mainfrom
claude/busy-poitras-f4f8e0
Jul 4, 2026
Merged

Benchmark window-state resource usage in CI and compare PRs against main#129
thdxg merged 5 commits into
mainfrom
claude/busy-poitras-f4f8e0

Conversation

@thdxg

@thdxg thdxg commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What

A Benchmark workflow that measures Macterm's resource usage in three window states — focused, open but unfocused, and minimized — and reports how a PR changes them relative to main:

  • Pushes to main run the benchmark and upload a benchmark-results artifact (the baseline).
  • PR runs benchmark the head, download the latest main baseline, and post a comparison table (CPU %, RSS, and best-effort powermetrics CPU ms/s + wakeups/s per state) to the job summary and a single updated-in-place PR comment.

Locally: mise run bench (BENCH_SECONDS to adjust the 30s default window).

How

CI can't drive window states via AppleScript or synthetic keystrokes — both need TCC grants a headless runner can't approve. Instead:

  • BenchmarkControl (app, armed only by MACTERM_BENCHMARK=1): listens for Darwin notifications (notifyutil -p com.thdxg.macterm.bench.<cmd>, no permissions needed) — open-project (a fresh runner otherwise sits on the WelcomeView with no shell/surface), activate, minimize, restore. Holds an NSProcessInfo activity assertion so App Nap can't suspend the app and swallow control commands — which also means the numbers measure the app's own timer/render behavior rather than OS nap throttling. Skips the notification-permission prompt (it would steal key focus mid-measurement).
  • scripts/benchmark.py: launches via open --env (launch-time activation counts as user intent; SwiftUI won't create the window until activation is granted), waits for the project-open readiness marker, then per state: settle, sample CPU-time delta + median RSS over the window, with a concurrent powermetrics --samplers tasks pass (null inside VMs where it fails). Focus changes use open (self) and open -a Finder (steal focus); minimize goes through the notification hook.
  • Isolation: runs under a throwaway $HOME, and — because App Support resolves via the user record, not $HOMEFileStorage gains a MACTERM_BENCHMARK_DATA_DIR override so benchmark runs never read or write real app data.

Caveats

The first main-branch run after merge establishes the baseline; PRs before that report absolute values only.

Adds a resource benchmark that measures CPU-time delta, RSS, and
(best-effort via powermetrics) wakeups across three window states:
focused, open-but-unfocused, and minimized. Pushes to main publish a
baseline artifact; PR runs fetch the latest main baseline and post a
comparison table to the job summary and an updated-in-place PR comment.

CI can't script window states through AppleScript or synthetic keys
(both need TCC grants a headless runner can't approve), so the app
gains a benchmark-only remote control: launched with MACTERM_BENCHMARK=1
it listens for Darwin notifications (notifyutil -p), which need no
permissions. Benchmark mode holds an NSProcessInfo activity assertion —
otherwise App Nap suspends the occluded app and queues the control
notifications undelivered — and skips the notification-permission
prompt that would steal key focus mid-measurement.

The harness launches via open --env rather than exec'ing the binary:
launch-time activation counts as user intent, and SwiftUI doesn't
create the window (or run the onAppear that wires app state) until
activation is granted. App Support resolves via the user record, not
$HOME, so benchmark runs point FileStorage at a throwaway directory
via MACTERM_BENCHMARK_DATA_DIR to keep real app data untouched.
@github-actions github-actions Bot added area:state AppState, models, persistence area:ci CI workflows, dev tooling area:docs Documentation labels Jul 4, 2026
The bench-built Release app carries the placeholder EdDSA key, so
Sparkle's updater fails to start and raises an app-modal alert that
blocks the launch run loop until someone clicks OK — locally that
interrupts the run, and on CI nobody is there to click. Don't start
the updater in benchmark mode.

Also nudge activation once a second until the window exists (a single
post-launch request can be denied or race session readiness on a fresh
runner), and dump the app's log plus a screenshot as an artifact when
the readiness marker never appears.
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Window-state benchmark

State Metric Value
focused CPU % 1.07
Memory (RSS MB) 108.7
CPU ms/s (powermetrics) 10.1
Wakeups/s (powermetrics) 127.0
unfocused CPU % 1.17
Memory (RSS MB) 112.9
CPU ms/s (powermetrics) 11.3
Wakeups/s (powermetrics) 132.7
minimized CPU % 0.07
Memory (RSS MB) 113.0
CPU ms/s (powermetrics) 0.7
Wakeups/s (powermetrics) 19.5

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, and those add the benchmark:regression / benchmark:improvement label.

No main-branch baseline found; showing absolute values only.

thdxg added 3 commits July 4, 2026 18:43
A delta is significant only when it clears both ±25% and the metric's
absolute noise floor — a pure percentage threshold would flag noise on
tiny absolute values (minimized CPU going 0.03% → 0.05% reads as +67%).
Significant deltas add a benchmark-regression or benchmark-improvement
label (kept in sync on later pushes) and the PR comment gains a section
explaining exactly which metrics triggered the label.
@thdxg thdxg enabled auto-merge (squash) July 4, 2026 09:53
@thdxg thdxg merged commit 1dffbce into main Jul 4, 2026
7 checks passed
@thdxg thdxg deleted the claude/busy-poitras-f4f8e0 branch July 4, 2026 09:54
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 area:state AppState, models, persistence

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant