Skip to content

perf(renderer): bail out of identity-equal terminal layout and cache-timer writes - #12420

Merged
brennanb2025 merged 4 commits into
mainfrom
brennanb2025/perf-store-write-bailouts
Aug 4, 2026
Merged

perf(renderer): bail out of identity-equal terminal layout and cache-timer writes#12420
brennanb2025 merged 4 commits into
mainfrom
brennanb2025/perf-store-write-bailouts

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Two terminal-store writes published a new AppState on every call even when nothing changed, and one remote IPC fired on every layout persist regardless of whether the host-visible layout had moved. Because the renderer's per-pane selectors are O(mounted panes), each redundant publish ran every subscriber's selector across the whole app.

  • setCacheTimerStartedAt spread { ...s.cacheTimerByKey, [key]: ts } and returned unconditionally, while every sibling action in the slice guards. The cadence is real: parked-terminal-byte-watcher writes null on every onAgentBecameWorking, every onAgentExited, and every stale-working-title clear — i.e. repeated null-over-null writes per agent transition, per parked pane. Now bails when the stored value is already identical (strict equality, so it covers both number and null). The :seed sentinel clear is preserved: a real pane write with an unchanged value still proceeds when a stale ${tabId}:seed entry exists, or the bailout would strand a phantom timer.
  • setTabLayout spread { ...s.terminalLayoutsByTabId } and returned unconditionally even when the normalized snapshot was structurally identical to the stored one. Its hot caller is TerminalPane.persistLayoutSnapshot, invoked from ~10 sites including an effect whose deps include paneTitles — so pane-title churn drove layout persistence. Now bails when terminalLayoutEqual(existing, normalized.snapshot). Normalization still runs first, and pty-ownership transfers are resolved before the bailout so that side effect is unchanged for any layout that normalization actually rewrites.
  • Remote layout push. After setTabLayout, if any pty was remote, persistLayoutSnapshot fired updateWebRuntimePaneLayout with no comparison against what was last pushed — one remote round trip per title change per tab. Extracted into createRemotePaneLayoutPusher, which dedupes in-flight and successful pushes by worktree, tab, and full snapshot; failed pushes re-arm the layout and remounts start with a fresh pusher.

The comparator that gates the layout write already existed as terminalLayoutEqual in web-session-tabs-sync.ts, module-private. It is now src/renderer/src/lib/terminal-layout-equality.ts and imported back into web-session-tabs-sync.ts unchanged — the moved implementation is character-identical, so the mirroring path's behavior is untouched.

No user-visible behavior changes: the only things eliminated are writes and IPCs that carried no new information.

Screenshots

No visual change.

Testing

  • pnpm lintoxlint, audit:code-quality:native, audit:code-quality:type-aware, check:reliability-gates, check:max-lines-ratchet all clean
  • pnpm typecheckconfig/tsconfig.tc.web.json clean
  • pnpm testsrc/renderer + src/shared: 23,181 passed, 19 skipped. The only 2 failures are in src/shared/setup-agent-sequencing.test.ts, which fails identically on the parent commit (9a97e737f5) in this environment — a pre-existing local red (Node v26 vs the repo's pinned Node 24), unrelated to this diff.
  • pnpm build — not run; typecheck covers the compile surface and the diff is renderer-only TypeScript with no build-config or native-module changes.
  • Added or updated high-quality tests

Measurements

Two new test files count the actual effect, and both are red on the parent commit:

Store-level subscriber wakeups (terminal-write-identity-bailouts.test.ts) — a subscriber counter attached to a real zustand store (createTestStore), 1,000 repeat writes each:

Scenario Before After
1,000 null-over-null setCacheTimerStartedAt 1,000 wakeups 0
1,000 repeats of the same timestamp 1,000 wakeups 0
1,000 structurally identical setTabLayout snapshots 1,000 wakeups 0

Verified red on parent by reverting terminals.ts to HEAD: expected 1000 to be +0 on all three, 6 of 10 tests failing.

Remote-push count (remote-pane-layout-push.test.ts) — IPC boundary mocked, 100 persist calls with an unchanged layout:

Before After
updateWebRuntimePaneLayout invocations 100 1

Verified red by deleting the dedupe guard: expected "vi.fn()" to be called 1 times, but got 100 times.

Comparator micro-bench — realistic 8-leaf split snapshot (balanced split tree, UUID leaf ids, all four leaf-keyed records populated), 1M iterations × 5 runs after a 200k warm-up, Node v26.5.0 / Apple M5 Pro:

Case µs/call
Fully equal (worst case — walks the whole tree and all 4 records) 0.582 (runs: 0.581 / 0.582 / 0.582 / 0.585 / 0.610)
First-field difference (early exit) 0.071

~0.58 µs against the 5 µs bar, i.e. the check costs roughly 1/8600th of a 5 ms frame budget, versus the O(panes) selector sweep it avoids. The bench was a throwaway; no timing assertion is committed, since wall-clock thresholds are flaky in CI.

Behavior coverage beyond the counters: publishes still happen for every tracked field (root shape, direction, ratio, activeLeafId, expandedLeafId, ptyIdsByLeafId, buffersByLeafId, scrollbackRefsByLeafId, titlesByLeafId); the first write for a key is recorded even when the value is null; a stale :seed sentinel is still cleared on an otherwise-unchanged pane write; the clearing call (setTabLayout(tabId, null)) still deletes and then bails when already absent; and a bailed-out identical layout is asserted to produce no pane-ownership transfers rather than that being assumed. The pusher tests also pin that the payload is byte-for-byte what the un-deduped path sent, that titlesByLeafId is still omitted when absent, and that the cache is not carried across tabs.

AI Review Report

Self-review of the diff, focused on the ways an identity bailout can silently change behavior:

  • Dropped side effects. The real hazard in setTabLayout is that normalizeTerminalLayoutPtyOwnership can rewrite a layout carrying a duplicate pty id, and resolveTerminalLayoutPtyOwnershipTransfers then moves pane-keyed store state. A naive bailout placed before that resolution would skip the transfer whenever the normalized result happened to match what was already stored. Transfers are therefore resolved before the equality check, so transferNormalizedTerminalLayoutPtyOwnership sees exactly what it saw before; only the state publish is skipped. Covered by a test that feeds a duplicate-pty layout, then replays the normalized snapshot 1,000 times and asserts both the stored reference and the pane-keyed maps are untouched.
  • return s vs return {}. zustand 5 short-circuits on Object.is(nextState, state), so returning the state object itself is what actually skips the listener sweep — returning {} would still Object.assign a fresh state and wake everyone. Both bailouts return s, matching the existing idiom in this slice (markTerminalTabUnread, markTerminalPaneUnread, markAgentCompletionPaneUnread).
  • The :seed carve-out. Bailing purely on value equality would have stranded the ${tabId}:seed sentinel that seedCacheTimersForIdleTabs plants, leaving a phantom countdown. The guard only fires when there is no stale sentinel to clear.
  • First-write-of-null. undefined === null is false, so a key's first null write still lands and is still recorded; only the second identical write bails. Pinned by a test.
  • Comparator extraction fidelity. Moved verbatim; web-session-tabs-sync.ts imports it and its mirroring call site is unchanged. Its whole test surface passes (285 files / 3,558 tests across terminal-pane + runtime).
  • Stale-cache risk on the remote push — the one genuinely new piece of state. The cache is keyed by worktree and tabId; failed pushes re-arm only the current attempt, and the comparator includes ptyIdsByLeafId, so a remote reconnect that mints new pty ids re-pushes. The residual case is a host that loses its layout while re-adopting byte-identical pty ids; see Notes.
  • Cross-platform. Reviewed for macOS / Linux / Windows: the diff adds no keyboard shortcuts, no shortcut labels, no path handling, no shell invocation, and no Electron main-process or platform-branching code. It is pure renderer state comparison plus one existing IPC call being made conditional. The SSH and folder-workspace paths are unaffected — isRemoteRuntimePtyId still gates the push exactly as before, and nothing here assumes a git worktree. No Git commands are added or changed, so the Git-version baseline is not in play. react-doctor reports one warning in TerminalPane.tsx at line 1649 (no-adjust-state-on-prop-change); it is present identically on the parent commit and is outside every line and effect this PR touches.

The same-model until-clean review returned clean after scoped fix and coverage passes, and $electron evidence is attached in the PR conversation.

Security Audit

No new attack surface. The diff adds no input parsing, no command execution, no filesystem or path handling, no auth or secret handling, and no dependencies. The one IPC touched (updateWebRuntimePaneLayout) is unchanged in payload, sender, and validation — it is simply invoked less often, and the dedupe decision is made entirely from renderer-local state that the renderer already owned. The extracted comparator uses Object.prototype.hasOwnProperty.call for record comparison, preserving the original's prototype-pollution-safe key check. No follow-up needed.

Notes

  • Remote-push cache staleness. createRemotePaneLayoutPusher skips a push when the tab's full layout snapshot matches the last one pushed. It is keyed by tab and worktree, failed pushes re-arm the current attempt, and the comparator includes ptyIdsByLeafId — so a remote runtime that reconnects with fresh pty ids re-pushes. The one path that could theoretically skip a needed push is a host that loses its stored pane geometry while the client re-adopts byte-identical pty ids for the same leaves; in that case the host would also be replaying a stale layout back through web-session-tabs-sync, which changes the client snapshot and re-arms the push. Flagging it as the one behavioral edge worth watching.
  • Comparison is stricter than the payload. The push payload is only root / expandedLeafId / titlesByLeafId, but the dedupe compares the whole snapshot. That is deliberate and conservative: equality on the full snapshot implies equality on the payload, so the dedupe can never skip a push whose payload actually differs.
  • Follow-up (not in scope). The underlying amplifier is that the app's per-pane selectors are O(mounted panes) per store publish, so any redundant write is expensive. This PR removes two of the loudest writers; narrowing those selectors is the structural fix and belongs in its own change.

…timer writes

setCacheTimerStartedAt and setTabLayout spread a fresh object and returned it
unconditionally, so every redundant call published a new AppState and ran every
zustand subscriber's selector across all mounted panes. Both have a real
redundant cadence: parked-terminal-byte-watcher writes a null cache timer on
each agent working/exit/stale-title transition, and TerminalPane re-persists an
identical layout on pane-title churn.

Extract the existing terminalLayoutEqual comparator out of web-session-tabs-sync
into a shared module and use it to gate the layout write, and dedupe the
remote-runtime layout IPC against the last snapshot pushed per tab.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change centralizes terminal layout equality in a shared utility. Store updates avoid redundant writes, remove stale timer sentinels, normalize PTY ownership, and handle layout deletion. Remote pane layout updates use a resettable pusher that deduplicates layouts per worktree and tab, sends changed geometry and titles, retries failed updates, and re-pushes after remounts. Tests cover equality, identity bailouts, cache behavior, PTY swaps, isolation, failures, remounts, and reconnects.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary renderer optimization by skipping identity-equal terminal layout and cache-timer writes.
Description check ✅ Passed The description covers the required sections, explains the changes and risks, documents testing results, and states why the build was not run.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ccc1cb0-255d-4561-8fbe-4aa2a36f5a59

📥 Commits

Reviewing files that changed from the base of the PR and between a6b14eb and 5d4e6b9.

📒 Files selected for processing (7)
  • src/renderer/src/components/terminal-pane/TerminalPane.tsx
  • src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts
  • src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts
  • src/renderer/src/lib/terminal-layout-equality.ts
  • src/renderer/src/runtime/web-session-tabs-sync.ts
  • src/renderer/src/store/slices/terminal-write-identity-bailouts.test.ts
  • src/renderer/src/store/slices/terminals.ts

Comment on lines +19 to +20
if (lastPushed?.tabId === tabId && terminalLayoutEqual(lastPushed.snapshot, layout)) {
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Compare the remote delivery state instead of the full persisted snapshot.

Line 19 compares activeLeafId, buffersByLeafId, and scrollbackRefsByLeafId. The runtime RPC does not receive these fields. A change only in one of these fields sends an identical remote layout update.

Keep PTY mappings in the comparison because a PTY swap changes the host session. Compare only PTY mappings plus root, expandedLeafId, and titlesByLeafId. Add tests for active-leaf and scrollback-only changes.

Comment thread src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts Outdated
Comment on lines +32 to +37
return (
b.type === 'split' &&
a.direction === b.direction &&
a.ratio === b.ratio &&
terminalLayoutNodeEqual(a.first, b.first) &&
terminalLayoutNodeEqual(a.second, b.second)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Normalize the default split ratio before comparison.

Line 35 treats undefined and 0.5 as different values. The shared type defines an absent ratio as 0.5. A host snapshot that omits the default ratio will bypass store and IPC identity bailouts when the renderer writes 0.5.

Proposed fix
-    a.ratio === b.ratio &&
+    (a.ratio ?? 0.5) === (b.ratio ?? 0.5) &&

Add coverage for an omitted ratio versus 0.5.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
b.type === 'split' &&
a.direction === b.direction &&
a.ratio === b.ratio &&
terminalLayoutNodeEqual(a.first, b.first) &&
terminalLayoutNodeEqual(a.second, b.second)
return (
b.type === 'split' &&
a.direction === b.direction &&
(a.ratio ?? 0.5) === (b.ratio ?? 0.5) &&
terminalLayoutNodeEqual(a.first, b.first) &&
terminalLayoutNodeEqual(a.second, b.second)

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Until-clean review and Electron QA

The same-model review returned CLEAN after two scoped follow-ups. It found and fixed one correctness regression in the remote layout dedupe: failed/inactive pushes were cached as successful, and the cache identity omitted the worktree. The pushed fix preserves in-flight/success dedupe, re-arms failed pushes, isolates worktree/tab identity, and covers stale async failure ordering.

Live Electron validation on the PR worktree:

  • 1,000 identical cache-timer writes: 0 subscriber wakeups
  • 1,000 identical two-pane layout writes: 0 subscriber wakeups
  • Stored split-layout reference remained stable
  • Created a real two-pane split and executed commands in both panes successfully

Single-pane live bailout measurement

Two-pane split remains interactive

Verification on pushed b91c1665be: full pnpm lint, full pnpm typecheck, and 109 focused layout/store/runtime tests passed.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Added paired Electron reconnect coverage in 10664b2.

The test now proves:

  • an offline pane-layout write is absent from the headless host
  • reconnect preserves the same terminal component
  • the second UI submit produces an identical full layout snapshot
  • the host persists that retry
  • a fresh isolated Electron client renders the saved pane title

Validation:

  • red/green check: old cache behavior fails this scenario; the PR fix passes
  • focused Electron E2E: 2/2 consecutive runs passed
  • focused renderer unit tests: 10/10 passed
  • typecheck, native + type-aware lint, formatting, max-lines, and reliability gates passed
  • same-model reviewer returned CLEAN on the final pass

Existing Electron screenshot evidence remains attached here: #12420 (comment)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/e2e/paired-remote-pane-layout-retry.spec.ts (2)

202-209: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the cleanup sequence fault tolerant.

observer?.dispose() and client?.dispose() are awaited without error handling. If either rejects, the remaining cleanup steps do not run. The test then leaks an Electron app, a daemon, and a temp user-data directory, which can destabilize later tests in the same run.

♻️ Proposed change
   } finally {
-    await observer?.dispose()
+    await observer?.dispose().catch(() => undefined)
     if (terminal) {
       await host.client.call('terminal.closeTab', { terminal }).catch(() => undefined)
     }
-    await client?.dispose()
+    await client?.dispose().catch(() => undefined)
     await host.dispose()
   }

185-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The local assertion after the retry is weak; consider asserting the push explicitly.

setPaneTitle sets the same title again. The Edit pane title: ${title} button from Line 105 is already visible from the first, failed attempt, so that step cannot fail. Line 186 then only proves the local layout is unchanged. The host poll on Lines 187-195 carries the whole proof of the retry. Add a console-event wait for a successful push, or assert a remote IPC call count, so a regression that stops re-arming the pusher fails on the client side too.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ee8517e5-72f7-4414-8856-3052057383d7

📥 Commits

Reviewing files that changed from the base of the PR and between b91c166 and 10664b2.

📒 Files selected for processing (1)
  • tests/e2e/paired-remote-pane-layout-retry.spec.ts

@brennanb2025
brennanb2025 merged commit 08abb75 into main Aug 4, 2026
45 checks passed
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