Skip to content

docs: update replay cost comments after the viewport read change - #369

Merged
ThomasK33 merged 3 commits into
mainfrom
docs/sync-replay-cost-comments
Nov 10, 2026
Merged

ThomasK33 merged 3 commits into
mainfrom
docs/sync-replay-cost-comments

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Why

Three comments and one doc sentence about the ghostty-web replay no longer match the code or a measurement.

  1. src/export/webm.ts and src/export/README.md said the longest silent stretch of a healthy timed replay is 8.4 s, "under 3%" of the 5-minute stall window. That is false. A flush of 520 full-screen repaints at 600x150 had a stretch of 15.9 to 20.7 s in three runs on current main. The window is 300 s, so that is 5.3 to 6.9 %.
  2. The comment on MAX_SPEED_FRAME_BUDGET in backend.ts gave per-flush costs from before perf: read the ghostty-web viewport once per decode #365. perf: read the ghostty-web viewport once per decode #365 made the in-page scan of the screen much cheaper. The comment also gave no measured scan cost.
  3. The comment on readViewportCells in embeddedHarnessHtml.ts said "a read in the task of a write or a resize sees the new screen". That holds for a resize only because of how the harness resizes. A later change that resizes with wasmTerm.resize() reads a stale screen.
  4. docs/USAGE.md gave the time of 400 resizes at 600x150 as 910 s. That number came from before perf: read the ghostty-web viewport once per decode #365, and the doc did not say which stage it timed. The replay now takes 592 s to 985 s, and the whole export takes 595 s to 2,582 s, depending on host load.

No code changes. Every changed line is a comment, a README line or a doc sentence.

What

  • src/export/webm.ts and src/export/README.md: the examples of silent stretches are now measured examples on current main, not bounds. They name the host CPU pressure of the runs. The comment says a stretch depends on the output in one flush, the terminal size and the host load. I removed "grows with the terminal size" because my runs of the stretch at 600x150 do not vary the size. The size claim rests on the per-flush costs below: one full-screen repaint costs 18 to 72 ms per flush at 80x24 and 157 to 191 ms at 200x60. The code comment no longer speaks in the first person. The "longest is 8.4 s, under 3 %" sentence is gone. The sentence "the window is the old fixed limit on the whole replay" stays in the code comment. It is still true.
  • src/renderer/ghosttyWeb/backend.ts: the MAX_SPEED_FRAME_BUDGET comment has the new per-flush numbers, the scan cost, the definition of "per flush" (replay time divided by the number of flushes) and the pressure of each group of runs. I removed the "400 outputs 40 ms apart" sentence. It had the same flush count as the 20 ms case, and the 20 ms runs now cover pressure from 7 % to 79 %.
  • src/renderer/ghosttyWeb/embeddedHarnessHtml.ts: the readViewportCells comment now says a read after a write needs no update(). After a resize it holds only because Terminal.resize renders at once, the render calls getCursor(), and getCursor() calls update(). After a direct wasmTerm.resize(), getViewport() reads the old screen until update() runs.
  • docs/USAGE.md: each number now says what it times. "7,401 events took 473 s" is now "exporting ... took 477 s, of which the replay took 477 s" (my run of the same log). "400 resizes ... took 910 s" is now "exporting ... took 595 s and 2,582 s in two runs ... The replay took 592 s and 985 s of that. The second export spent the other 1,597 s outside the replay, in the browser start, the glyph check or the video finalize." The last new sentence says that the replay is the stage that the stall window watches.

I checked src/renderer/ghosttyWeb/README.md, src/export/README.md, docs/USAGE.md and RELEASE.md for other numbers from before #365. The ghostty-web README and RELEASE.md have none. docs/USAGE.md had the two numbers above. A repo-wide grep for 8.4 s, 3%, 11,000, 4.4 s and 1.3 to 2.0 finds no other copy.

Evidence

Every number comes from the real backend: generateWebmExport with a real GhosttyWebBackend and video, on main at 74c22d74, Node 26.2.0. Host CPU pressure is some avg10 of /proc/pressure/cpu, read by the scripts like this: the per-flush and stretch scripts read it at the start, every 2 s during the run and at the end, and the table shows the min to max of those readings. The scan script read it twice per run, at the start and at the end. The host was shared and busy (load average 10 to 165). Raw results and harness scripts are in docsync-evidence/ next to this file (results*.jsonl, harness/).

Per-flush cost, 80x24, 400 full-screen repaints 20 ms apart, max-speed (replay time / 400 flushes)

Run ms per flush Pressure
a6 18.5 7 to 9 %
a7 20.2 7 to 9 %
a8 22.8 13 to 16 %
a4 23.2 23 to 32 %
a5 28.5 28 to 31 %
a3 35.1 45 to 51 %
a1 41.7 54 to 74 %
a2 46.4 61 to 79 %
video-a1 71.9 67 to 74 %

The median page call is 16.4 to 16.7 ms (one animation frame) in the quiet runs. Before #365 (776ee5c9, interleaved with a1 to a3 on the same host): 44.5, 45.9 and 41.7 ms. At 80x24 #365 changes the cost per flush by less than the noise.

Worst case, 200x60, 2,400 full-screen repaints 17 ms apart, max-speed (2,400 flushes, 2,400 page calls)

Run Replay ms per flush Pressure
main b2 377 s 157 21 to 60 %
main b1 459 s 191 41 to 56 %
before #365 b2 510 s 213 33 to 63 %
before #365 b1 539 s 225 29 to 63 %

The runs alternated (main, before, main, before). In the two alternating pairs, main is 15 % and 26 % faster at 200x60 (459 s against 539 s, 377 s against 510 s), with overlapping pressure ranges. The old comment said 287 s to 405 s at pressure 15 to 45 %. My runs of the old code took 510 s and 539 s at 29 to 63 %, so the host is slower now. I did not change code to improve these numbers.

In-page scan (notePaintedGraphemes) of a full screen, median of 25

Size Main Before #365
80x24 1.2 ms and 1.1 ms 2.7 ms and 2.6 ms
200x60 5.0 ms and 5.3 ms 72.4 ms and 73.4 ms
600x150 96 ms and 88 ms 813 ms and 796 ms

Pressure was 36 to 44 % (the two readings of each of the four scan runs). This reproduces "about 75 ms to about 6 ms" at 200x60. The scan is now about 3 % of a 200x60 flush. The rest is the frame and the canvas draw.

Silent stretches at 600x150, recorded (the gaps that the stall watchdog sees)

Log Start Longest stretch End Replay Pressure
50 tiny outputs, 3 runs 1.34, 1.49, 1.41 s 0.46, 0.89, 0.58 s 1.25, 1.16, 1.16 s 3.4 to 4.0 s 41 to 53 %
one flush of 520 repaints of about 93,475 B each (48,607,273 B in all), 3 runs 1.90, 1.72, 1.22 s 20.7, 18.9, 15.9 s 1.26, 1.12, 1.41 s 18.6 to 23.9 s 36 to 51 %
one flush of 11,000 events of 4,096 B (45 MB), 2 runs 1.23, 1.60 s 3.5, 5.8 s 1.16, 1.20 s 9.4, 14.5 s 34 to 57 %
400 resizes, each after a tiny output 1.41 s 2.2 s 1.07 s 592 s 21 to 51 %
the same, second run 0.76 s 12.8 s 1.28 s 985 s 9 to 88 %

20.7 s is 6.9 % of the 300 s window. Replay time and whole export time. The Replay column is the time inside replayWithTiming. The whole export also covers the browser start, the video finalize, the browser close and the temp-folder cleanup. The glyph check after the replay reads a result that the final hold already cached, so it costs about nothing. I measured both for three runs:

Run Replay Whole export Outside the replay Pressure
400 resizes, first 591.7 s 595.0 s 3.3 s 21 to 51 %
400 resizes, second 984.6 s 2,581.9 s 1,597.3 s 9 to 88 %
7,401 events, accelerated 476.6 s 477.3 s 0.6 s 9 to 48 %

The raw data cannot split the 1,597 s between the steps outside the replay. The harness timed the replay and the whole export, and nothing between them. I did not repeat the run. The docs name the steps outside the replay and do not say which one took the time. All of them run outside the stall watchdog. A verifier measured 13.9 s (4.6 %) and 9.5 s for the same kind of flush while reviewing #356. Both are below my 15.9 to 20.7 s. I did not measure them, so the comments do not use them. The longest stretch that I measured is 20.7 s.

Other runs

  • 7,401 events 1 ms apart, alternating output and input, 80x24, accelerated: replay 477 s and whole export 477 s at pressure 9 to 48 %. fix: stop a webm replay only when the renderer stalls #356 measured 473 s for the whole export.
  • Video length (ffprobe): the 400-repaint run at 80x24 had a replay of 28.8 s and a video of 30.5 s. The video records the replay as it runs, so 2,400 flushes of 40 ms or more make 96 s or more.

Claims of the MAX_SPEED_FRAME_BUDGET comment checked against the code

  • Flush count: minSplitMs = max(1, hold, span / 2400) (backend.ts:759). The 17 ms log has span 40.8 s, a split of 17.0 ms and 2,400 flushes. Measured 2,400.
  • Resize sentence: case 'resize' calls flushOutputBatch(true, preResizeHoldMs) (backend.ts:904-907). That ends a flush only when output waits. It is outside the budget. True.
  • "No limit on the length of its replay": replayWithStallWatchdog (webm.ts:156) restarts a 5-minute timer on every report. record-export.ts and src/export/ have no other timer. Runs of 459 s, 539 s, 592 s and 985 s finished. True.
  • 96 s: VIDEO_FPS = 25 (backend.ts:173) and recordVideo.fps (backend.ts:1415) give 40 ms per frame. 2,400 x 40 ms = 96 s. True.
  • One page call per flush: writeBatchBridge was called 400 times for 400 flushes and 2,400 times for 2,400. True.

Item 3, the stale viewport read (ghostty-web 0.4.0, node_modules/ghostty-web/dist/ghostty-web.js)

  • On the wasm terminal (wasmTerm, minified class K): getViewport() (line 225) never calls update(). getCursor() (line 176) calls update() (line 177). resize (line 147) does not call update().
  • Terminal.writeInternal (line 2388) calls wasmTerm.write and no update().
  • Terminal.resize (line 2422) calls wasmTerm.resize(...) and then this.renderer.render(...). render (line 1397) starts with A.getCursor().
  • The harness resizes only at embeddedHarnessHtml.ts:813 (terminal.resize(cols, rows)).
  • Experiment in the real page (docsync-evidence/stale1.out, stale2.out), screen filled with one letter per row:
    • A write and then getViewport() in the same task shows the new text. Same for terminal.write.
    • terminal.resize(40, 10) and then getViewport() equals the result after update() (rows TTTT..., UUUU...).
    • wasmTerm.resize(40, 10) and then getViewport() gives the old rows read with the new stride (AAAA..., BBBB...). After update() it gives TTTT....
    • wasmTerm.resize(40, 10), then getCursor(), then getViewport() is fresh. Resizing back to 80x24 without update() gives rows A, C, E, G and not A, B, C, D.

Validation

Run on the final head (the review rounds changed only docs/USAGE.md, src/export/README.md, the comment in src/export/webm.ts and one comment in src/renderer/ghosttyWeb/backend.ts):

  • npm run format:check: pass.
  • npm run lint: 0 warnings, 0 errors.
  • npm run typecheck: pass.
  • npx vitest run test/unit: 149 files, 2,401 of 2,401 tests pass (pressure 44 to 49 %). An earlier full run at pressure 65 to 72 % failed 3 tests by their test timeouts (5 s, and 30 s for artifactManifestLock). The tests were doctor.test.ts > returns unique passing checks ..., gc.test.ts > removes the session folder when a manifest write lands during its rm and artifactManifestLock.test.ts > lets one writer at a time in when many find a dead owner's lock. The three files passed together on a rerun at pressure 64 to 69 %. A full run on origin/main at 74c22d74 at pressure 64 % failed doctor.test.ts again and 3 other tests (lifecycle.test.ts zombie tests). So these are load timeouts, not caused by a comment change.
  • check.sh --drift-only: "Covered code matches the sync record."

Earlier on the first head, I also ran npx vitest run --maxWorkers=1 --retry=0 test/integration/renderer-backend.test.ts (36 of 36 pass), because the first head changed a comment inside the template string of embeddedHarnessHtml.ts. The review round did not touch that file. On that first run, test/unit/commands/gc.test.ts > removes the session folder when a manifest write lands during its rm hit its 5 s timeout at pressure 78 % and passed on rerun alone. It passed in the run above. The change touches no code.


Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh

- webm.ts and the export README gave a longest silent stretch of 8.4 s,
  under 3% of the stall window. A flush of 520 full-screen repaints at
  600x150 has a stretch of 15.9 to 20.7 s on main. The examples are now
  measured examples with their host CPU pressure, not bounds.
- The MAX_SPEED_FRAME_BUDGET comment gets per-flush costs and scan costs
  measured after #365, with the pressure of each group of runs.
- The readViewportCells comment says why a read after a resize is fresh:
  Terminal.resize renders at once and the render calls getCursor(), which
  calls update(). A direct wasmTerm.resize() reads the old screen.
- docs/USAGE.md: the replay time of 400 resizes at 600x150 is 592 s and
  985 s, not 910 s.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T15:14:26.142616Z a47d908 Manual request
🔒 Security Review ✅ Completed 2026-11-10T15:15:10.613686Z a47d908 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: ffe6155512

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ffe6155512

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/USAGE.md Resolved
- docs/USAGE.md: each number says what it times. The 400-resize export
  took 595 s and 2,582 s in all, of which the replay took 592 s and
  985 s. The second export spent 1,597 s outside the replay. The
  7,401-event export took 477 s, all of it in the replay.
- webm.ts and the export README: a stretch depends on the output in one
  flush, the terminal size and the load of the host. The measurements do
  not isolate the terminal size alone. The examples drop the first person.
The fastest 80x24 run took 7,393 ms for 400 flushes, 18.48 ms per flush.
The comment said 19 to 29 ms. It now says 18 to 29 ms.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: a47d908171

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: a47d908171

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 569c4fd Nov 10, 2026
28 checks passed
@ThomasK33
ThomasK33 deleted the docs/sync-replay-cost-comments branch November 10, 2026 16:13
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