mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
@@ -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.`;
|
||||
|
||||
@@ -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>");
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user