mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
perf(tiles): replay a capture at xterm's own pace, not one slice a frame
A tile's replay (writeChunked) wrote its capture 32 KB per animation frame, so a 1 MiB load took about a second of frames, and in the grid the load queue's slot was held across all of it: tile N+1's capture waited for tile N's last frame. xterm 6 already parses its write queue in 12 ms slices and yields between them, so the slices now all go in at once (up to a 1 MiB window, since xterm's queue throws past 50 MB and Pane B's unbounded full=1 capture can reach the server's 32 MB) and the replay resolves on the callback of an empty write queued behind them, i.e. once xterm has parsed the last slice. The single-flight flag is still held for the whole replay. A disposed xterm never runs that callback, so destroy() now settles a replay in progress: a removed tile can no longer hold its flag or the grid's one load queue. Queued up front, the capture also stays in one piece during a refresh: live output written meanwhile lands after it, not between two of its slices. Measured (tileperf, 6 printing shells with 1 MiB histories, headless, n=3 interleaved A/B against the starting file, load 8.6 to 11.8): - grid fresh open, 6 tiles, all painted: 5.10 s -> 2.57 s (-50%); restore after reload: 6.49 s -> 3.86 s (-41%); per-tile replay 669 to 734 ms -> 298 to 321 ms (median). - Same work in half the time: frames over 20 ms 26% -> 40% of the (shorter) load window, about 86 -> 62 slow frames in all; longest long task on restore 304 -> 227 ms; server event-loop delay unchanged (max 111 to 122 -> 122 to 134 ms, one capture in flight throughout). - Split Pane B (the other TerminalTile) with the main terminal on WebGL and its long-task guard armed: load 1.6 to 3.8 s -> 0.8 to 1.7 s over 15 loads each; 0 long tasks of 200 ms or more either way, the guard never tripped. With an unbounded full=1 capture (about 21k lines): 2.5 to 3.4 s -> 1.9 to 2.6 s, 0 long tasks of 200 ms or more. - At checkpoint 1 (equivalent patch, n=3 to 6): fresh 6.4 -> 2.7 s, restore 8.9 -> 4.2 s, TUI-style reconnect 11.2 to 11.8 -> 6.2 s. Tests: the replay queues every slice at once and holds the flag until xterm has parsed it; a replay larger than the window goes one window at a time; a pane destroyed mid-parse settles at once; in the grid, a tile destroyed while xterm still parses its replay releases the queue and the next tile loads (fake xterm whose callbacks never run). The rAF-driven tests now hold the parse callbacks instead. All mutation-checked (no settle in destroy, settle before the parse, no window). Browser split-pane-terminal: same 1 failed / 2 passed as at the starting HEAD (the failure is in the test's own setup, before connect). Scope: PR 1 (terminal-tile.js writeChunked and destroy(); Pane B replays the same way). Moving it onto PR 1 needs its two call sites adapted (PR 1 has no _runLoad yet) and leaves the tile-grid-load-queue.test.ts hunk with PR 2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,9 +86,13 @@ class FakeTerminal {
|
||||
attachCustomKeyEventHandler() {}
|
||||
registerLinkProvider() {}
|
||||
textarea = { addEventListener() {}, removeEventListener() {} };
|
||||
/** Set by a test: write callbacks never run, as on a disposed xterm. */
|
||||
holdParse = false;
|
||||
write(data: string, cb?: () => void) {
|
||||
this.writes.push(data);
|
||||
cb?.();
|
||||
// An empty write puts nothing on screen; the replay queues one only to hear
|
||||
// (its callback) that everything before it has been parsed.
|
||||
if (data) this.writes.push(data);
|
||||
if (!this.holdParse) cb?.();
|
||||
}
|
||||
clear() {
|
||||
this.writes.push('<CLEAR>');
|
||||
@@ -412,6 +416,27 @@ describe('teardown', () => {
|
||||
await Promise.all(connecting);
|
||||
});
|
||||
|
||||
it('destroying a tile while xterm still parses its replay settles the load, and the queue moves on', async () => {
|
||||
// The replay waits for xterm's write callback (writeChunked), which a
|
||||
// disposed xterm never runs: unsettled, the tile's load would hold the
|
||||
// grid's one queue, and every other tile behind it, forever.
|
||||
const { tiles } = makeGrid(['a', 'b']);
|
||||
const connecting = tiles.map((t) => t.connect());
|
||||
await settle();
|
||||
tiles[0].terminal!.holdParse = true;
|
||||
captures[0].answer('replay of a');
|
||||
await settle();
|
||||
// a's replay is still parsing: b waits its turn.
|
||||
expect(captures.map((c) => c.url.split('/')[3])).toEqual(['a']);
|
||||
|
||||
tiles[0].destroy();
|
||||
await settle();
|
||||
expect(captures.map((c) => c.url.split('/')[3])).toEqual(['a', 'b']);
|
||||
captures[1].answer('b');
|
||||
await settle();
|
||||
await Promise.all(connecting);
|
||||
});
|
||||
|
||||
it('a capture that never answers is cut off by its deadline, and the next tile loads', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { tiles } = makeGrid(['a', 'b']);
|
||||
|
||||
Reference in New Issue
Block a user