Skip to content

fix(app): reuse terminal cells during serialization - #46763

Merged
Hona merged 3 commits into
anomalyco:v2from
Hona:terminal-hidden-work
Sep 2, 2026
Merged

fix(app): reuse terminal cells during serialization#46763
Hona merged 3 commits into
anomalyco:v2from
Hona:terminal-hidden-work

Conversation

@Hona

@Hona Hona commented Sep 2, 2026

Copy link
Copy Markdown
Member
  • SerializeAddon recorded every style change by calling buffer.getLine(row).getCell(col) again. In ghostty-web, getLine copies the whole row out of WASM, so a colored log paid one extra row copy per SGR change. The cell already passed to _nextCell is an immutable snapshot of the same data, so reuse it.
  • No change to serialized output, final flush ordering, scrollback retention, PTY lifetime, or any public API.
// packages/app/src/session/terminal/serialize.ts
-      const line = this._buffer.getLine(row)
-      const cellFromLine = line?.getCell(col)
-      if (cellFromLine) {
-        this._cursorStyle = cellFromLine
-      }
+      // Ghostty cells are snapshots; rereading this cell copies the whole row again.
+      this._cursorStyle = cell

Benchmark

Closing a session tab whose terminal holds a full colored scrollback. Baseline 335e4ca56f (v2), candidate 618f154a39. 20 samples per case per build, serial ABBA blocks of 5, frozen production Vite bundles (00CB8784… / 8739C1BF…), Windows 11 / i7-12800H, Playwright 1.59.1 Chromium, 1440x900 viewport.

Metric (ms, median / p95) Before After
Close session tab → Home ready + snapshot complete 490.0 / 546.7 323.5 / 564.5
Serialization of retained scrollback 331.4 / 374.6 167.7 / 246.6
Teardown renderer TaskDuration 450.9 / 498.7 272.7 / 423.5
Serialized bytes (all 40 samples) 648,905 648,905
  • Workload: one connected production Terminal (172x11 cells) receiving a 12,000-line, 1,497,808-byte colored build-log fixture from a real PowerShell ConPTY (@lydell/node-pty) via a Playwright WebSocket route, without producer throttling. The app's 10,000-line setting retained 4,293 scrollback rows after the initial 80-column resize; both builds retained the same rows.
  • Path under test: native ConPTY → WebSocket fixture → production Terminal, writer, Ghostty WASM/canvas, SerializeAddon. Session metadata is mocked, transcript empty.
  • All 40 teardown snapshots were byte-identical across builds (SHA256 896afd33…), and the new unit test round-trips colors/styles through a real Ghostty terminal while asserting bounded row reads.

Controls measured in the same runs (unchanged code path; tails were noisy on this machine and are not attributed to the change):

Control (ms, median / p95) Before After
Visible output completion 1,922.7 / 1,951.0 1,926.5 / 3,997.0
Hidden output completion 1,920.7 / 2,940.7 1,923.9 / 4,011.4
Return to hidden terminal 20.4 / 26.7 24.5 / 55.4

Not claimed

  • No streaming, tail-latency, hidden-rendering, or memory improvement. Home-ready p95 did not improve.
  • Chromium renderer measurements only; not Electron process RAM, the production PTY backend, or IPC.
  • Ghostty still draws while the terminal panel is hidden (a separate diagnostic counted 82,044 fillText calls during hidden output on baseline). Pausing that needs a Ghostty visibility API and is out of scope here.

Benchmark harness

packages/app/e2e/performance/terminals/ adds a manual, Windows-only Playwright benchmark that owns its ConPTY process and preview server, checks completion in Ghostty plus completed write callbacks, and verifies input, focus, resize, no PTY deletion, and native process survival after UI teardown. It is excluded from normal discovery; see its README.

Before After

Rendering of the fixture is byte-identical in both builds.

Hona added 3 commits September 2, 2026 13:56
Stop request handlers and close the page before killing the fixture-owned
ConPTY so a debounced PTY resize cannot race the dead process. Await the
resize acknowledgement after the viewport change and count actual canvas
fillText draws when TERMINAL_DRAW_PROBE is set.
@Hona
Hona marked this pull request as ready for review September 2, 2026 07:58
@Hona
Hona requested a review from Brendonovich as a code owner September 2, 2026 07:58
Copilot AI lite review requested due to automatic review settings September 2, 2026 07:58

Copilot AI 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.

🟡 Changes recommended

The new manual Windows benchmark needs stronger platform/env guarding (and a small buffer index safety fix) to avoid accidental non-Windows discovery failures and potential flakiness.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Improves terminal scrollback serialization performance in the app by avoiding redundant Ghostty WASM row copies during style changes, while adding targeted regression tests and a manual performance benchmark harness to validate behavior and measure impact.

Changes:

  • Reuse the already-available Ghostty cell snapshot when updating cursor style during serialization (avoids extra buffer.getLine(row) row copies).
  • Add unit tests covering serializer row-read behavior and terminal writer flush ordering.
  • Add a Windows-only, manual Playwright/Vite benchmark harness for end-to-end terminal workload + teardown measurement.
File summaries
File Description
packages/app/src/session/terminal/serialize.ts Reuses the current cell snapshot for cursor style updates to reduce row-copy overhead.
packages/app/src/session/terminal/serialize.test.ts Adds a regression test asserting bounded getLine reads and round-trip style fidelity.
packages/app/src/session/terminal/writer.test.ts Adds coverage for flush behavior when writes are in-flight and more output is queued.
packages/app/e2e/performance/terminals/vite.config.ts Builds a benchmark-only bundle that injects the probe module into index.html.
packages/app/e2e/performance/terminals/terminal-benchmark.spec.ts Manual benchmark spec driving a ConPTY workload through the production terminal stack.
packages/app/e2e/performance/terminals/shell.ps1 PowerShell fixture that emits the workload and responds to simple commands.
packages/app/e2e/performance/terminals/README.md Documents how to run the manual benchmark and what it measures.
packages/app/e2e/performance/terminals/probe.ts Benchmark-only instrumentation for render/write/serialize observation.
packages/app/e2e/performance/terminals/playwright.config.ts Dedicated Playwright config for running only the terminal benchmark spec.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +4 to +20
export default defineConfig({
...config,
testDir: ".",
testIgnore: [],
testMatch: "terminal-benchmark.spec.ts",
workers: 1,
retries: 0,
timeout: 120_000,
outputDir: process.env.TERMINAL_RESULTS,
reporter: [["line"]],
webServer: {
command: `bun x vite preview --host 127.0.0.1 --port ${new URL(process.env.PLAYWRIGHT_BASE_URL!).port} --strictPort --outDir ${process.env.TERMINAL_BUILD}`,
url: process.env.PLAYWRIGHT_BASE_URL,
reuseExistingServer: false,
},
use: { ...config.use, viewport: { width: 1440, height: 900 }, trace: "off", video: "off", serviceWorkers: "block" },
})
Comment on lines +12 to +26
// Use the same installed native PTY package as Core, with a fixture-owned process.
const native = createRequire(new URL("../../../../core/package.json", import.meta.url))("@lydell/node-pty") as {
spawn: (
file: string,
args: string[],
options: { cols: number; rows: number; cwd: string },
) => {
pid: number
write: (data: string) => void
resize: (cols: number, rows: number) => void
kill: () => void
onData: (handler: (data: string) => void) => { dispose: () => void }
onExit: (handler: () => void) => { dispose: () => void }
}
}
Comment on lines +342 to +346
const buffer = term.buffer.active
return Array.from(
{ length: term.rows },
(_, i) => buffer.getLine(buffer.length - term.rows + i)?.translateToString(true) ?? "",
)
@Hona
Hona enabled auto-merge (squash) September 2, 2026 08:03
@Hona
Hona merged commit 499e22b into anomalyco:v2 Sep 2, 2026
10 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.

2 participants