From ffe6155512e2491adf213be04c9e992ae79d9c82 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 10 Nov 2026 14:09:55 +0000 Subject: [PATCH 1/3] docs: update replay cost comments after the viewport read change - 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. --- docs/USAGE.md | 2 +- src/export/README.md | 2 +- src/export/webm.ts | 17 +++++++------ src/renderer/ghosttyWeb/backend.ts | 25 +++++++++++-------- .../ghosttyWeb/embeddedHarnessHtml.ts | 7 ++++-- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index fc1547e6..62a262c0 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -298,7 +298,7 @@ agent-tty record export --format webm --json | jq -c '.result.missi WebM export replays with recorded wall-clock timing by default. Pass `--timing accelerated` (idle gaps clamped to 400ms) or `--timing max-speed` for a time-compressed video. A frame of the video shows the screen after the output that arrived within one frame hold of the first output of that frame: 16 ms in `recorded` and `max-speed` mode, 100 ms in `accelerated` mode. Output events that close together share one frame, so a burst of output does not cost one frame per event, and a state that the next output of the frame replaces is not shown. In `recorded` and `accelerated` mode, an event that changes no pixel (an input, a marker, a run-complete, a signal or the exit) also ends a frame. In `max-speed` mode it does not: recorded time, a resize and the end of the recording end a frame, so output on both sides of an input event that is less than 16 ms apart shares one frame. `max-speed` removes the idle time. Recorded time ends a frame at most once per 16 ms, so a progress bar that updates every 30 ms shows many of its steps and not only its last one. Recorded time ends at most about 2,400 frames, so a recording longer than 38 s gets one frame per 1/2,400 of its recorded time instead (a 1-hour recording: one per 1.5 s). A resize that finds output waiting ends one more frame, and the limits above do not count it: 100,000 outputs that each have a resize 1 ms after them make 100,000 frames. A resize adds at most one frame, and none when no output is waiting. So the export time of a recording follows its length and its number of resizes, and not its number of output events. `max-speed` waits for one animation frame after each frame but holds no further time between two frames, so two frames that follow each other can end up in one video frame. The video has 25 frames per second, so it can skip a state that lasts less than one video frame (40 ms). A step of 30 ms is shorter than that, and the number of steps that survive depends on how fast the host runs the export. The screen before a resize is the exception: `max-speed` keeps it on the page for one video frame (40 ms) before the resize, so that the video shows it. The video can still miss it in rare cases. -With recorded timing the replay waits out every recorded gap, so an export takes at least as long as the recording. It takes longer when the recording is dense or resizes a large terminal, because each frame costs time in the browser and each resize of a large terminal costs seconds. On a shared host, a recording of 7.4 s with 7,401 events took 473 s at `--timing accelerated`, and a recording of 0.8 s with 400 resizes of a 600x150 terminal took 910 s at recorded timing. The export has no total time limit. It stops only when the renderer makes no progress for 5 minutes, and then fails with `EXPORT_ERROR`. The message names the window and the last event that the replay reached, and `details` holds `stallWindowMs`, `lastSeq` and `timingMode`. Waiting out a recorded gap is not a stall. +With recorded timing the replay waits out every recorded gap, so an export takes at least as long as the recording. It takes longer when the recording is dense or resizes a large terminal, because each frame costs time in the browser and each resize of a large terminal costs seconds. On a shared host, a recording of 7.4 s with 7,401 events took 477 s at `--timing accelerated`, and a recording of 0.8 s with 400 resizes of a 600x150 terminal took 592 s and 985 s in two runs at recorded timing, depending on the load of the host. The export has no total time limit. It stops only when the renderer makes no progress for 5 minutes, and then fails with `EXPORT_ERROR`. The message names the window and the last event that the replay reached, and `details` holds `stallWindowMs`, `lastSeq` and `timingMode`. Waiting out a recorded gap is not a stall. SVG export renders styled grid frames from the event log through the native `libghostty-vt` backend with no browser and no ffmpeg, so it requires the optional `@coder/libghostty-vt-node` package (there is no `ghostty-web` fallback). The output is deterministic: exporting the same session twice produces byte-identical, diffable SVG. `--format svg` writes a still image of the final screen and ignores event timing, so it also works on a log whose clock stepped backward (`--animate` refuses such a log and names the event where the clock stepped back); add `--animate` for an animated SVG of de-duplicated frames replayed with recorded event-log timing (`--timing` is not supported with SVG). diff --git a/src/export/README.md b/src/export/README.md index 3423b1a2..a679ed63 100644 --- a/src/export/README.md +++ b/src/export/README.md @@ -27,7 +27,7 @@ This directory converts a session's event log into recording exports. `generateA - A requested `libghostty-vt` renderer is replaced with `ghostty-web`. A code comment says `libghostty-vt` is semantic-only [webm.ts:generateWebmExport]. - The default backend factory accepts only `ghostty-web` [webm.ts:createDefaultBackend]. - Timing presets: `accelerated` caps gaps at 400 ms and holds frames at least 100 ms. `max-speed` uses a frame hold of 16 ms, but holds it only after the last flush of the replay and after a resize, and holds one video frame (40 ms) before a resize (see the `ghostty-web` README). Each mode holds the final frame (1000 ms, or 500 ms for `max-speed`) [webm.ts:buildReplayTimingOptions]. -- The timed replay has no total time limit. It fails with `WebmReplayStallError` only when the renderer makes no progress for `REPLAY_STALL_WINDOW_MS` (5 minutes). The backend reports progress through the `onProgress` argument of `replayWithTiming`: each report restarts the window. A report that comes before a wait on the timing schedule carries the length of that wait, and the window starts after it, so a long recorded gap is not a stall. A stretch without a report is one page call or a fixed hold, never the whole replay: a call carries at most 1000 chunks, and a report follows it. The stretches are the start to the first report (the replay sizes the terminal and holds one frame), one page call (the call that ends a flush also writes the chunks that the earlier calls of the flush held, so the event log limit of 50 MiB bounds it), and the last report to the end (the final frame hold of 1000 ms, or 500 ms at `max-speed`, with the glyph probes inside it, and then a read of the screen). Examples that I measured at 600x150 on a shared host under load: the start took 1.3 to 2.0 s, the end 1.8 to 2.8 s, a flush of 11,000 events of 4 KB 8.4 s, and one stretch inside the loop 4.4 s. These are examples and not bounds. The longest, 8.4 s, is under 3% of the window. No timer runs longer than 2^31-1 ms (the longest delay that a timer runs): a longer span is a chain of timers. The watchdog starts no timer once the replay has ended, failed or stalled. Boot and finalize run outside the watchdog [webm.ts:replayWithStallWatchdog, webm.ts:REPLAY_STALL_WINDOW_MS]. +- The timed replay has no total time limit. It fails with `WebmReplayStallError` only when the renderer makes no progress for `REPLAY_STALL_WINDOW_MS` (5 minutes). The backend reports progress through the `onProgress` argument of `replayWithTiming`: each report restarts the window. A report that comes before a wait on the timing schedule carries the length of that wait, and the window starts after it, so a long recorded gap is not a stall. A stretch without a report is one page call or a fixed hold, never the whole replay: a call carries at most 1000 chunks, and a report follows it. The stretches are the start to the first report (the replay sizes the terminal and holds one frame), one page call (the call that ends a flush also writes the chunks that the earlier calls of the flush held, so the event log limit of 50 MiB bounds it), and the last report to the end (the final frame hold of 1000 ms, or 500 ms at `max-speed`, with the glyph probes inside it, and then a read of the screen). Examples that I measured at 600x150 on a shared host under load (host CPU pressure 34 to 57 %): the start took 1.2 to 1.9 s and the end 1.1 to 1.4 s. One flush of 520 full-screen repaints of 93 KB (48.6 MB) had a longest stretch of 15.9 to 20.7 s in three runs, 5.3 to 6.9 % of the window. One flush of 11,000 events of 4 KB (45 MB) had a longest stretch of 3.5 s and 5.8 s in two runs. These are examples and not bounds: a stretch grows with the terminal size and the load of the host. No timer runs longer than 2^31-1 ms (the longest delay that a timer runs): a longer span is a chain of timers. The watchdog starts no timer once the replay has ended, failed or stalled. Boot and finalize run outside the watchdog [webm.ts:replayWithStallWatchdog, webm.ts:REPLAY_STALL_WINDOW_MS]. - `renderGridFramesToSvg` needs the profile font size `SVG_FONT_SIZE` (14) and draws fixed 8.4x18 px cells, so equal input gives byte-identical output [svg.ts:renderGridFramesToSvg]. It always embeds the JetBrains Mono subset, and embeds each other bundled face only when a glyph needs it [svg.ts:embeddedFontAssets]. Output past `defaultMaxSvgOutputLength` of the V8 heap limit (the heap limit divided by 8 bytes, at most `MAX_SVG_OUTPUT_LENGTH`) fails with `EXPORT_ERROR` [svg.ts:defaultMaxSvgOutputLength]. ## Side Effects diff --git a/src/export/webm.ts b/src/export/webm.ts index f9f7e5cf..08f3ea3f 100644 --- a/src/export/webm.ts +++ b/src/export/webm.ts @@ -33,13 +33,16 @@ import { invariant, unreachable } from '../util/assert.js'; // frame), one page call (the call that ends a flush also writes the chunks that // the earlier calls of the flush held, so the event log limit of 50 MiB bounds // it), and the last report to the end (the final frame hold, which also covers -// the glyph probes, and then a read of the screen). Examples that I measured at -// 600x150 on a shared host under load: the start took 1.3 to 2.0 s, the end 1.8 -// to 2.8 s, a flush of 11,000 events of 4 KB 8.4 s, and one stretch inside the -// loop 4.4 s. These are examples and not bounds. The longest, 8.4 s, is under -// 3% of the window. The window is the old fixed limit on the whole replay, so -// an export that fit before still fits. Boot and finalize run outside the -// watchdog. +// the glyph probes, and then a read of the screen). Examples that I measured +// at 600x150 on a shared host under load (host CPU pressure 34 to 57 %): the +// start took 1.2 to 1.9 s and the end 1.1 to 1.4 s. One flush of 520 +// full-screen repaints of 93 KB (48.6 MB) had a longest stretch of 15.9 to +// 20.7 s in three runs, 5.3 to 6.9 % of the window. One flush of 11,000 +// events of 4 KB (45 MB) had a longest stretch of 3.5 s and 5.8 s in two +// runs. These are examples and not bounds: a stretch grows with the terminal +// size and the load of the host. The window is the old fixed limit on the +// whole replay, so an export that fit before still fits. Boot and finalize +// run outside the watchdog. const REPLAY_STALL_WINDOW_MS = 5 * 60 * 1000; // Node runs a timer with a longer delay after 1 ms. const MAX_TIMER_DELAY_MS = 2 ** 31 - 1; diff --git a/src/renderer/ghosttyWeb/backend.ts b/src/renderer/ghosttyWeb/backend.ts index f1957bb6..9a02d61a 100644 --- a/src/renderer/ghosttyWeb/backend.ts +++ b/src/renderer/ghosttyWeb/backend.ts @@ -148,17 +148,20 @@ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1; * more flush, and this limit does not count it, so a log with many resizes * makes more (at most one more for each resize). A flush costs one * animation frame, one page call and one scan of the screen, and that cost - * grows with the terminal size and the load of the host. At 80x24, 400 - * outputs 20 ms apart took 18 to 20 ms per flush at host CPU pressure 4 to - * 22 %, and 400 outputs 40 ms apart took 27 to 54 ms per flush at pressure 38 - * to 70 %. A worst-case log of 2,400 full-screen repaints 17 ms apart at - * 200x60 took 287 s (120 ms per flush) in two runs at pressure 15 to 44 %, and - * 368 s to 405 s (153 to 169 ms per flush) in three runs of a verifier at - * pressure 23 to 45 %. A WebM export has no limit on the length of its replay, - * so a loaded host makes it slower and does not fail it. The export fails only - * when the renderer makes no progress for 5 minutes (REPLAY_STALL_WINDOW_MS in - * webm.ts). The video has 25 frames per second and records the replay as it - * runs, so this many flushes of 40 ms or more make a video of 96 s or more. + * grows with the terminal size and the load of the host. The scan of a full + * screen took 1 ms at 80x24, 5 ms at 200x60 and about 90 ms at 600x150 (median + * of 25 scans, at host CPU pressure 36 to 44 %: `some avg10` in + * /proc/pressure/cpu). The cost per flush below is the replay time divided by + * the number of flushes. At 80x24, 400 full-screen repaints 20 ms apart took 19 + * to 29 ms per flush at pressure 7 to 32 % (five runs) and 35 to 72 ms at + * pressure 45 to 79 % (four runs). A worst-case log of 2,400 full-screen + * repaints 17 ms apart at 200x60 took 377 s (157 ms per flush) at pressure 21 + * to 60 % and 459 s (191 ms per flush) at pressure 41 to 56 %. A WebM export + * has no limit on the length of its replay, so a loaded host makes it slower + * and does not fail it. The export fails only when the renderer makes no + * progress for 5 minutes (REPLAY_STALL_WINDOW_MS in webm.ts). The video has 25 + * frames per second and records the replay as it runs, so this many flushes of + * 40 ms or more make a video of 96 s or more. */ const MAX_SPEED_FRAME_BUDGET = 2_400; /** diff --git a/src/renderer/ghosttyWeb/embeddedHarnessHtml.ts b/src/renderer/ghosttyWeb/embeddedHarnessHtml.ts index 5a9be5b6..e24049aa 100644 --- a/src/renderer/ghosttyWeb/embeddedHarnessHtml.ts +++ b/src/renderer/ghosttyWeb/embeddedHarnessHtml.ts @@ -408,8 +408,11 @@ export const EMBEDDED_HARNESS_HTML = ` // read each cell once per row. The pool that getViewport() returns never // shrinks, so it can hold more cells than the screen: the width of the // screen sets the row stride, not the length of the pool. No update() is - // needed first: a read in the task of a write or a resize sees the new - // screen. + // needed first after a write: a read in the task of a write sees the new + // screen. After a resize it holds only because the harness resizes with + // Terminal.resize of ghostty-web, which renders at once, and the render + // calls getCursor(), which calls update(): after a direct + // wasmTerm.resize(), getViewport() reads the old screen until update(). function readViewportCells(wasmTerm, cols, rows) { const cells = wasmTerm.getViewport(); invariant( From a3643f9c9b7697a4775cd40529b571b1e7e44023 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 10 Nov 2026 15:01:23 +0000 Subject: [PATCH 2/3] docs: label replay and whole-export times, drop unmeasured size claim - 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. --- docs/USAGE.md | 2 +- src/export/README.md | 2 +- src/export/webm.ts | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index 62a262c0..db94babb 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -298,7 +298,7 @@ agent-tty record export --format webm --json | jq -c '.result.missi WebM export replays with recorded wall-clock timing by default. Pass `--timing accelerated` (idle gaps clamped to 400ms) or `--timing max-speed` for a time-compressed video. A frame of the video shows the screen after the output that arrived within one frame hold of the first output of that frame: 16 ms in `recorded` and `max-speed` mode, 100 ms in `accelerated` mode. Output events that close together share one frame, so a burst of output does not cost one frame per event, and a state that the next output of the frame replaces is not shown. In `recorded` and `accelerated` mode, an event that changes no pixel (an input, a marker, a run-complete, a signal or the exit) also ends a frame. In `max-speed` mode it does not: recorded time, a resize and the end of the recording end a frame, so output on both sides of an input event that is less than 16 ms apart shares one frame. `max-speed` removes the idle time. Recorded time ends a frame at most once per 16 ms, so a progress bar that updates every 30 ms shows many of its steps and not only its last one. Recorded time ends at most about 2,400 frames, so a recording longer than 38 s gets one frame per 1/2,400 of its recorded time instead (a 1-hour recording: one per 1.5 s). A resize that finds output waiting ends one more frame, and the limits above do not count it: 100,000 outputs that each have a resize 1 ms after them make 100,000 frames. A resize adds at most one frame, and none when no output is waiting. So the export time of a recording follows its length and its number of resizes, and not its number of output events. `max-speed` waits for one animation frame after each frame but holds no further time between two frames, so two frames that follow each other can end up in one video frame. The video has 25 frames per second, so it can skip a state that lasts less than one video frame (40 ms). A step of 30 ms is shorter than that, and the number of steps that survive depends on how fast the host runs the export. The screen before a resize is the exception: `max-speed` keeps it on the page for one video frame (40 ms) before the resize, so that the video shows it. The video can still miss it in rare cases. -With recorded timing the replay waits out every recorded gap, so an export takes at least as long as the recording. It takes longer when the recording is dense or resizes a large terminal, because each frame costs time in the browser and each resize of a large terminal costs seconds. On a shared host, a recording of 7.4 s with 7,401 events took 477 s at `--timing accelerated`, and a recording of 0.8 s with 400 resizes of a 600x150 terminal took 592 s and 985 s in two runs at recorded timing, depending on the load of the host. The export has no total time limit. It stops only when the renderer makes no progress for 5 minutes, and then fails with `EXPORT_ERROR`. The message names the window and the last event that the replay reached, and `details` holds `stallWindowMs`, `lastSeq` and `timingMode`. Waiting out a recorded gap is not a stall. +With recorded timing the replay waits out every recorded gap, so an export takes at least as long as the recording. It takes longer when the recording is dense or resizes a large terminal, because each frame costs time in the browser and each resize of a large terminal costs seconds. On a shared host, exporting a recording of 7.4 s with 7,401 events at `--timing accelerated` took 477 s, of which the replay took 477 s. Exporting a recording of 0.8 s with 400 resizes of a 600x150 terminal at recorded timing took 595 s and 2,582 s in two runs, depending on the load of the host. 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 replay is the stage that the stall window below watches. The export has no total time limit. It stops only when the renderer makes no progress for 5 minutes, and then fails with `EXPORT_ERROR`. The message names the window and the last event that the replay reached, and `details` holds `stallWindowMs`, `lastSeq` and `timingMode`. Waiting out a recorded gap is not a stall. SVG export renders styled grid frames from the event log through the native `libghostty-vt` backend with no browser and no ffmpeg, so it requires the optional `@coder/libghostty-vt-node` package (there is no `ghostty-web` fallback). The output is deterministic: exporting the same session twice produces byte-identical, diffable SVG. `--format svg` writes a still image of the final screen and ignores event timing, so it also works on a log whose clock stepped backward (`--animate` refuses such a log and names the event where the clock stepped back); add `--animate` for an animated SVG of de-duplicated frames replayed with recorded event-log timing (`--timing` is not supported with SVG). diff --git a/src/export/README.md b/src/export/README.md index a679ed63..bfdf34fd 100644 --- a/src/export/README.md +++ b/src/export/README.md @@ -27,7 +27,7 @@ This directory converts a session's event log into recording exports. `generateA - A requested `libghostty-vt` renderer is replaced with `ghostty-web`. A code comment says `libghostty-vt` is semantic-only [webm.ts:generateWebmExport]. - The default backend factory accepts only `ghostty-web` [webm.ts:createDefaultBackend]. - Timing presets: `accelerated` caps gaps at 400 ms and holds frames at least 100 ms. `max-speed` uses a frame hold of 16 ms, but holds it only after the last flush of the replay and after a resize, and holds one video frame (40 ms) before a resize (see the `ghostty-web` README). Each mode holds the final frame (1000 ms, or 500 ms for `max-speed`) [webm.ts:buildReplayTimingOptions]. -- The timed replay has no total time limit. It fails with `WebmReplayStallError` only when the renderer makes no progress for `REPLAY_STALL_WINDOW_MS` (5 minutes). The backend reports progress through the `onProgress` argument of `replayWithTiming`: each report restarts the window. A report that comes before a wait on the timing schedule carries the length of that wait, and the window starts after it, so a long recorded gap is not a stall. A stretch without a report is one page call or a fixed hold, never the whole replay: a call carries at most 1000 chunks, and a report follows it. The stretches are the start to the first report (the replay sizes the terminal and holds one frame), one page call (the call that ends a flush also writes the chunks that the earlier calls of the flush held, so the event log limit of 50 MiB bounds it), and the last report to the end (the final frame hold of 1000 ms, or 500 ms at `max-speed`, with the glyph probes inside it, and then a read of the screen). Examples that I measured at 600x150 on a shared host under load (host CPU pressure 34 to 57 %): the start took 1.2 to 1.9 s and the end 1.1 to 1.4 s. One flush of 520 full-screen repaints of 93 KB (48.6 MB) had a longest stretch of 15.9 to 20.7 s in three runs, 5.3 to 6.9 % of the window. One flush of 11,000 events of 4 KB (45 MB) had a longest stretch of 3.5 s and 5.8 s in two runs. These are examples and not bounds: a stretch grows with the terminal size and the load of the host. No timer runs longer than 2^31-1 ms (the longest delay that a timer runs): a longer span is a chain of timers. The watchdog starts no timer once the replay has ended, failed or stalled. Boot and finalize run outside the watchdog [webm.ts:replayWithStallWatchdog, webm.ts:REPLAY_STALL_WINDOW_MS]. +- The timed replay has no total time limit. It fails with `WebmReplayStallError` only when the renderer makes no progress for `REPLAY_STALL_WINDOW_MS` (5 minutes). The backend reports progress through the `onProgress` argument of `replayWithTiming`: each report restarts the window. A report that comes before a wait on the timing schedule carries the length of that wait, and the window starts after it, so a long recorded gap is not a stall. A stretch without a report is one page call or a fixed hold, never the whole replay: a call carries at most 1000 chunks, and a report follows it. The stretches are the start to the first report (the replay sizes the terminal and holds one frame), one page call (the call that ends a flush also writes the chunks that the earlier calls of the flush held, so the event log limit of 50 MiB bounds it), and the last report to the end (the final frame hold of 1000 ms, or 500 ms at `max-speed`, with the glyph probes inside it, and then a read of the screen). Examples measured at 600x150 on a shared host under load (host CPU pressure 34 to 57 %): the start took 1.2 to 1.9 s and the end 1.1 to 1.4 s. One flush of 520 full-screen repaints of 93 KB (48.6 MB) had a longest stretch of 15.9 to 20.7 s in three runs, 5.3 to 6.9 % of the window. One flush of 11,000 events of 4 KB (45 MB) had a longest stretch of 3.5 s and 5.8 s in two runs. These are examples and not bounds: a stretch depends on the output in one flush, the terminal size and the load of the host. No timer runs longer than 2^31-1 ms (the longest delay that a timer runs): a longer span is a chain of timers. The watchdog starts no timer once the replay has ended, failed or stalled. Boot and finalize run outside the watchdog [webm.ts:replayWithStallWatchdog, webm.ts:REPLAY_STALL_WINDOW_MS]. - `renderGridFramesToSvg` needs the profile font size `SVG_FONT_SIZE` (14) and draws fixed 8.4x18 px cells, so equal input gives byte-identical output [svg.ts:renderGridFramesToSvg]. It always embeds the JetBrains Mono subset, and embeds each other bundled face only when a glyph needs it [svg.ts:embeddedFontAssets]. Output past `defaultMaxSvgOutputLength` of the V8 heap limit (the heap limit divided by 8 bytes, at most `MAX_SVG_OUTPUT_LENGTH`) fails with `EXPORT_ERROR` [svg.ts:defaultMaxSvgOutputLength]. ## Side Effects diff --git a/src/export/webm.ts b/src/export/webm.ts index 08f3ea3f..775d3820 100644 --- a/src/export/webm.ts +++ b/src/export/webm.ts @@ -33,16 +33,16 @@ import { invariant, unreachable } from '../util/assert.js'; // frame), one page call (the call that ends a flush also writes the chunks that // the earlier calls of the flush held, so the event log limit of 50 MiB bounds // it), and the last report to the end (the final frame hold, which also covers -// the glyph probes, and then a read of the screen). Examples that I measured -// at 600x150 on a shared host under load (host CPU pressure 34 to 57 %): the +// the glyph probes, and then a read of the screen). Examples measured at +// 600x150 on a shared host under load (host CPU pressure 34 to 57 %): the // start took 1.2 to 1.9 s and the end 1.1 to 1.4 s. One flush of 520 // full-screen repaints of 93 KB (48.6 MB) had a longest stretch of 15.9 to // 20.7 s in three runs, 5.3 to 6.9 % of the window. One flush of 11,000 // events of 4 KB (45 MB) had a longest stretch of 3.5 s and 5.8 s in two -// runs. These are examples and not bounds: a stretch grows with the terminal -// size and the load of the host. The window is the old fixed limit on the -// whole replay, so an export that fit before still fits. Boot and finalize -// run outside the watchdog. +// runs. These are examples and not bounds: a stretch depends on the output +// in one flush, the terminal size and the load of the host. The window is +// the old fixed limit on the whole replay, so an export that fit before +// still fits. Boot and finalize run outside the watchdog. const REPLAY_STALL_WINDOW_MS = 5 * 60 * 1000; // Node runs a timer with a longer delay after 1 ms. const MAX_TIMER_DELAY_MS = 2 ** 31 - 1; From a47d9081710683dc5ca3c001380f81ca750cedb7 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 10 Nov 2026 15:10:26 +0000 Subject: [PATCH 3/3] docs: widen the per-flush range to cover the fastest run 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. --- src/renderer/ghosttyWeb/backend.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/ghosttyWeb/backend.ts b/src/renderer/ghosttyWeb/backend.ts index 9a02d61a..1f008fea 100644 --- a/src/renderer/ghosttyWeb/backend.ts +++ b/src/renderer/ghosttyWeb/backend.ts @@ -152,7 +152,7 @@ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1; * screen took 1 ms at 80x24, 5 ms at 200x60 and about 90 ms at 600x150 (median * of 25 scans, at host CPU pressure 36 to 44 %: `some avg10` in * /proc/pressure/cpu). The cost per flush below is the replay time divided by - * the number of flushes. At 80x24, 400 full-screen repaints 20 ms apart took 19 + * the number of flushes. At 80x24, 400 full-screen repaints 20 ms apart took 18 * to 29 ms per flush at pressure 7 to 32 % (five runs) and 35 to 72 ms at * pressure 45 to 79 % (four runs). A worst-case log of 2,400 full-screen * repaints 17 ms apart at 200x60 took 377 s (157 ms per flush) at pressure 21