Files
NetMesh/components/terminal/runtime/writeCoalescer.test.ts

382 lines
11 KiB
TypeScript
Raw Permalink Normal View History

import assert from "node:assert/strict";
import { beforeEach, test } from "node:test";
import {
createWriteCoalescer,
MAX_PENDING_WRITE_COALESCE_BYTES,
WRITE_COALESCE_BURST_MS,
type WriteCoalescerOptions,
} from "./writeCoalescer.ts";
type FrameCallback = (time: number) => void;
let frameCallbacks: Map<number, FrameCallback>;
let timeoutCallbacks: Map<number, () => void>;
let nextFrameId: number;
const createTestCoalescer = (
write: (data: string) => void,
options: Omit<WriteCoalescerOptions, "scheduleFrame" | "scheduleTimeout"> = {},
) =>
createWriteCoalescer(write, {
...options,
scheduleFrame(callback) {
const id = nextFrameId;
nextFrameId += 1;
frameCallbacks.set(id, callback);
return () => {
frameCallbacks.delete(id);
};
},
scheduleTimeout(callback) {
const id = nextFrameId;
nextFrameId += 1;
timeoutCallbacks.set(id, callback);
return () => {
timeoutCallbacks.delete(id);
};
},
});
const fireFrame = (): void => {
const callbacks = [...frameCallbacks.values()];
frameCallbacks.clear();
for (const callback of callbacks) {
callback(performance.now());
}
};
const fireTimeout = (): void => {
const callbacks = [...timeoutCallbacks.values()];
timeoutCallbacks.clear();
for (const callback of callbacks) {
callback();
}
};
beforeEach(() => {
frameCallbacks = new Map();
timeoutCallbacks = new Map();
nextFrameId = 1;
});
test("coalesces chunks in the same frame into one write", () => {
const writes: string[] = [];
const coalescer = createTestCoalescer((data) => writes.push(data));
coalescer.push("foo");
coalescer.push("bar");
coalescer.push("baz");
assert.equal(writes.length, 0);
fireFrame();
assert.deepEqual(writes, ["foobarbaz"]);
});
test("microtask schedule mode flushes after the current turn without rAF", async () => {
const writes: string[] = [];
const coalescer = createWriteCoalescer((data) => writes.push(data), {
resolveScheduleMode: () => "microtask",
});
coalescer.push("a");
coalescer.push("b");
assert.equal(writes.length, 0);
await Promise.resolve();
assert.deepEqual(writes, ["ab"]);
coalescer.dispose();
});
test("burst mode flushes idle output on the next microtask", async () => {
const writes: string[] = [];
const coalescer = createWriteCoalescer((data) => writes.push(data), {
resolveScheduleMode: () => "burst",
});
coalescer.push("a");
coalescer.push("b");
assert.equal(writes.length, 0);
await Promise.resolve();
assert.deepEqual(writes, ["ab"]);
coalescer.dispose();
});
test("burst mode merges writes that arrive inside the 16ms window", () => {
const writes: string[] = [];
let now = 1000;
const timeouts: Array<{ callback: () => void; ms: number }> = [];
const coalescer = createWriteCoalescer((data) => writes.push(data), {
resolveScheduleMode: () => "burst",
now: () => now,
burstWindowMs: WRITE_COALESCE_BURST_MS,
scheduleTimeout(callback, ms) {
timeouts.push({ callback, ms: ms ?? 0 });
return () => {
const index = timeouts.findIndex((entry) => entry.callback === callback);
if (index >= 0) timeouts.splice(index, 1);
};
},
});
coalescer.push("first");
coalescer.flushSync();
assert.deepEqual(writes, ["first"]);
now += 4;
coalescer.push("second");
coalescer.push("third");
assert.deepEqual(writes, ["first"]);
assert.equal(timeouts.length, 1);
assert.ok(timeouts[0]!.ms <= WRITE_COALESCE_BURST_MS);
timeouts[0]!.callback();
assert.deepEqual(writes, ["first", "secondthird"]);
coalescer.dispose();
});
test("upgrades a pending microtask schedule to rAF when a later chunk requests it", () => {
const writes: string[] = [];
const microtasks: Array<() => void> = [];
const frames: Array<() => void> = [];
let modeForChunk = "microtask" as "microtask" | "raf";
const originalMicrotask = globalThis.queueMicrotask;
const originalRaf = Object.getOwnPropertyDescriptor(globalThis, "requestAnimationFrame");
const originalCancel = Object.getOwnPropertyDescriptor(globalThis, "cancelAnimationFrame");
globalThis.queueMicrotask = (callback: () => void) => {
microtasks.push(callback);
};
Object.defineProperty(globalThis, "requestAnimationFrame", {
configurable: true,
value: (callback: FrameRequestCallback) => {
frames.push(() => callback(0));
return frames.length;
},
});
Object.defineProperty(globalThis, "cancelAnimationFrame", {
configurable: true,
value: () => {},
});
try {
const coalescer = createWriteCoalescer((data) => writes.push(data), {
resolveScheduleMode: () => modeForChunk,
});
coalescer.push("shell");
assert.equal(microtasks.length, 1);
assert.equal(frames.length, 0);
modeForChunk = "raf";
coalescer.push("\x1b[?1049hTUI");
// Microtask cancelled; rAF armed for the combined batch.
assert.equal(frames.length, 1);
assert.deepEqual(writes, []);
// Cancelled microtask must not flush.
for (const task of microtasks.splice(0)) task();
assert.deepEqual(writes, []);
frames[0]!();
assert.deepEqual(writes, ["shell\x1b[?1049hTUI"]);
coalescer.dispose();
} finally {
globalThis.queueMicrotask = originalMicrotask;
if (originalRaf) {
Object.defineProperty(globalThis, "requestAnimationFrame", originalRaf);
} else {
Reflect.deleteProperty(globalThis, "requestAnimationFrame");
}
if (originalCancel) {
Object.defineProperty(globalThis, "cancelAnimationFrame", originalCancel);
} else {
Reflect.deleteProperty(globalThis, "cancelAnimationFrame");
}
}
});
test("coalesces a large TUI repaint until the scheduled frame", () => {
const writes: string[] = [];
const coalescer = createTestCoalescer((data) => writes.push(data));
const chunk = "x".repeat(4 * 1024);
for (let index = 0; index < 20; index += 1) {
coalescer.push(chunk);
}
assert.equal(writes.length, 0);
fireFrame();
assert.deepEqual(writes.map((write) => write.length), [80 * 1024]);
});
test("schedules a new frame for data arriving after a flush", () => {
const writes: string[] = [];
const coalescer = createTestCoalescer((data) => writes.push(data));
coalescer.push("first");
fireFrame();
coalescer.push("second");
fireFrame();
assert.deepEqual(writes, ["first", "second"]);
});
test("flushSync writes pending bytes immediately and cancels the scheduled frame", () => {
const writes: string[] = [];
const coalescer = createTestCoalescer((data) => writes.push(data));
coalescer.push("pending");
coalescer.flushSync();
assert.deepEqual(writes, ["pending"]);
fireFrame();
assert.deepEqual(writes, ["pending"]);
});
test("flushes synchronously when pending bytes exceed the cap", () => {
const writes: string[] = [];
const coalescer = createTestCoalescer((data) => writes.push(data));
const chunk = "x".repeat(MAX_PENDING_WRITE_COALESCE_BYTES);
coalescer.push(chunk);
coalescer.push("y");
assert.equal(writes.length, 1);
assert.equal(writes[0]?.length, MAX_PENDING_WRITE_COALESCE_BYTES + 1);
});
test("uses a tighter pending-byte cap when getMaxPendingBytes is set", () => {
const writes: string[] = [];
const cap = 64;
const coalescer = createTestCoalescer((data) => writes.push(data), {
getMaxPendingBytes: () => cap,
});
coalescer.push("x".repeat(cap - 1));
coalescer.push("yy");
assert.equal(writes.length, 1);
assert.equal(writes[0]?.length, cap + 1);
});
test("byte-cap flushes even while scheduled frames are gated", () => {
const writes: string[] = [];
let canFlush = false;
const cap = 8;
const coalescer = createTestCoalescer((data) => writes.push(data), {
getMaxPendingBytes: () => cap,
shouldFlushScheduledFrame: () => canFlush,
});
// Under the cap: gated frame holds.
coalescer.push("x".repeat(cap));
fireFrame();
assert.deepEqual(writes, []);
// Past the cap: drain immediately so hidden log floods stay bounded.
coalescer.push("y");
assert.deepEqual(writes, ["x".repeat(cap) + "y"]);
});
test("still resolves schedule mode before gated under-cap frames and cap drains", () => {
const writes: string[] = [];
let canFlush = false;
const probed: string[] = [];
const cap = 8;
const coalescer = createTestCoalescer((data) => writes.push(data), {
getMaxPendingBytes: () => cap,
shouldFlushScheduledFrame: () => canFlush,
resolveScheduleMode: ({ nextChunk }) => {
probed.push(nextChunk);
return nextChunk.includes("\x1b[?1049h") ? "raf" : "microtask";
},
});
// First chunk arms a gated under-cap frame; second exceeds cap and drains.
// resolveScheduleMode must still run so enter-alt latches fire.
coalescer.push("shell");
coalescer.push(`\x1b[?1049h${"x".repeat(cap)}`);
assert.deepEqual(probed, ["shell", `\x1b[?1049h${"x".repeat(cap)}`]);
assert.equal(writes.join("").includes("\x1b[?1049h"), true);
assert.equal(writes.join("").startsWith("shell"), true);
});
test("dispose flushes remaining bytes and stops accepting new chunks", () => {
const writes: string[] = [];
const coalescer = createTestCoalescer((data) => writes.push(data));
coalescer.push("tail");
coalescer.dispose();
assert.deepEqual(writes, ["tail"]);
coalescer.push("ignored");
fireFrame();
assert.deepEqual(writes, ["tail"]);
});
test("raf output drains when the animation frame callback never arrives", () => {
const writes: string[] = [];
const coalescer = createWriteCoalescer((data) => writes.push(data), {
scheduleFrame() {
// Chromium can suppress an already-requested frame while a window is
// occluded. Keep the callback pending forever to reproduce that edge.
return () => {};
},
scheduleTimeout(callback) {
timeoutCallbacks.set(1, callback);
return () => timeoutCallbacks.delete(1);
},
});
coalescer.push("stranded");
assert.deepEqual(writes, []);
fireTimeout();
assert.deepEqual(writes, ["stranded"]);
coalescer.dispose();
});
test("a gated frame keeps retrying until the terminal becomes visible", () => {
const writes: string[] = [];
let canFlush = false;
const coalescer = createTestCoalescer((data) => writes.push(data), {
shouldFlushScheduledFrame: () => canFlush,
});
coalescer.push("held");
fireFrame();
assert.deepEqual(writes, []);
assert.equal(coalescer.pendingBytes(), 4);
canFlush = true;
fireTimeout();
assert.deepEqual(writes, ["held"]);
assert.equal(coalescer.pendingBytes(), 0);
coalescer.dispose();
});
test("continuous sub-deadline chunks cannot postpone the wall-clock drain", () => {
const writes: string[] = [];
const coalescer = createWriteCoalescer((data) => writes.push(data), {
scheduleFrame() {
return () => {};
},
scheduleTimeout(callback) {
timeoutCallbacks.set(1, callback);
return () => timeoutCallbacks.delete(1);
},
});
try {
coalescer.push("a");
for (let index = 0; index < 8; index += 1) {
coalescer.push("x");
}
assert.equal(timeoutCallbacks.size, 1);
fireTimeout();
assert.ok(writes.length > 0, "continuous ingress must drain before it becomes idle");
} finally {
coalescer.dispose();
}
});