Cut native pane action latency from seconds to one frame of feedback - #1223
Merged
Conversation
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
force-pushed
the
perf/dev3-native-pane-action-latency
branch
from
August 2, 2026 10:55
21b2aab to
1f137f8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 ownuseState;TaskTerminaldraws 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.pson the click path, synchronously.classifyOwnershipproves a recorded PID was not reused by readingps -p PID -o lstart=, twice per pane, viaspawnSync— 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 itsnavigatecallback, 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.tsis 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.TaskPaneControlsholds 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.readProcessStartSignatureis async,classifyOwnershipstarts host and shell probes together, andrecover()/listPanes()fan out over the pane set.nativePaneActionreads the layout only: every action decides from the tree, so the full state's per-pane ownership sweep was a secondpspass nothing consumed. Recovery still runs, so dead-pane reconciliation is unchanged.Numbers
Backend,
bun run measure:native-pane-latency, macOS, real hosts, p50:psprobeIn 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-busyset on the first frame; three rapid split clicks produced one pane. Narrow-viewport pane-state reads dropped from ~28/s to ~2/s.readStatep50 at 6 panes is 78 ms, still above the 50 ms target: a read classifies the pane set twice (describeSessionrecovers and reconciles, thenlistPanessnapshots). Collapsing those touches dead-pane reconciliation, so it is left for its own change. Rationale and alternatives indecisions/192-native-pane-action-latency.md.