Skip to content

Cut native pane action latency from seconds to one frame of feedback - #1223

Merged
h0x91b merged 2 commits into
mainfrom
perf/dev3-native-pane-action-latency
Aug 2, 2026
Merged

Cut native pane action latency from seconds to one frame of feedback#1223
h0x91b merged 2 commits into
mainfrom
perf/dev3-native-pane-action-latency

Conversation

@h0x91b

@h0x91b h0x91b commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Splitting a pane or picking a layout on a native-backend task looked inert for roughly 2–3 seconds. Measurement (bun run measure:native-pane-latency, real hosts) put the backend at 20 ms for a 2-pane layout change and 130–230 ms for a split, so the delay was not the work — it was the delivery.

What was wrong

Two unconnected copies of TaskPaneState. TaskPaneControls (inspector) owned the buttons and wrote the action's authoritative response into its own useState; TaskTerminal draws the geometry and learned about the change only from its own 2500 ms poll. Click-to-settled was therefore real work plus 0–2500 ms of waiting, with no click-to-feedback at all — no busy state, no duplicate suppression.

ps on the click path, synchronously. classifyOwnership proves a recorded PID was not reused by reading ps -p PID -o lstart=, twice per pane, via spawnSync — so a pane set could not overlap its probes and the Bun event loop blocked for the duration. Profiling a 6-pane read: 121 ms, of which ~64 ms was one classification pass and ~64 ms an immediately repeated second one. The coordinator file lock measured 0.13 ms, i.e. not a factor.

A ~30 Hz read loop in the narrow carousel. MobilePaneCarousel's poll effect depends on its navigate callback, whose identity came from the pane ids in state — every read produced a fresh array, so the 3 s poll re-armed immediately. Browser-observed: ~28 pane-state reads per second, each one an ownership sweep.

What changed

  • src/mainview/pane-state-bus.ts is the single arrival point for a task's pane state. Reads and actions go through it and the server's own response is broadcast to every subscriber; polling stays purely as reconciliation. Per-request tickets drop a response older than one already delivered, so a slow poll cannot reinstate pre-action geometry. Nothing here computes a tree locally.
  • TaskPaneControls holds every mutating control (disabled + aria-busy) for one action's duration, guarded by a ref so two clicks in a frame cannot both fire. Capability stays separate from busy, so the "needs two panes" tooltip never fires for a control that is merely in flight.
  • readProcessStartSignature is async, classifyOwnership starts host and shell probes together, and recover() / listPanes() fan out over the pane set.
  • nativePaneAction reads the layout only: every action decides from the tree, so the full state's per-pane ownership sweep was a second ps pass nothing consumed. Recovery still runs, so dead-pane reconciliation is unchanged.
  • Auxiliary pane labels from Open real auxiliary panes on the native terminal backend #1222 now come from the state the caller already read, and ride along on the state an action returns — a split no longer blanks "Dev Server" back to "Pane 2" until the next poll.

Numbers

Backend, bun run measure:native-pane-latency, macOS, real hosts, p50:

before after
layout preset, 6 panes 60 ms 39 ms
layout cycle, 6 panes 66 ms 37 ms
split, 6 panes 228 ms 170 ms
read state, 6 panes 119 ms 78 ms
single ps probe 4.9 ms 0.5 ms

In the running app (headless Chromium, native task): layout cycle acknowledged in 2.1 ms and settled in 43.7 ms / 6 frames; a split acknowledged in 6.4 ms with aria-busy set on the first frame; three rapid split clicks produced one pane. Narrow-viewport pane-state reads dropped from ~28/s to ~2/s.

readState p50 at 6 panes is 78 ms, still above the 50 ms target: a read classifies the pane set twice (describeSession recovers and reconciles, then listPanes snapshots). Collapsing those touches dead-pane reconciliation, so it is left for its own change. Rationale and alternatives in decisions/192-native-pane-action-latency.md.

h0x91b added 2 commits August 2, 2026 13:51
The inspector toolbar and the terminal canvas held separate copies of
TaskPaneState, so a split or layout click updated the toolbar and the
canvas only caught up on its 2500 ms poll. Route every pane read and
action through a single bus that broadcasts the server's own response,
with per-request tickets so a slow poll cannot reinstate pre-action
geometry, and hold the mutating controls for the duration of one action.

Also cut the ownership probing on that path: readProcessStartSignature
is async so a pane set classifies in one round trip instead of N blocking
forks, and the action dispatcher reads the layout instead of the full
state, dropping a per-pane ps pass nothing consumed.
MobilePaneCarousel's poll effect depends on the navigate callback, whose
identity was derived from the pane ids in state. Every read produced a
fresh array, so the effect re-ran immediately and the 3s poll became a
~30 Hz read loop — visible in the browser as ~28 pane-state reads per
second, each one an ownership sweep. Resolve the ids through a ref.
@h0x91b
h0x91b force-pushed the perf/dev3-native-pane-action-latency branch from 21b2aab to 1f137f8 Compare August 2, 2026 10:55
@h0x91b
h0x91b merged commit 1eda67e into main Aug 2, 2026
15 checks passed
@h0x91b
h0x91b deleted the perf/dev3-native-pane-action-latency branch August 2, 2026 11:01
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