Skip to content

Poll foreground processes adaptively instead of at a fixed 250ms#126

Merged
thdxg merged 3 commits into
mainfrom
poll-cadence
Jul 4, 2026
Merged

Poll foreground processes adaptively instead of at a fixed 250ms#126
thdxg merged 3 commits into
mainfrom
poll-cadence

Conversation

@thdxg

@thdxg thdxg commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What

Replaces the unconditional 250ms foreground-process poll with an adaptive cadence: 250ms only while something is moving, 1s when the app is active but idle, 2s when inactive with a window visible, and no timer at all when nothing is on screen.

Why

After #115 (occlusion parking) and #123 (occlusion settle guard), the fixed 250ms poll was the app's last structural idle cost — ~0.9% of a core, forever, scaling with pane count. Polling fast is only useful while something is moving, and every interesting moment already announces itself (tab switch, keystroke, OSC title, execution-state transition), so those drive an instant resume. This is the third and final piece of the #110 CPU story; a companion PR (#125) removes the 120Hz libghostty tick timer.

How

  • PollCadence (Macterm/App/PollCadence.swift): pure, clock-injected value type — Mode { fast, idle, background, paused }, a Context snapshot, noteEvent/notePolled/mode/nextDelay. Event storms coalesce to the fast interval; notePolled before the tick's work breaks the poll→execution-transition→event recursion.
  • AppState drives a single non-repeating rescheduled Timer (.common mode, 10% tolerance) off the cadence, with injected isAppActive/isAnyWindowVisible closures (the quick terminal's non-activating panel counts; the incubator's ordered-out window doesn't).
  • New .terminalPollEvent funnel posted from tab-select (Workspace), OSC title, user interaction, and executionState transitions (value-change only — no per-frame render spam). Plus app-activate/resign, occlusion-change, and wake observers.
  • Quit dialog re-reads process names one-shot, since the poll may be paused when quitting from a minimized window.
  • CLAUDE.md's Tab Naming section updated.

Verified

  • mise run format, mise run lint, mise run test all pass
  • 13 PollCadence unit tests: mode/interval table across contexts, burst decay, coalescing, resume-after-pause, quick-terminal-typing-while-inactive, storm cap
  • Ran the debug app: active-idle drops to the 1s cadence (~4× fewer timer wakeups), timer fully parks when the window is hidden; title updates on tab switch / command launch stay prompt

Stacked on #123 (now merged); main merged in.

🤖 Generated with Claude Code

thdxg added 3 commits July 4, 2026 15:20
…ompletion

Since #115 parks an occluded surface's renderer, hidden panes emit no
render/scrollbar heartbeats. The status indicator's quiet-settle read that
silence as "command finished" and flipped a still-running TUI in a hidden
tab to a done checkmark — which sticks, because activity events can never
revive a done pane.

Settle now runs only while the pane's window is visible. On the
occluded-to-visible edge the quiet window restarts, so a still-running
program gets its heartbeats back before the 3s settle can fire. The
occlusion probe is an injected closure (no window = occluded, covering
incubator-hosted panes) so the behavior is unit-tested.
The unconditional 250ms poll was the app's last structural idle cost
(~0.9% of a core, scaling with pane count — the residue of #110 after
occlusion parking landed in #115). Polling fast is only useful while
something is moving, and every interesting moment already announces
itself: tab switches, keystrokes, OSC titles, and execution-state
transitions.

PollCadence (pure, clock-injected, fully unit-tested) picks the
interval: 250ms during a 5s burst after any such event or while a
command runs frontmost, 1s when the app is active but idle, 2s when
inactive with a window visible (sidebar titles must not freeze), and no
timer at all when nothing is on screen — events resume instantly, and
polls fired by event storms coalesce to the fast interval.

Title-pipeline guarantees survive: liveness is now event-bounded rather
than interval-bounded, programTitle expiry rides the same refresh, and
the quit dialog re-reads names one-shot because the poll may be paused
when quitting from a minimized window.
# Conflicts:
#	Macterm/App/AppState.swift
#	MactermTests/App/AppStateTests.swift
@github-actions github-actions Bot added area:state AppState, models, persistence area:tests Test changes area:docs Documentation labels Jul 4, 2026
@thdxg thdxg merged commit ff2ba62 into main Jul 4, 2026
5 checks passed
@thdxg thdxg deleted the poll-cadence branch July 4, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentation area:state AppState, models, persistence area:tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant