Repository navigation
perf(terminal): one agent-pty-data dispatcher per window and the WebGL renderer for xterm hosts #768
Description
Activity
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.
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.
- addedtype:perfPerformance or resource usePerformance or resource usesev:P3MinorMinor
on Sep 25, 2026 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.
TerminalHostCoreis dropped. What is left per host is ~60 lines of construct/fit/observe/dispose, and the hosts genuinely diverge in it:- AgentTerminalLeaf owns provider PTY size through dimension-ownership epochs, attach backfill queueing and pending input/resize replay (Same-id wake of a mounted terminal-view agent leaves the xterm frozen on the dead process #1281, fix(sessions): a raw PTY view keeps receiving output across a same-id wake (#1281) #1311);
- TerminalLeaf uses
convertEol: true, the shell scrollback default, and the pane toast wiring; - AgentInlineTerminal uses a different font size and no PTY size ownership.
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.
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:
TerminalHostCorewas 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
ipcRenderer.on('session:agent-pty-data')for the whole channel and filterssid !== sessionIdin JS (AgentTerminalLeaf.tsx:259-260,TerminalLeaf,AgentInlineTerminal; preloadsession.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-datais uncoalesced (onewebContents.sendper chunk,forwarder.ts:108-113).@xterm/addon-webgl/addon-canvasanywhere (ls node_modules/@xterm→ addon-fit, headless, xterm): xterm 6's DOM renderer rebuilds every dirty row's spans withreplaceChildren(DomRenderer.ts:441-460,520-525), and Claude Code repaints the whole viewport inside?2026brackets, 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
session:agent-pty-data(and oneterminal-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-webgl0.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-ReactTerminalHostCoreso this is one change in one place.Refs #760, #745, #749.
Refs #103, #390. Source: architecture audit 2026-09-03 (scratchpad/arch-perf-audit.md).