Skip to content

perf(terminal): coalesce WebGL redraws and cache character joining#8178

Merged
nwparker merged 1 commit into
mainfrom
nwparker/user-s-CPU-usage
Jul 11, 2026
Merged

perf(terminal): coalesce WebGL redraws and cache character joining#8178
nwparker merged 1 commit into
mainfrom
nwparker/user-s-CPU-usage

Conversation

@nwparker

@nwparker nwparker commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Merge forced WebGL repaint repair into xterm's already-queued frame while preserving the synchronous DOM/Windows ConPTY repair path.
  • Register RTL shaping only immediately before the first live or replayed RTL bytes.
  • Cache exact ligature-join results with font invalidation and bounded LRU eviction.

Related issue: #6655. The issue remains open for reporter hardware confirmation.

Screenshots

No visual change.

Testing

  • pnpm lint — focused oxlint and oxfmt passed for all 17 changed files; the PR verify job passed before the comment-only follow-up.
  • pnpm typecheckpnpm run typecheck:web passed.
  • pnpm test — 521 focused Vitest tests and the 41-test registered xterm containment slice passed.
  • pnpm build — the focused Electron run rebuilt the e2e app successfully.
  • Added or updated high-quality tests that would catch regressions.

Additional runtime validation:

pnpm exec playwright test tests/e2e/terminal-foreground-redraw-freeze.spec.ts --config tests/playwright.config.ts --project electron-headless -g 'Codex-style line rewrites|active OpenTUI-style redraw bursts'  # 2 passed
pnpm check:max-lines-ratchet  # passed, no new bypasses

After the final comment-only change, the directly affected PTY/scheduler slice passed 402/402 tests plus focused lint/format.

Trace-backed cause

The reporter's 14.7s macOS trace was not a WebGL-loss/DOM-fallback case. It showed 548 of 549 xterm write timers immediately followed by an animation-frame render:

  • Orca synchronously forced a full-grid refresh after xterm parsed each foreground rewrite.
  • xterm then ran the render frame it had already queued for the same write.
  • Character joining was inclusive for 6.02s; the programming-ligature fallback matcher alone took 2.86s.
  • Orca's always-registered RTL joiner also made xterm scan visible cells even for ordinary ASCII output.

Measured effect

On the same live macOS Electron pane with real WebGL, replaying 180 rewrite frames at about 35 FPS:

Policy Sync repairs Actual renders renderRows time
Previous policy (simulated) 180 360 203.5ms
This branch 0 180 95.8ms

That halves actual renders and reduced measured render work by about 53% in this replay. Ordinary output kept the character-joiner count at zero; the first Arabic output registered shaping before rendering (0 -> 1 joiner).

Reliability proof

  • Reliability invariant / gates: terminal-performance.input-throughput is primary, while preserving terminal-performance.output-backpressure-budget, terminal-render.windows-cjk-repaint, and xterm-addon.boundary-containment. Foreground repair must not synchronously duplicate an already-queued WebGL frame; DOM/Windows repair remains synchronous; ordinary non-RTL panes must not pay for the RTL joiner; first RTL bytes must still be prepared before xterm parses them.
  • Material impact: Removes the duplicate-render class captured at 100-200% renderer CPU and bounds/caches repeated ligature matching without dropping stale-cell, replay, or RTL safeguards.
  • Product change type: Performance hardening plus runtime reliability hardening and regression coverage.
  • Performance risk inventory: Touches foreground render, replay/restore render, PTY output, the shared output scheduler, xterm character joiners, and font-refresh invalidation. It adds no polling, provider listing, startup awaits, resize IPC, subprocesses, store scans, or hidden-pane wake loop. The pre-RTL check is one linear scan over incoming chunks until first RTL; the ligature cache is capped at 100,000 UTF-16 code units and 2,048 entries per pane.
  • No-regression evidence: Live WebGL changed 360 -> 180 renders as measured above. The Electron run requires an observed public WebGL refresh with zero synchronous WebGL repairs. Queue/backpressure bounds are unchanged. Cache eviction and live renderer transitions have deterministic count tests.
  • Correctness evidence: Unit coverage exercises DOM/WebGL selection, live renderer resolution at parse and follow-up time, replay/restore, mixed queued producers, RTL live/replay ordering, failure containment, ligature cloning/invalidation/LRU bounds, and cleanup.
  • Diagnostics: The focused Electron spec annotates per-renderer synchronous/debounced refresh counts and timer drift. The original trace and local render counters provide before/after evidence; no production telemetry was added.
  • Promotion status: No reliability gate is promoted. terminal-performance.input-throughput remains a registered gap with protection none; the Electron probe is not manifest-registered and lacks cross-platform soak history.
  • Rollback/demotion rule: Revert the renderer-sensitive policy if WebGL output becomes stale or synchronous WebGL repairs return. Disable/follow up the joiner changes if RTL shaping, ligature ranges, output delivery, or teardown regress. Keep the Electron probe advisory if unsupported environments or unexplained flakes prevent a stable soak window.

AI Review Report

The review covered renderer-mode transitions at initial parse and scheduled follow-up, replay/restore behavior, mixed-producer queue policy, xterm addon containment, lazy RTL ordering and teardown, ligature-cache mutation/invalidation/bounds, memory growth, and test oracle quality. It found and fixed early renderer-state capture, synchronous WebGL replay refreshes, overwritten per-chunk preparation, registration-failure output loss, an unbounded cache-entry count, a vacuous DOM-passing WebGL E2E, a Linux GPU-disabled harness failure, and a missing layout-manager test method. The final adversarial pass found no blockers.

Cross-platform review: no shortcuts, labels, shell commands, or paths changed. The renderer policy is provider-independent across local, daemon, SSH, WSL, and remote-runtime. macOS WebGL was tested live; Linux golden and Wayland CI passed on the prior code commit, and Windows keeps the deterministic synchronous DOM/ConPTY repair path. Live Linux WebGL and live Windows remain accepted gaps.

Security Audit

No new IPC, network, auth, secret, environment-variable, filesystem, command-execution, dependency, or persisted-data surface was added. Terminal output is only scanned in-memory for existing RTL code-point ranges; it is not logged or serialized. The new cache is pane-local, bounded by characters and entry count, invalidated on font changes/refresh, and released through addon disposal. No follow-up security work is required.

Notes

  • The reporter has not tested this branch on the M5 / 5120x2160 / 120Hz setup.
  • GPU-disabled Electron environments explicitly skip the advisory WebGL probe; deterministic unit tests remain the portable gate.
  • The probe proves refresh-policy selection, not sustained CPU by itself; the trace replay supplies the render-count/time comparison.
  • After a pane has displayed RTL, its joiner remains registered through later DOM fallback so shaping can resume on WebGL reattach. Frequently unique ligature text still misses the exact-text cache.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e20e7b0f-9bda-4a5a-b786-a2b5dba76fd6

📥 Commits

Reviewing files that changed from the base of the PR and between f8559b3 and 77ea614.

📒 Files selected for processing (16)
  • src/renderer/src/components/terminal-pane/layout-serialization.test.ts
  • src/renderer/src/components/terminal-pane/layout-serialization.ts
  • src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/components/terminal-pane/replay-guard.test.ts
  • src/renderer/src/components/terminal-pane/replay-guard.ts
  • src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts
  • src/renderer/src/lib/pane-manager/pane-lifecycle.ts
  • src/renderer/src/lib/pane-manager/pane-manager.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-foreground-render-settle.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.ts
  • src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.test.ts
  • src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.ts
  • src/renderer/src/lib/pane-manager/terminal-ligatures-addon.test.ts
  • src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • src/renderer/src/lib/pane-manager/pane-manager.ts
  • src/renderer/src/components/terminal-pane/layout-serialization.test.ts
  • src/renderer/src/components/terminal-pane/layout-serialization.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-foreground-render-settle.ts
  • src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.test.ts
  • src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts
  • src/renderer/src/lib/pane-manager/terminal-ligatures-addon.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts
  • src/renderer/src/lib/pane-manager/pane-lifecycle.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.ts

📝 Walkthrough

Walkthrough

The update adds renderer-aware refresh policies throughout terminal output, replay, scrollback restoration, and foreground settling. WebGL paths use debounced public refreshes while DOM paths can request synchronous core refreshes. Arabic shaping registration is now lazy and triggered by RTL output, including chunk-boundary handling and cleanup. A new ligatures addon adds bounded caching with invalidation on refresh and font changes. Unit and end-to-end tests cover these behaviors.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.63% 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 is concise and accurately summarizes the main performance changes to terminal redraw coalescing and character-joining cache.
Description check ✅ Passed The description includes all required sections and covers testing, review, security, notes, and cross-platform considerations.
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
📝 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.

🧹 Nitpick comments (2)
src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.ts (1)

506-527: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the cross-chunk aggregation invariant.

The resolver OR and callback fan-in are subtle queue semantics; add a brief reason for preserving all forced refresh policies and preparation hooks.

Suggested clarification
+    // Why: one drained write can combine chunks queued under different renderer
+    // states; preserve every forced refresh policy and producer preparation hook.
     if (chunk.forceForegroundRefresh) {

As per coding guidelines, “When writing or modifying code driven by a design doc or another non-obvious constraint, add a brief comment explaining why the code behaves that way.”

Source: Coding guidelines

src/renderer/src/components/terminal-pane/pty-connection.ts (1)

4365-4368: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Explain the shaping-before-write ordering.

Registering the joiner before output bookkeeping is intentional but non-obvious; document that it must happen before xterm parses the triggering bytes.

Suggested clarification
 function beforeTerminalOutputWrite(data: string): void {
+  // Why: the joiner must register before xterm parses the RTL bytes that require it.
   ensureArabicShapingJoinerForText(pane.terminal, data)
   recordTerminalOutput(pane.terminal)
 }

As per coding guidelines, “When writing or modifying code driven by a design doc or another non-obvious constraint, add a brief comment explaining why the code behaves that way.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 56d9a130-c9f6-4aae-b515-b895f6879693

📥 Commits

Reviewing files that changed from the base of the PR and between c633a63 and c390023.

📒 Files selected for processing (17)
  • src/renderer/src/components/terminal-pane/layout-serialization.test.ts
  • src/renderer/src/components/terminal-pane/layout-serialization.ts
  • src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/components/terminal-pane/replay-guard.test.ts
  • src/renderer/src/components/terminal-pane/replay-guard.ts
  • src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts
  • src/renderer/src/lib/pane-manager/pane-lifecycle.ts
  • src/renderer/src/lib/pane-manager/pane-manager.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-foreground-render-settle.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.ts
  • src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.test.ts
  • src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.ts
  • src/renderer/src/lib/pane-manager/terminal-ligatures-addon.test.ts
  • src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts
  • tests/e2e/terminal-foreground-redraw-freeze.spec.ts

@nwparker
nwparker force-pushed the nwparker/user-s-CPU-usage branch from c390023 to f8559b3 Compare July 11, 2026 00:15
@nwparker

Copy link
Copy Markdown
Contributor Author

CI note: the second verify run failed only three src/renderer/src/store/slices/store-cascades.test.ts cases for wrapped remote-runtime worktree-delete errors.

This is reproducible after rebasing onto current origin/main with:

pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/store-cascades.test.ts
# 3 failed, 110 passed

The regression arrived with #7997 (Fix SSH worktree delete routing after host reconnect), which changed src/renderer/src/store/slices/worktrees.ts; this PR changes no store/worktree code. The substantive version of this PR passed verify before main advanced. I am keeping the unrelated base regression out of this terminal-performance PR and will rerun/rebase after main is repaired.

Failed job: https://github.com/stablyai/orca/actions/runs/29132214554/job/86489555998

@nwparker
nwparker force-pushed the nwparker/user-s-CPU-usage branch from f8559b3 to 77ea614 Compare July 11, 2026 02:36
@nwparker

Copy link
Copy Markdown
Contributor Author

Rebased onto current main across #7214 (terminal-performance integration). The conflict resolution composes both sides:

  • Output scheduler: Terminal performance initiative: pipeline fixes + term-speed-2 revival + PTY flow control (integration branch) #7214's ack-credit flow control, composeParsedCallback, and parse-clock pacer are intact on every foreground/background write path; this PR's per-chunk beforeWrite and shouldRefreshForegroundSynchronously policy are layered on top (always-sync default preserves main's behavior for non-resolver callers).
  • Replay guard: main's probe-certified stall release kept as-is; the render options and lazy RTL joiner ride along. Test-only stallCheckMs now travels in the same options object.
  • pty-connection: main's hidden-output flood suppression untouched; beforeTerminalOutputWrite gains the pre-parse RTL joiner registration.

Re-validated post-rebase: renderer typecheck, 2,348 pane-manager/terminal-pane unit tests (including #7214's flow-control suites), the 2-scenario Electron redraw spec, lint/format, and the max-lines ratchet — all green. A two-lane adversarial review of the rebase surface returned clean. The previously reported store-cascades failures from #7997 are fixed on current main; those 114 tests pass on this branch.

@nwparker
nwparker merged commit c7777e4 into main Jul 11, 2026
5 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