perf(process): stop padding rendered terminal lines to full width (#1450)

Rendered frames padded every visible row with trailing spaces to the
terminal width. Blank rows cost a full row each and every frame repeats
them, so the padding made up 14-39% of capture Evidence (2 KB of sparse
output produced 228 KB). The padding carried no information: columns is
recorded and serialized_state keeps the exact terminal state.

Lines are now trimmed after normalization, so line.padEnd(columns)
restores the previous value. The comparison contract commits
rendered_line_format so older padded captures do not compare as the
same contract.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-09 23:18:13 -07:00
1 parent c89c8f5318
commit 2781cad201
5 files changed
+66 -6

No files matched your search

+6
View File
@@ -110,6 +110,12 @@ while comparisons and trace assertions use the normalized text. Retaining the
original adds at most `limits.output_bytes` of text, because it comes from the
same admitted PTY chunks rather than a second unbounded stream.
Rendered frame `lines` hold the visible rows after normalization, without
trailing spaces; pad a line to the frame's `columns` to restore the fixed-width
row. `serialized_state` keeps the exact terminal state. Captures commit this
line format in their comparison contract, so a capture whose lines were padded
to full width does not compare as the same contract.
Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and
`listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with
ports from 0 through 65535. A port may end a sentence, as in `port: 8080.`;
+1
View File
@@ -108,6 +108,7 @@ export const processComparisonContract = (
...(scenario.normalization.ports
? { port_normalization_version: "contextual-endpoints-v2" }
: {}),
rendered_line_format: "trailing-spaces-trimmed-v1",
});
/**
@@ -0,0 +1,47 @@
import { expect, it } from "vitest";
import { TerminalRenderer } from "./TerminalRenderer.js";
const renderFrames = async (
data: string,
normalize: (value: string) => string = (value) => value,
) => {
const renderer = new TerminalRenderer({
columns: 20,
rows: 4,
scrollback: 10,
maxBytes: 100_000,
normalize,
});
renderer.write(data, 0);
const frames = await renderer.frames();
const retention = renderer.retention();
await renderer.dispose();
return { frames, retention };
};
it("omits trailing blank cells from visible lines without losing rows", async () => {
const { frames, retention } = await renderFrames(
"left right \r\n\r\n indented",
);
const [frame] = frames;
expect(frame?.lines).toEqual(["left right", "", " indented", ""]);
// Fixed-width rows remain recoverable from the recorded column count.
expect(frame?.lines.map((line) => line.padEnd(frame.columns, " "))).toEqual([
"left right ",
" ".repeat(20),
" indented ",
" ".repeat(20),
]);
expect(retention.retained_bytes).toBe(
Buffer.byteLength(frame?.serialized_state ?? "") +
Buffer.byteLength("left right indented"),
);
});
it("trims only after normalization sees the full-width row", async () => {
const { frames } = await renderFrames("pid 4242", (value) =>
value.replace(/4242 +$/u, "<pid>"),
);
expect(frames[0]?.lines[0]).toBe("pid <pid>");
});
+11 -6
View File
@@ -14,6 +14,8 @@ const HeadlessPackage =
const SerializePackage =
require("@xterm/addon-serialize") as typeof import("@xterm/addon-serialize");
const TRAILING_SPACES = / +$/u;
interface TerminalRendererOptions {
readonly columns: number;
readonly rows: number;
@@ -106,13 +108,16 @@ export class TerminalRenderer {
const lines: string[] = [];
for (let row = 0; row < this.#terminal.rows; row += 1) {
const line = buffer.getLine(buffer.viewportY + row);
// Trailing blank cells carry no information beyond `columns`; dropping
// them keeps sparse screens from costing a full row per blank line.
lines.push(
this.options.normalize(
(line?.translateToString(false, 0, this.#terminal.cols) ?? "").padEnd(
this.#terminal.cols,
" ",
),
),
this.options
.normalize(
(
line?.translateToString(false, 0, this.#terminal.cols) ?? ""
).padEnd(this.#terminal.cols, " "),
)
.replace(TRAILING_SPACES, ""),
);
}
const serializedState = this.options.normalize(
@@ -64,6 +64,7 @@ itWithCaptureCapability(
).toBe(true);
expect(capture.manifest.comparison_contract).toMatchObject({
port_normalization_version: "contextual-endpoints-v2",
rendered_line_format: "trailing-spaces-trimmed-v1",
});
expect(capture.exit).toMatchObject({ code: 0, reason: "exited" });
expect(capture.cleanup).toMatchObject({