[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

✅ 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.

Taras Mankovski (Apr 11, 2026, 6:30 AM EDT) fbc05cd1 c35b5e2c

+446
+182
virtualizer/test/bench.test.ts
··· 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 + });
+264
virtualizer/test/property.test.ts
··· 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 + // Simple deterministic PRNG for reproducibility 13 + function mulberry32(seed: number) { 14 + return () => { 15 + seed |= 0; 16 + seed = (seed + 0x6d2b79f5) | 0; 17 + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); 18 + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; 19 + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; 20 + }; 21 + } 22 + 23 + function randomLine(rng: () => number): string { 24 + let kind = rng(); 25 + if (kind < 0.3) { 26 + // ASCII 27 + let len = Math.floor(rng() * 120); 28 + return Array.from({ length: len }, () => 29 + String.fromCharCode(32 + Math.floor(rng() * 95)) 30 + ).join(""); 31 + } else if (kind < 0.5) { 32 + // CJK 33 + let len = Math.floor(rng() * 40); 34 + return Array.from({ length: len }, () => 35 + String.fromCharCode(0x4e00 + Math.floor(rng() * 0x5200)) 36 + ).join(""); 37 + } else if (kind < 0.7) { 38 + // Mixed with ANSI 39 + let parts: string[] = []; 40 + let len = Math.floor(rng() * 50); 41 + for (let i = 0; i < len; i++) { 42 + if (rng() < 0.2) { 43 + parts.push(`\x1b[${Math.floor(rng() * 37 + 1)}m`); 44 + } else { 45 + parts.push(String.fromCharCode(32 + Math.floor(rng() * 95))); 46 + } 47 + } 48 + return parts.join(""); 49 + } else { 50 + // Empty or short 51 + return "x".repeat(Math.floor(rng() * 5)); 52 + } 53 + } 54 + 55 + function assertViewportInvariants(v: Virtualizer) { 56 + let vp = v.resolveViewport(); 57 + 58 + // O-1: entries ordered 59 + for (let i = 1; i < vp.entries.length; i++) { 60 + expect(vp.entries[i].lineIndex).toBeGreaterThan(vp.entries[i - 1].lineIndex); 61 + } 62 + 63 + let totalVisible = 0; 64 + for (let entry of vp.entries) { 65 + // O-3: wrapPoints monotonic and in range 66 + for (let j = 0; j < entry.wrapPoints.length; j++) { 67 + expect(entry.wrapPoints[j]).toBeGreaterThan(0); 68 + expect(entry.wrapPoints[j]).toBeLessThan(entry.text.length); 69 + if (j > 0) expect(entry.wrapPoints[j]).toBeGreaterThan(entry.wrapPoints[j - 1]); 70 + } 71 + 72 + // O-4: not in surrogate 73 + for (let wp of entry.wrapPoints) { 74 + if (wp > 0) { 75 + let prev = entry.text.charCodeAt(wp - 1); 76 + expect(prev >= 0xd800 && prev <= 0xdbff).toBe(false); 77 + } 78 + } 79 + 80 + // O-5: not in escape 81 + for (let wp of entry.wrapPoints) { 82 + let i = 0; 83 + while (i < entry.text.length) { 84 + let skip = skipAnsiSequence(entry.text, i); 85 + if (skip > 0) { 86 + expect(wp <= i || wp >= i + skip).toBe(true); 87 + i += skip; 88 + } else { 89 + i++; 90 + } 91 + } 92 + } 93 + 94 + // O-6: totalSubRows 95 + expect(entry.totalSubRows).toBe(entry.wrapPoints.length + 1); 96 + 97 + // O-7: subrow bounds 98 + expect(entry.firstSubRow).toBeGreaterThanOrEqual(0); 99 + expect(entry.visibleSubRows).toBeGreaterThanOrEqual(0); 100 + expect(entry.firstSubRow + entry.visibleSubRows).toBeLessThanOrEqual(entry.totalSubRows); 101 + 102 + totalVisible += entry.visibleSubRows; 103 + } 104 + 105 + // O-8: visible rows within budget 106 + expect(totalVisible).toBeLessThanOrEqual(v.rows); 107 + } 108 + 109 + describe("Property-based tests", () => { 110 + it("P.IDENTITY.never-reused — no two returned lineIndex values are equal", () => { 111 + let rng = mulberry32(42); 112 + let v = new Virtualizer({ measureWidth: charMeasure, columns: 40, rows: 10, maxLines: 20 }); 113 + let indices = new Set<number>(); 114 + for (let i = 0; i < 100; i++) { 115 + let idx = v.appendLine(randomLine(rng)); 116 + expect(indices.has(idx)).toBe(false); 117 + indices.add(idx); 118 + } 119 + }); 120 + 121 + it("P.IDENTITY.monotonic — later lineIndex > earlier lineIndex", () => { 122 + let rng = mulberry32(123); 123 + let v = new Virtualizer({ measureWidth: charMeasure, columns: 40, rows: 10, maxLines: 50 }); 124 + let prev = -1; 125 + for (let i = 0; i < 100; i++) { 126 + let idx = v.appendLine(randomLine(rng)); 127 + expect(idx).toBeGreaterThan(prev); 128 + prev = idx; 129 + } 130 + }); 131 + 132 + it("P.IDENTITY.survives-eviction — surviving lines' lineIndex unchanged", () => { 133 + let rng = mulberry32(456); 134 + let v = new Virtualizer({ measureWidth: charMeasure, columns: 40, rows: 10, maxLines: 10 }); 135 + let assigned = new Map<number, string>(); 136 + for (let i = 0; i < 50; i++) { 137 + let text = randomLine(rng); 138 + let idx = v.appendLine(text); 139 + assigned.set(idx, text); 140 + } 141 + // Verify surviving lines 142 + let vp = v.resolveViewport(); 143 + for (let entry of vp.entries) { 144 + expect(assigned.get(entry.lineIndex)).toBe(entry.text); 145 + } 146 + }); 147 + 148 + it("P.APPEND.scroll-position-independent — totalEstimatedVisualRows increment same regardless of scroll", () => { 149 + let text = "hello world test line"; 150 + let v1 = new Virtualizer({ measureWidth: charMeasure, columns: 20, rows: 10 }); 151 + let v2 = new Virtualizer({ measureWidth: charMeasure, columns: 20, rows: 10 }); 152 + 153 + for (let i = 0; i < 20; i++) { 154 + v1.appendLine(`line ${i}`); 155 + v2.appendLine(`line ${i}`); 156 + } 157 + v2.scrollBy(-10); // different scroll position 158 + 159 + let before1 = v1.totalEstimatedVisualRows; 160 + let before2 = v2.totalEstimatedVisualRows; 161 + v1.appendLine(text); 162 + v2.appendLine(text); 163 + expect(v1.totalEstimatedVisualRows - before1).toBe(v2.totalEstimatedVisualRows - before2); 164 + }); 165 + 166 + it("P.VIEWPORT.all-invariants — O-1 through O-9 hold after random ops", () => { 167 + let rng = mulberry32(789); 168 + for (let trial = 0; trial < 5; trial++) { 169 + let columns = 10 + Math.floor(rng() * 70); 170 + let v = new Virtualizer({ measureWidth: charMeasure, columns, rows: 10, maxLines: 30 }); 171 + 172 + for (let i = 0; i < 50; i++) { 173 + v.appendLine(randomLine(rng)); 174 + } 175 + 176 + // Random scroll 177 + let delta = Math.floor(rng() * 40) - 20; 178 + v.scrollBy(delta); 179 + 180 + assertViewportInvariants(v); 181 + } 182 + }); 183 + 184 + it("P.EVICT.viewport-stable — if anchor survives, viewport identical before/after eviction", () => { 185 + let v = new Virtualizer({ measureWidth: charMeasure, columns: 40, rows: 3, maxLines: 10 }); 186 + for (let i = 0; i < 10; i++) v.appendLine(`line ${i}`); 187 + // Scroll to middle so anchor is far from eviction frontier 188 + v.scrollBy(-3); 189 + let anchorBefore = v.anchorLineIndex; 190 + let vp1 = v.resolveViewport(); 191 + let entries1 = vp1.entries.map((e) => e.lineIndex); 192 + 193 + v.appendLine("new line"); // evicts oldest 194 + // Anchor should survive (it's in the middle) 195 + if (v.anchorLineIndex === anchorBefore) { 196 + let vp2 = v.resolveViewport(); 197 + let entries2 = vp2.entries.map((e) => e.lineIndex); 198 + expect(entries2).toEqual(entries1); 199 + } 200 + }); 201 + 202 + it("P.ESTIMATE.constraints-hold — estimation constraints hold after every op", () => { 203 + let rng = mulberry32(999); 204 + let v = new Virtualizer({ measureWidth: charMeasure, columns: 20, rows: 10, maxLines: 30 }); 205 + 206 + for (let i = 0; i < 60; i++) { 207 + v.appendLine(randomLine(rng)); 208 + expect(v.totalEstimatedVisualRows).toBeGreaterThanOrEqual(0); 209 + if (v.lineCount > 0) { 210 + expect(v.totalEstimatedVisualRows).toBeGreaterThan(0); 211 + expect(v.currentEstimatedVisualRow).toBeGreaterThanOrEqual(0); 212 + expect(v.currentEstimatedVisualRow).toBeLessThan(v.totalEstimatedVisualRows); 213 + } 214 + } 215 + 216 + // After scroll 217 + v.scrollBy(-15); 218 + expect(v.currentEstimatedVisualRow).toBeGreaterThanOrEqual(0); 219 + expect(v.currentEstimatedVisualRow).toBeLessThan(v.totalEstimatedVisualRows); 220 + 221 + // After resize 222 + v.resize(15, 10); 223 + expect(v.totalEstimatedVisualRows).toBeGreaterThan(0); 224 + expect(v.currentEstimatedVisualRow).toBeGreaterThanOrEqual(0); 225 + expect(v.currentEstimatedVisualRow).toBeLessThan(v.totalEstimatedVisualRows); 226 + }); 227 + 228 + it("P.ESTIMATE.monotonicity-under-mismatch — scrollBy maintains monotonicity with wide chars", () => { 229 + let rng = mulberry32(111); 230 + let v = new Virtualizer({ measureWidth: charMeasure, columns: 5, rows: 24 }); 231 + for (let i = 0; i < 30; i++) { 232 + v.appendLine(randomLine(rng)); 233 + } 234 + v.scrollBy(-50); // scroll near top 235 + 236 + let prev = v.currentEstimatedVisualRow; 237 + for (let i = 0; i < 15; i++) { 238 + v.scrollBy(1); 239 + expect(v.currentEstimatedVisualRow).toBeGreaterThanOrEqual(prev); 240 + prev = v.currentEstimatedVisualRow; 241 + } 242 + 243 + prev = v.currentEstimatedVisualRow; 244 + for (let i = 0; i < 15; i++) { 245 + v.scrollBy(-1); 246 + expect(v.currentEstimatedVisualRow).toBeLessThanOrEqual(prev); 247 + prev = v.currentEstimatedVisualRow; 248 + } 249 + }); 250 + 251 + it("P.RESIZE.viewport-invariants-hold — all invariants hold after random resize", () => { 252 + let rng = mulberry32(222); 253 + let v = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 24 }); 254 + for (let i = 0; i < 30; i++) { 255 + v.appendLine(randomLine(rng)); 256 + } 257 + 258 + let widths = [20, 40, 60, 80, 120, 10]; 259 + for (let w of widths) { 260 + v.resize(w, 24); 261 + assertViewportInvariants(v); 262 + } 263 + }); 264 + });