Skip to content

perf: write each ghostty-web replay flush in one frame - #358

Merged
ThomasK33 merged 17 commits into
mainfrom
perf/batch-ghostty-web-replay
Nov 9, 2026
Merged

ThomasK33 merged 17 commits into
mainfrom
perf/batch-ghostty-web-replay

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Why

Replay through the ghostty-web reference renderer cost about 17 to 23 ms per output event on main (17.1 and 18.5 ms in the evidence below, 19.3 ms and 20 to 23 ms in the Speed table). It affects offline screenshot, snapshot --renderer ghostty-web, wait, record diff and record export --format webm. It also affects the first live screenshot of a session, because the host replays the whole log in its own ghostty-web page. A 20,000-event log took 385.7 s for an offline screenshot (main at 7334f032, host load average 48 to 99). A log at the 250,000-event cap (#340) needs about 80 minutes at 19.3 ms per event (385.7 s over 20,000 events). I extrapolated that figure and did not run it.

The limit was not the browser round trips. Node already sent up to 1000 chunks in each page.evaluate call. The harness inside the page then awaited terminal.write(data, callback) once per chunk. ghostty-web runs that callback one requestAnimationFrame later, so every event waited one frame (16.7 ms in the evidence below, and longer under load).

Evidence, measured on main at 9c019706 (the ghostty-web code of the base 7334f032 is unchanged there). I replayed 2,001 one-character output events with replayTo in one process, in two runs at CPU pressure 15 to 18 %:

  • replayTo made 5 page.evaluate calls: the initial resize, 3 batch calls (1000, 1000 and 1 chunk) and the snapshot. The two batch calls of 1000 chunks took 34,153 of the 34,176 ms in the first run (16.8 s and 17.4 s) and 37,007 of the 37,036 ms in the second.
  • Node used 135 and 34 ms of CPU in the two runs. A no-op page.evaluate took 0.5 to 0.6 ms.
  • An awaited bridge.write('x') inside the page took 16.4 and 16.5 ms. The frame interval in the same page was 16.7 ms in both runs.

The same script on 08a8c13a replays the 2,001 events in 52 and 56 ms, in the same 5 calls. 08a8c13a is the commit of a later fix for logs with a resize. It differs from e98841cd only for such logs, so the measurements below that name 08a8c13a hold for e98841cd.

Writing a flush in one frame has consequences for a WebM export. This PR handles them:

  1. A WebM records every paint. A flush of more than 1000 chunks takes several page calls, and the screen between two calls must not paint. The page holds the chunks until the call that ends the flush.
  2. --timing max-speed must not collapse a recording that only prints into its last state.
  3. --timing max-speed must not make one frame per event. A sparse recording of 20,000 events took more than 5 minutes to export when every event was its own frame.
  4. The missing-glyph note must not cost a page call of its own for each flush.
  5. A page call that fails in the middle of a flush must not leave chunks in the page. HostRendererManager keeps the backend after a failed replay, and a retry meets them.

What

  • window.__agentTty.writeBatch(chunks, endsFlush, paintedCap) is new. The wasm write is synchronous. The call that ends a flush writes every held chunk in one task and then waits for one frame. A call that does not end the flush only holds its chunks. write() and resize() throw while chunks are held, because they run ahead of the held chunks.
  • flushOutputBatch sets endsFlush on the last page call of a flush only. A flush of exactly 1000 or 2000 chunks ends with its last call.
  • One page call per flush. In replayWithTiming, the call that ends a flush also adds the graphemes on the screen to the set that the missing-glyph check probes. paintedCap is the cap of that set. The note used to be a second page call that queued behind a frame. A failed note returns its message and does not fail the flush. The backend then omits the WebM missingGlyphs until it boots again. replayTo never asks for the note. A resize is still two page calls (the resize and the note).
  • Failed flush. When a page call, or the hook between two calls, fails, flushOutputBatch calls the new discardHeldChunks() in the page and then throws the original error. A retry starts from the screen that the failed flush found, and its output is written once. Before, a retry that started with a resize threw "resize() cannot run while writeBatch() holds the chunks of a flush" until the backend was disposed.
  • max-speed ends a flush by recorded time and at a resize. A flush ends at the first event that is at least max(16 ms, span / 2,400) of recorded time after the first output of the batch. I call that distance the split. The span is the time between the first and the last event that the replay applies. An input, marker, run-complete, signal or exit event does not end a flush. A resize and the end of the replay do. So recorded time makes about one flush per 16 ms for a recording shorter than 38 s and at most about 2,400 flushes for a longer one, whatever its number of output events. Each resize event ends one more flush, and the budget does not count it, so a recording with many resizes makes more flushes.
  • max-speed waits one frame hold (16 ms) after the last flush of the replay, after a resize, and before a resize that finds pending output. It holds nothing between two other flushes. One case has no hold before a resize (see Known limits).
  • recorded and accelerated flush as before: an event that changes no pixel ends a flush, and a hold follows every flush. accelerated keeps its documented 100 ms hold.
  • The live RPC path is not changed. It reaches this code through HostRendererManager.withBackend, so it gets the speed-up too. Since fix: keep a live render waiting while its replay moves forward #359, the host replays a long backlog in slices and each slice is one replayTo call, so each slice ends its own flush.
  • docs/USAGE.md, src/renderer/ghosttyWeb/README.md and src/export/README.md describe the frame rule, the budget and the hold.

What a max-speed WebM shows now

The page paints a flush once. So the states inside a flush never paint. main waited one animation frame after each chunk, so each chunk of a flush had a frame of its own. That is why main was slow and why it showed a state for every chunk.

Output on both sides of an input or marker event that is less than one split apart now shares a flush, where main ended the flush at the input. The table counts the distinct states that I decode from the video (method below). It shows what that costs.

Case main This PR
100 outputs 5 ms apart, an input between each pair 83 to 97 11 to 15
100 outputs 1 ms apart, an input between each pair 84 to 94 3 to 5
100 outputs 30 ms apart, an input between each pair 83 to 92 43 to 58
100 outputs 30 ms apart, no inputs 43 to 46 42 to 51
3 outputs at 0, 5 and 45 ms, one input at 1 ms (of 3 states) 2 or 3 (3 in 3 of 5 runs) 1 or 2 (2 in 2 of 5 runs)

Each cell is the lowest and the highest count of 5 runs, run in turn on the same host with main at 9c019706 (CPU pressure 1 to 25 %). The counts rise with the load of the host, because a slower flush gives the 25 fps video more time per state. An earlier run at CPU pressure 35 to 80 % gave 89 to 100, 86 to 100, 95 to 99, 56 to 79 and 2 or 3 for main, and 20 to 23, 5 to 7, 77 to 90, 63 to 92 and 2 for this PR. I craft the offline session so that each output paints a 10-cell code in row 0 (a white and a black reference cell and 8 grey cells of 2 bits). I export with record export --format webm --timing max-speed and read the first row of each video frame with ffmpeg -vf format=gray,crop=80:1:0:8 -fps_mode passthrough -f rawvideo. I count the distinct codes. The input events sit half a gap after each output (the same millisecond in the 1 ms case).

main shows more states with inputs because it is slower: it makes a flush and a 16 ms hold for each input, so its video is longer than the recording. The 30 ms case with inputs records 3 s of output. The video of main has 124 to 147 frames (5 to 6 s at 25 fps) and the video of this PR has 82 to 103 frames. The 5 ms case records 0.5 s. The video of main has 123 to 156 frames and the video of this PR has 51 to 58. In main a state stays in the video for more than 40 ms, and in this PR a state of 30 ms or less can fall between two video frames. Without inputs, both show about 45 of 100 states on an idle host.

missingGlyphs of a WebM follows the same flush states, and it differs from the list of main in both directions. U+0378 is a code point that no bundled font has. I export 3 crafted sessions with --timing max-speed and list what each one reports.

Session main This PR
U+0378 at 0 ms, an input at 1 ms, the screen cleared at 5 ms lists U+0378 lists nothing
U+0378 at 0 ms, the screen cleared at 5 ms, no input lists nothing lists nothing
U+0378 at 0 ms, the screen cleared at 200 ms lists nothing lists U+0378

In recorded mode all three sessions give the same list as main (the first and the third list U+0378).

Known limits

  • A max-speed WebM shows fewer states than main when outputs are less than one split apart, also with inputs between them (the first two rows of the table above), and when inputs sit between outputs that are 30 ms apart (the third row). The video has 25 frames per second, so a state that lasts less than 40 ms can be skipped in any case.
  • A max-speed export of a sparse log on a large terminal is slower than on main. Each flush costs one page call, one animation frame and one scan of every screen cell. I measured the scan inside the page on 08a8c13a in 3 runs at CPU pressure 15 to 17 %: 1.7 to 2.0 ms (80x24), 6.2 to 7.0 ms (120x40) and 15.8 to 21.7 ms (200x60), against 0.5 to 1.7 ms for an empty page.evaluate. An earlier run on e819920b at pressure 31 to 38 % gave 4.9 to 5.1 ms, 7.0 ms and 21 to 38 ms. main makes 5 page calls for the log in the table below and this PR makes 403.
  • Worst case at 200x60. A log of 2,400 full-screen repaints 17 ms apart on a 200x60 terminal makes about 2,400 flushes. I replayed it with replayWithTiming in max-speed mode on 08a8c13a. It took 287.0 s and 286.8 s (120 ms per flush, 2,403 page calls) at CPU pressure 15 to 44 %. main at 9c019706 took 115.0 s (9 page calls) at pressure 15 to 21 %. A verifier measured 367.9 s (153 ms per flush) at pressure 23 to 38 %. The CLI export of that log failed with EXPORT_ERROR at 306.1 s at pressure 42 %, and exported in 255.3 s at pressure 7.7 % (main: 151.5 s). The export gives its replay 5 minutes (REPLAY_TIMEOUT_MS), so on a loaded host a log like this can fail the export until fix: stop a webm replay only when the renderer stalls #356 merges. fix: stop a webm replay only when the renderer stalls #356 replaces that limit with a stall window of 5 minutes without progress, and each flush counts as progress. I sized the budget of 2,400 flushes from 112 ms per flush at 200x60, and the cost of a flush grows with the load of the host.
  • A max-speed flush that the timestamp check ends right before a resize has no hold before that resize. This is the case when the resize event, or an input before it, arrives at least one split after the first output of the pending batch. The resize then runs at once, and the screen before it can miss the video (Codex review of this head). In 10 exports of two crafted logs with the real CLI, the video kept the screen before the resize every time, so I did not manage to show the loss in a video. A unit test written for the fix shows the missing hold. I have a fix ready (13 lines of source and 3 tests) and did not include it.

Validation

Speed

Same crafted logs and same real CLI (screenshot, offline) on main and on this PR. I measured these on dfde02d9. The ghostty-web source of this branch differs from it in comments only, and the merge of main changes nothing under src/renderer/ghosttyWeb/. Host load average 48 to 99.

Events main This PR
2,000 46.8 s, 40.6 s 6.6 s, 6.4 s
20,000 385.7 s 7.2 s, 6.5 s
249,999 not run (about 80 min (extrapolated, not run)) 9.6 s, 8.5 s

The PNG sha256 is the same on main and on this PR for the 2,000-event log (fb5fb930...) and for the 20,000-event log (db073912...).

I ran the same logs again with the real CLI on this branch after the merge of main (CPU pressure 38 to 41 %). The 2,000-event screenshot took 6.7 s with the same sha256 (main at 9c019706: 52.5 s). The 20,000-event screenshot took 7.2 s with sha256 db073912.... The 249,999-event screenshot took 14.0 s and 13.3 s with sha256 a4bdcf4c..., the same as before. A live screenshot through a host (--renderer ghostty-web) also works on this branch.

max-speed WebM export time

I export with main (9c019706) and this PR in turn, 3 runs per cell for the page-level logs and 5 for the real session. The host was fairly quiet (CPU pressure 4 to 22 %). The ratio is the median of the paired ratios, with its range.

Log main This PR This PR / main
400 outputs 20 ms apart, each repaints 80x24 7,268 ms, 8,682 ms, 7,282 ms 7,346 ms, 7,994 ms, 8,087 ms 1.01 (0.92 to 1.11)
400 outputs 20 ms apart, each repaints 200x60 23,014 ms, 20,449 ms, 24,444 ms 39,758 ms, 27,315 ms, 34,885 ms 1.43 (1.34 to 1.73)
2,000 outputs 1 ms apart, 80x24 36,086 ms, 34,118 ms, 34,024 ms 3,104 ms, 3,291 ms, 2,621 ms 0.09 (0.08 to 0.10)
a real shell session of 98 events (7 typed commands, a vitest run, 9 s), 80x24 2.9 s (2.8 to 3.7 s) 2.2 s (2.1 to 2.7 s) 0.73 (0.71 to 0.76)

Page calls for the 400-output log: main 5, this PR 403.

An earlier run of the two 400-output logs on a busier host (CPU pressure 35 to 80 %, main at 7334f032) gave ratios of 1.40 (1.22 to 1.65) at 80x24 and 1.56 (0.74 to 1.82) at 200x60. The cost of a flush grows with the load of the host.

Two long WebM exports through the real CLI (80x24, one run each, CPU pressure 10 to 22 %):

Log main This PR
20,000 outputs 20 ms apart (400 s of recording) fails after 301.8 s (the 5-minute replay limit) exported in 40.4 s
10,000 outputs 40 ms apart with an input between each pair (20,000 events, 400 s of recording) fails after 301.0 s exported in 42.5 s

On a busier host (CPU pressure up to 80 %) the first export took 71 to 161 s (5 runs) and the second took 81 s.

Same output as before

Differential check of main against this PR. 67 seeded logs, each replayed to 12 target seqs (802 targets). The logs hold resizes, wide glyphs and emoji, combining marks, 256-colour and truecolor, mouse-mode toggles, markers, input and signal events, alt screen, scroll regions, split escape sequences and scrollback. 13 logs hold a run of more than 1,000 output events. 9 of them hold an isolated run of exactly 999, 1000, 1001, 1999, 2000, 2001, 2999, 3000 or 3001 chunks. At every target, the replay state, the snapshot JSON with scrollback and cells, and both cursor variants of the screenshot PNG (with the ScreenshotResult fields) are identical. That is 936 snapshots and 1,872 PNGs with 0 differences. 288 targets carry a non-empty missingGlyphs list. The same logs through replayWithTiming (max-speed, and accelerated with maxGapMs 5) end in the same state, snapshot and PNGs (134 runs). I ran this check on dfde02d9. The ghostty-web source of this branch differs from it in comments only, and the merge of main changes nothing under src/renderer/ghosttyWeb/.

11 more seeded logs crash or hang the ghostty-web page on main and on this PR alike (Target crashed, a RangeError from terminal.write, or no end). I left them out. A control with a planted bug (the page drops the first chunk of every flush) is flagged in 8 of 8 logs and at 57 of 95 targets.

Tests

  • npx vitest run test/unit: 148 files, 2,334 tests pass on e98841cd.

  • npm run format:check, npm run lint, npm run typecheck and check.sh --drift-only ("Covered code matches the sync record") pass.

  • Unit, test/unit/renderer/ghosttyWebBackend.test.ts (62 tests):

    • The page call that ends a flush gets the note cap, and no other call does. replayTo asks for no note. A replay makes exactly one page call per 1000 chunks, with no call for the note.
    • A page that reports a failed note: the backend omits the glyph fields, logs once and asks for no more notes.
    • A failing page call, and a failing hook between two calls, drop the held chunks and reject with the original error. A page that cannot drop them either does not hide the original error.
    • 9,000 outputs 20 ms apart, and 9,000 outputs 40 ms apart with an input between each pair, both make exactly 2,250 flushes, and no output is lost or moved. A budget of 2,000 gives 1,800 flushes and a budget of 3,000 gives 3,000.
    • The frame budget counts the events that the replay applies (an earlier replay applied most of the log).
    • max-speed ends no flush at an input or exit event and recorded does.
    • The hold: one frame hold between the last flush and a resize, one after a resize, and one after the last flush of the replay. The note follows a resize at once, in recorded and max-speed mode.
  • Integration, test/integration/renderer-backend.test.ts, real Chromium (33 tests):

    • A replay whose second page call fails, then a retry that starts with a resize, shows the output once and in order.
    • A flush of several page calls paints one frame in replayTo and in replayWithTiming. The test waits for a quiet first screen before it counts frames.
    • A glyph that a flush painted and a later flush erased is still recorded, and the flushes use no page call of their own for the note.
    • A glyph that a resize brings back from the scrollback is recorded.
    • A note that fails (a cap of 0) still writes the flush, and the call returns the message.
    • The call that ends a flush refreshes data-cols and data-rows of the body.
    • The painted set stops at the cap and reports itself as incomplete.
  • Mutations (each changes one thing and must turn a test red). The control with identical text stays green. I ran them on e39bd151. The ghostty-web source and the two test files above are identical on this branch.

    Mutation Red tests
    the budget is 2,000 flushes 2 unit (the exact count of 2,250)
    the budget is 3,000 flushes 2 unit
    no frame budget 2 unit
    the budget uses the number of events, not the span 2 unit
    the span starts at seq 0 and not after the applied seq 1 unit
    a flush needs more than the split, not at least 1 unit
    max-speed ends a flush at input events again 3 unit
    no hold before a resize 1 unit
    no note after a resize 1 integration (glyph from the scrollback), 4 unit
    no hold after a resize 1 unit
    no hold after the last flush 1 unit
    max-speed holds every flush 1 unit
    the call that ends a flush never asks for the note 1 integration, 2 unit
    the note is asked on every call 1 unit
    replayTo asks for the note 1 unit
    the harness skips the note 3 integration
    a failed note fails the flush (harness) 1 integration
    the backend ignores a reported note failure 1 unit
    the cap check ignores an earlier failed note 1 unit
    the painted set ignores the cap 1 integration
    updateDocumentState is not called at the end of a flush 1 integration
    no discardHeldChunks call after a failure 1 integration (retry), 4 unit
    discardHeldChunks does nothing in the page 1 integration (retry)
    a failed discard hides the original error 2 unit
    every call of a flush writes at once and waits a frame 7 integration, among them both screencast tests
  • Integration and e2e files that reach the replay, renderer or export code, run with --maxWorkers=1 --retry=0 on the merge result (CPU pressure 6 to 49 %): renderer-backend (33 tests), record-export (17), screenshot-missing-glyphs (1), record-diff (10), screen-hash (6), cross-backend-screen-hash (6), wait-render (31), host-renderer-rpc (9), screenshot-out (9), empty-session-capture (11), batch (11), mouse (15), backend-selection (7), artifact-manifest-race (3), run (15), evals-snapshots (7), event-log (5), cli (77), slow-replay (5), create-slow-host (4), lifecycle (73) and the 10 test/e2e files (35 tests). That is 390 tests in 31 files. All pass in the first run, and no test needed a rerun. That run was on the merge result e819920b. e98841cd differs from it in docs only.

Merge with main and with #356

This branch contains main at 9c019706 (#359 to #362) through a normal merge commit. The merge had no conflict. #359 to #362 touch no file under src/renderer/ghosttyWeb/, src/export/ or the WebM export code.

I also merged fix/webm-timeout-follows-recording-length (3407142c) into this branch in a scratch worktree, after the merge of main. Three hunks conflict: the flushOutputBatch call in replayWithTiming, one line of src/renderer/ghosttyWeb/README.md and one line of src/export/README.md. I resolved the call so that the hook between two page calls reports { seq: highestProcessedSeq, scheduledWaitMs: 0 } to onProgress and returns a resolved promise, and the fourth argument asks for the note. The hook still runs after each page call, so progress is reported at least once per page call. tsc is clean after the merge. 3 tests of that merge then need an edit in #356, and the full unit suite (148 files, 2,360 tests) passes:

  1. reports progress after each bridge batch of a timed replay flush (test/unit/renderer/ghosttyWebBackend.test.ts) lists a note entry between a write and its report. There is no separate note call now, so the entries go.
  2. The max-speed row of exports a dense $timingMode recording whose replay runs far past its plan (test/unit/export/webm.test.ts) assumes that an input event after each output event forces a flush. max-speed no longer does that, so the row goes. The accelerated row still holds.
  3. exports a max-speed recording whose output goes to the renderer in one flush needs event(seq, 0) (one timestamp) and not event(seq, seq). Its events are 1 ms apart, so max-speed splits them into about 2,300 flushes.

#356 removes the total 5-minute limit of the replay. After it, a max-speed export fails only when the renderer makes no progress for 5 minutes. The budget of this PR bounds the flushes that recorded time ends, and each resize adds one more, so the export time follows the length of the recording and its number of resizes.


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

The harness awaited terminal.write(data, callback) for every output
chunk. ghostty-web runs that callback one animation frame later, so a
replay cost about 17-24 ms per event even though Node already sent up
to 1000 chunks per page call.

Add a harness writeBatch(chunks, waitForFrame) that writes all chunks
at once and waits for one frame only when asked. The backend asks for
the frame on the last page call of a flush. Screenshots and snapshots
are byte-identical to before. In a WebM replay, the chunks of one flush
now share one video frame.
@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 6, 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-08T13:07:23.554347Z e98841c Manual request
🔒 Security Review ✅ Completed 2026-11-08T13:05:38.739845Z e98841c 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: ff8f2d264c

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: ff8f2d264c

ℹ️ 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 src/renderer/ghosttyWeb/backend.ts Resolved
… flush once

Max-speed flushed once per run of output events, and a flush is one frame
since the batched write. A WebM of an output-only recording then showed only
its final state. Max-speed now flushes every frame hold (16 ms) of recorded
time and holds the frame only after the last flush of a run, so it is never
slower than one frame per output event.

A flush of more than 1000 chunks takes several page calls. ghostty-web paints
on every animation frame, so a state between two calls reached the video. The
page now holds the chunks of a flush until the call that ends it and writes
them in one task. Tests pin flushes of exactly 1000 and 2000 chunks and read
the screencast frames.
@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 Security Review

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

Reviewed commit: 060eed1df6

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: 060eed1df6

ℹ️ 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 src/renderer/ghosttyWeb/backend.ts Resolved
@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 Security Review

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

Reviewed commit: dfde02d942

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: dfde02d942

ℹ️ 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 src/renderer/ghosttyWeb/backend.ts Resolved
@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 Security Review

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

Reviewed commit: e98841cdc0

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: e98841cdc0

ℹ️ 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 src/renderer/ghosttyWeb/backend.ts Resolved
Comment thread src/renderer/ghosttyWeb/backend.ts Resolved
@ThomasK33

Copy link
Copy Markdown
Member Author

Review-cap note: Codex round 4 on e98841c raised two P2 findings (frame budget does not scale with terminal size; pre-resize hold after a timed flush). The repo owner waived the code lane for this PR at this head. Both fixes, plus doc and comment corrections from verification, ship in a follow-up PR right after this merge.

@ThomasK33

Copy link
Copy Markdown
Member Author

Correction to the review-cap note above: the follow-up work is three PRs in this run, not one. (1) The pre-resize frame hold fix plus the doc and comment corrections, opened right after this merge. (2) A rework of the max-speed flush rule, with a frame limit based on the flushes the replay will make. (3) A cheaper per-flush glyph scan, which cuts the cost that grows with terminal size. PR #356, which replaces the fixed five-minute replay limit, merges right after this PR.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 934760d Nov 9, 2026
29 checks passed
@ThomasK33
ThomasK33 deleted the perf/batch-ghostty-web-replay branch November 9, 2026 09:38
@github-actions github-actions Bot mentioned this pull request Nov 9, 2026
ThomasK33 added a commit that referenced this pull request Nov 9, 2026
A resize event is two page calls: the resize and the note of the painted
graphemes. A report between them keeps each call a stretch of its own for the
stall watchdog.

The stall watchdog tests also follow the batched replay of #358 now, and new
tests cover the survivors of the earlier mutation runs: the "after event 0"
message, the cause and output path of a stall error, an infinite wait, the
stall window check and a late report after the replay ended.
ThomasK33 added a commit that referenced this pull request Nov 9, 2026
The hook is a promise again, as the flush tests of #358 expect: a failing hook
must drop the held chunks and reject with its own error.
ThomasK33 added a commit that referenced this pull request Nov 9, 2026
Main holds #358 as one squash commit. Its tree equals the e98841c tree plus
#363, and this branch already had both, so the merge keeps this branch's files.
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