Skip to content

Commit fbc05cd

Browse files
committed
✅ add property-based tests + validation-gate benchmarks (PR 5)
Property tests cover identity monotonicity, eviction stability, estimation constraints, and viewport invariants under random ops. Benchmarks report informational gate results for all 7 perf gates.
1 parent c35b5e2 commit fbc05cd

2 files changed

Lines changed: 446 additions & 0 deletions

File tree

virtualizer/test/bench.test.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { describe, expect, it } from "./suite.ts";
2+
import { Virtualizer } from "../mod.ts";
3+
import { skipAnsiSequence } from "../ansi-scanner.ts";
4+
5+
function charMeasure(text: string): number {
6+
let cp = text.codePointAt(0)!;
7+
if (cp >= 0x4e00 && cp <= 0x9fff) return 2;
8+
if (cp < 0x20) return 0;
9+
return 1;
10+
}
11+
12+
function assertViewportInvariants(v: Virtualizer) {
13+
let vp = v.resolveViewport();
14+
for (let entry of vp.entries) {
15+
expect(entry.totalSubRows).toBe(entry.wrapPoints.length + 1);
16+
expect(entry.firstSubRow).toBeGreaterThanOrEqual(0);
17+
expect(entry.firstSubRow + entry.visibleSubRows).toBeLessThanOrEqual(entry.totalSubRows);
18+
for (let wp of entry.wrapPoints) {
19+
let i = 0;
20+
while (i < entry.text.length) {
21+
let skip = skipAnsiSequence(entry.text, i);
22+
if (skip > 0) {
23+
expect(wp <= i || wp >= i + skip).toBe(true);
24+
i += skip;
25+
} else {
26+
i++;
27+
}
28+
}
29+
}
30+
}
31+
}
32+
33+
// Generate ANSI-heavy lines like htop/npm output
34+
function ansiHeavyLine(len: number): string {
35+
let parts: string[] = [];
36+
for (let i = 0; i < len; i++) {
37+
if (i % 5 === 0) parts.push(`\x1b[${(i % 7) + 30}m`);
38+
parts.push(String.fromCharCode(65 + (i % 26)));
39+
}
40+
parts.push("\x1b[0m");
41+
return parts.join("");
42+
}
43+
44+
describe("Validation-gate benchmarks (informational)", () => {
45+
it("Gate 1: measureWidth overhead — appendLine with 10K lines", () => {
46+
let v = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 24, maxLines: 10_000 });
47+
let lines: string[] = [];
48+
for (let i = 0; i < 10_000; i++) {
49+
lines.push("a".repeat(80));
50+
}
51+
52+
let start = performance.now();
53+
for (let line of lines) {
54+
v.appendLine(line);
55+
}
56+
let elapsed = performance.now() - start;
57+
let perCall = (elapsed / 10_000) * 1000; // microseconds
58+
console.log(` Gate 1: ${perCall.toFixed(2)}μs/appendLine (target ≤1μs, fail >5μs)`);
59+
// Informational — not a hard pass/fail
60+
expect(v.lineCount).toBe(10_000);
61+
});
62+
63+
it("Gate 2: Extended ANSI corpus — invariants hold at multiple widths", () => {
64+
let corpus = [
65+
ansiHeavyLine(80),
66+
ansiHeavyLine(120),
67+
"\x1b[1m\x1b[31m" + "ERROR".repeat(20) + "\x1b[0m",
68+
"\x1b]0;npm install\x07\x1b[32m✓\x1b[0m packages installed",
69+
"plain text line with no escapes at all",
70+
];
71+
72+
for (let cols of [80, 40, 20]) {
73+
let v = new Virtualizer({ measureWidth: charMeasure, columns: cols, rows: 24 });
74+
for (let line of corpus) v.appendLine(line);
75+
assertViewportInvariants(v);
76+
}
77+
});
78+
79+
it("Gate 3: Estimation accuracy — ceil(dw/cols) vs exact match rate", () => {
80+
let v = new Virtualizer({ measureWidth: charMeasure, columns: 40, rows: 24, maxLines: 10_000 });
81+
let lines: string[] = [];
82+
for (let i = 0; i < 10_000; i++) {
83+
let len = 10 + (i % 100);
84+
if (i % 3 === 0) {
85+
lines.push("文字".repeat(len / 2));
86+
} else {
87+
lines.push("a".repeat(len));
88+
}
89+
}
90+
for (let line of lines) v.appendLine(line);
91+
92+
// Resolve to get exact wrap counts for visible lines
93+
let vp = v.resolveViewport();
94+
let matches = 0;
95+
let total = vp.entries.length;
96+
for (let entry of vp.entries) {
97+
let dw = v.getLineDisplayWidth(entry.lineIndex)!;
98+
let estimated = Math.max(1, Math.ceil(dw / 40));
99+
if (estimated === entry.totalSubRows) matches++;
100+
}
101+
let rate = total > 0 ? (matches / total) * 100 : 100;
102+
console.log(` Gate 3: ${rate.toFixed(1)}% match rate (target >99%, fail <95%)`);
103+
expect(rate).toBeGreaterThan(90); // soft check
104+
});
105+
106+
it("Gate 4: scrollToFraction performance — 100K lines", () => {
107+
let v = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 24, maxLines: 100_000 });
108+
for (let i = 0; i < 100_000; i++) {
109+
v.appendLine("a".repeat(40 + (i % 40)));
110+
}
111+
112+
let start = performance.now();
113+
v.scrollToFraction(0.5);
114+
let elapsed = performance.now() - start;
115+
console.log(` Gate 4: scrollToFraction(0.5) at 100K lines: ${elapsed.toFixed(2)}ms (target <5ms)`);
116+
expect(v.anchorLineIndex).toBeGreaterThan(0);
117+
});
118+
119+
it("Gate 5: Skip optimization — frame time comparison", () => {
120+
let v = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 50 });
121+
for (let i = 0; i < 1000; i++) v.appendLine(`line ${i}`);
122+
v.scrollBy(-500); // scroll to middle
123+
124+
// Append while scrolled up — should not need to resolve new lines
125+
let start = performance.now();
126+
for (let i = 0; i < 100; i++) {
127+
v.appendLine(`new line ${i}`);
128+
v.resolveViewport();
129+
}
130+
let withSkip = performance.now() - start;
131+
132+
// At bottom — appends change viewport
133+
let v2 = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 50 });
134+
for (let i = 0; i < 1000; i++) v2.appendLine(`line ${i}`);
135+
136+
start = performance.now();
137+
for (let i = 0; i < 100; i++) {
138+
v2.appendLine(`new line ${i}`);
139+
v2.resolveViewport();
140+
}
141+
let withoutSkip = performance.now() - start;
142+
143+
console.log(` Gate 5: scrolled-up=${withSkip.toFixed(2)}ms, at-bottom=${withoutSkip.toFixed(2)}ms`);
144+
// Just verify both complete without error
145+
expect(v.lineCount).toBeGreaterThan(0);
146+
});
147+
148+
it("Gate 6: resolveViewport ANSI-heavy vs plain — ratio", () => {
149+
let v1 = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 50 });
150+
let v2 = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 50 });
151+
152+
for (let i = 0; i < 200; i++) {
153+
v1.appendLine("a".repeat(80));
154+
v2.appendLine(ansiHeavyLine(80));
155+
}
156+
157+
let start = performance.now();
158+
for (let i = 0; i < 100; i++) v1.resolveViewport();
159+
let plain = performance.now() - start;
160+
161+
start = performance.now();
162+
for (let i = 0; i < 100; i++) v2.resolveViewport();
163+
let ansi = performance.now() - start;
164+
165+
let ratio = plain > 0 ? ansi / plain : 1;
166+
console.log(` Gate 6: plain=${plain.toFixed(2)}ms, ANSI=${ansi.toFixed(2)}ms, ratio=${ratio.toFixed(2)}x (target <2x)`);
167+
expect(ratio).toBeLessThan(10); // soft bound
168+
});
169+
170+
it("Gate 7: resize performance — 100K lines", () => {
171+
let v = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 24, maxLines: 100_000 });
172+
for (let i = 0; i < 100_000; i++) {
173+
v.appendLine("a".repeat(40 + (i % 40)));
174+
}
175+
176+
let start = performance.now();
177+
v.resize(40, 24);
178+
let elapsed = performance.now() - start;
179+
console.log(` Gate 7: resize(40, 24) at 100K lines: ${elapsed.toFixed(2)}ms (target <4ms, fail >8ms)`);
180+
expect(v.columns).toBe(40);
181+
});
182+
});

0 commit comments

Comments
 (0)