Skip to content

perf(terminal): one agent-pty-data dispatcher per window and the WebGL renderer for xterm hosts #768

Description

@Juliusolsson05

Status (quality-loop audit, 2026-09-25)

Landed: #783: the keyed per-window dispatcher and WebGL with a DOM fallback (later moved to the beta line in #873).

Remaining: TerminalHostCore was never extracted. Scrollback is still 2000 everywhere; 500 for unfocused panes depends on #760.

Acceptance: The remaining items land, or they are dropped as not worth it.

Original report

Symptom

  • Each xterm host registers its own ipcRenderer.on('session:agent-pty-data') for the whole channel and filters sid !== sessionId in JS (AgentTerminalLeaf.tsx:259-260, TerminalLeaf, AgentInlineTerminal; preload session.ts:225); the preload's own header (session.ts:39-43) says to subscribe once per event type. N mounted panes → every PTY chunk delivered to N listeners, N−1 discard it; 984k chunks in the two-day run; agent-pty-data is uncoalesced (one webContents.send per chunk, forwarder.ts:108-113).
  • No @xterm/addon-webgl / addon-canvas anywhere (ls node_modules/@xterm → addon-fit, headless, xterm): xterm 6's DOM renderer rebuilds every dirty row's spans with replaceChildren (DomRenderer.ts:441-460,520-525), and Claude Code repaints the whole viewport inside ?2026 brackets, so nearly every row is dirty on every repaint (40–60 rows × 10–40 spans). The GPU process already exists (8–16% CPU); WebGL adds no process.

Expected

  • One session:agent-pty-data (and one terminal-data) subscription per window routing by session id to registered hosts (two hours; groundwork for the registry in perf(terminal): own xterm instances outside the React tree (per-session terminal registry) #760).
  • @xterm/addon-webgl 0.19.0 (released with xterm 6.0.0) loaded in the shared host lifecycle with context-loss → DOM fallback; extract the shared lifecycle of the three hosts into a non-React TerminalHostCore so this is one change in one place.
  • Renderer scrollback 2000 → 500 for non-focused panes once the registry exists.

Refs #760, #745, #749.
Refs #103, #390. Source: architecture audit 2026-09-03 (scratchpad/arch-perf-audit.md).

Activity

  1. Juliusolsson05 commented on Sep 5, 2026

    @Juliusolsson05
    OwnerAuthor

    Implementation evidence from 2026-09-04:

    • A live Agent Code run with one visibly unusable Claude raw terminal had 57 direct children and about 148% aggregate child CPU.
    • 14 Claude processes accounted for about 38% CPU, while Electron helpers accounted for about 54% CPU. Two renderer processes alone were repeatedly in the 13–18% range.
    • The current raw-terminal hosts still each subscribe to the global session:agent-pty-data / session:terminal-data channels and discard other session ids in their own callbacks, despite the preload contract saying those event types should be subscribed once per renderer.
    • The full-screen repaint pattern therefore multiplies callback delivery by mounted terminal count before the owning xterm uses the DOM renderer to rebuild dirty rows.
    • Replay-input fixes from perf(terminal): attaching an agent terminal replays 512 KB into xterm and floods stdin with query replies #745/perf(terminal): silence replies provoked by replay and coalesce keystrokes #749 are present and valuable, but they do not remove this live O(N) fanout or DOM-render cost.

    I am implementing this issue on perf/claude-terminal-throughput: one renderer-side keyed dispatcher per raw PTY channel, plus a shared WebGL-addon lifecycle with context-loss fallback for every xterm host. Regression tests will protect keyed delivery, listener cardinality, teardown, late async addon loading, and fallback.

  2. Juliusolsson05 commented on Sep 5, 2026

    @Juliusolsson05
    OwnerAuthor

    PR #783 implements keyed per-window PTY dispatch and lazy WebGL with safe DOM fallback across agent, shell and inline hosts. Full CI passed for implementation commit 4557b72; the documentation-only follow-up is rerunning checks. Real Electron context-loss/input smoke passed. This is a contribution, not closure: broader host-core/persistent-registry acceptance criteria remain open. No measured live key-to-echo gain is claimed until a controlled app restart.

  3. Juliusolsson05 commented on Sep 27, 2026

    @Juliusolsson05
    OwnerAuthor

    Triage (W2, 2026-09-27): closing the remaining items as not worth it, which the audit's acceptance allows. Evidence is from origin/main.

    Already shared. Every piece this issue asked to share is now its own module, used by all three xterm hosts (AgentTerminalLeaf, TerminalLeaf, AgentInlineTerminal):

    • terminal/sessionDataDispatcher (subscribeToAgentPtyData / subscribeToTerminalData): one subscription per window;
    • terminal/xtermWebglRenderer (attachXtermWebglRenderer): WebGL with context-loss → DOM fallback, now on the beta line (perf(terminal): re-enable WebGL rendering on the xterm beta line #873);
    • terminal/terminalWheelBoundary;
    • tile-tree/terminalInputForwarder.

    TerminalHostCore is dropped. What is left per host is ~60 lines of construct/fit/observe/dispose, and the hosts genuinely diverge in it:

    A shared core would either expose all of that as options or move the fragile sizing/attach logic behind a new abstraction. That is a large regression surface for no remaining perf gain: the per-chunk fan-out and the DOM renderer were the costs, and both are fixed.

    Scrollback 500 for unfocused panes depends on the per-session registry, so it moves to #760 (noted there).

    Claim marked done. Reopen if someone wants the extraction for its own sake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    sev:P3Minortype:perfPerformance or resource use

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions