test(terminal): add renderer bench smoke script
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -39,6 +39,7 @@ dependencies = [
|
|||||||
"console_log",
|
"console_log",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"icons",
|
"icons",
|
||||||
|
"js-sys",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -15,3 +15,22 @@ Then open:
|
|||||||
- `http://127.0.0.1:3100/rustui/deep/nested`
|
- `http://127.0.0.1:3100/rustui/deep/nested`
|
||||||
|
|
||||||
Change `app_config/src/lib.rs` to test another base path.
|
Change `app_config/src/lib.rs` to test another base path.
|
||||||
|
|
||||||
|
## Terminal Renderer Bench
|
||||||
|
|
||||||
|
The terminal benchmark script connects to an already-running dev server; it does
|
||||||
|
not start or stop services.
|
||||||
|
|
||||||
|
Install the Node dependency and Chromium browser once before running it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npx playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run terminal:bench -- --url http://127.0.0.1:3100/rustui/terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
It writes `target/terminal-bench/terminal-bench.json` plus screenshots when
|
||||||
|
Playwright is available in the workspace.
|
||||||
|
|||||||
@@ -236,7 +236,11 @@
|
|||||||
- context lost 或 WebGL 初始化失败时显示独立 Canvas 2D fallback surface,避免同一 canvas 上 WebGL lost 后无法获取 2D context 导致白屏。
|
- context lost 或 WebGL 初始化失败时显示独立 Canvas 2D fallback surface,避免同一 canvas 上 WebGL lost 后无法获取 2D context 导致白屏。
|
||||||
- restore 后清空旧 state 并触发重绘,下一帧重建 shader/program/buffer/texture/glyph atlas。
|
- restore 后清空旧 state 并触发重绘,下一帧重建 shader/program/buffer/texture/glyph atlas。
|
||||||
- context lifecycle listener 由 `WebGlContextLifecycle` 持有,组件卸载或 renderer 切换时自动移除。
|
- context lifecycle listener 由 `WebGlContextLifecycle` 持有,组件卸载或 renderer 切换时自动移除。
|
||||||
2. 渲染性能 benchmark 和自动截图 / pixel smoke test。
|
2. **渲染性能 benchmark 和自动截图 / pixel smoke test** ✅
|
||||||
|
- `scripts/terminal-bench.mjs` 连接已启动的 `/rustui/terminal`,不负责启动/关闭服务。
|
||||||
|
- 固定覆盖 `200x80`、`240x100` viewport,支持 `--lines`、`--viewport`、`--output` 参数。
|
||||||
|
- 记录行输出耗时、rows/sec、requestAnimationFrame frame time 摘要、`window.__terminalBench` render probe。
|
||||||
|
- 截图 smoke 默认走 Playwright screenshot PNG 非空/熵检查;Canvas 2D fallback 可读时额外做像素采样。
|
||||||
3. Canvas 2D / WebGL runtime switch。
|
3. Canvas 2D / WebGL runtime switch。
|
||||||
4. 与 Canvas 2D 做 benchmark 对比,按 viewport size / output rate 自动选择 renderer。
|
4. 与 Canvas 2D 做 benchmark 对比,按 viewport size / output rate 自动选择 renderer。
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ leptos.workspace = true
|
|||||||
leptos_axum = { workspace = true, optional = true }
|
leptos_axum = { workspace = true, optional = true }
|
||||||
leptos_meta.workspace = true
|
leptos_meta.workspace = true
|
||||||
leptos_router.workspace = true
|
leptos_router.workspace = true
|
||||||
|
js-sys.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
redis = { workspace = true, optional = true }
|
redis = { workspace = true, optional = true }
|
||||||
reqwest = { workspace = true, optional = true }
|
reqwest = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -1,16 +1,89 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
use crate::components::layout::{AdminShell, Breadcrumb};
|
use crate::components::layout::{AdminShell, Breadcrumb};
|
||||||
use crate::terminal::TerminalPanel;
|
use crate::terminal::{TerminalPanel, TerminalRenderEvent};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TerminalPage() -> impl IntoView {
|
pub fn TerminalPage() -> impl IntoView {
|
||||||
|
let on_render = Callback::new(update_terminal_bench_probe);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<Breadcrumb current="浏览器终端" />
|
<Breadcrumb current="浏览器终端" />
|
||||||
<section class="cowa-work-surface terminal-page-shell">
|
<section class="cowa-work-surface terminal-page-shell">
|
||||||
<TerminalPanel />
|
<TerminalPanel on_render=on_render />
|
||||||
</section>
|
</section>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn update_terminal_bench_probe(event: TerminalRenderEvent) {
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
|
let Some(window) = web_sys::window() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(probe) = js_sys::Reflect::get(&window, &JsValue::from_str("__terminalBench")) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let probe = if probe.is_object() {
|
||||||
|
probe
|
||||||
|
} else {
|
||||||
|
let object = js_sys::Object::new();
|
||||||
|
let _ = js_sys::Reflect::set(&window, &JsValue::from_str("__terminalBench"), &object);
|
||||||
|
object.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let count = js_sys::Reflect::get(&probe, &JsValue::from_str("renderCount"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.as_f64())
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
+ 1.0;
|
||||||
|
let mode = if event.mode.is_live() {
|
||||||
|
"live"
|
||||||
|
} else {
|
||||||
|
"history"
|
||||||
|
};
|
||||||
|
let renderer = format!("{:?}", event.renderer);
|
||||||
|
|
||||||
|
let _ = js_sys::Reflect::set(
|
||||||
|
&probe,
|
||||||
|
&JsValue::from_str("renderCount"),
|
||||||
|
&JsValue::from_f64(count),
|
||||||
|
);
|
||||||
|
let _ = js_sys::Reflect::set(
|
||||||
|
&probe,
|
||||||
|
&JsValue::from_str("lastRenderAt"),
|
||||||
|
&JsValue::from_f64(js_sys::Date::now()),
|
||||||
|
);
|
||||||
|
let _ = js_sys::Reflect::set(
|
||||||
|
&probe,
|
||||||
|
&JsValue::from_str("renderer"),
|
||||||
|
&JsValue::from_str(&renderer),
|
||||||
|
);
|
||||||
|
let _ = js_sys::Reflect::set(&probe, &JsValue::from_str("mode"), &JsValue::from_str(mode));
|
||||||
|
let _ = js_sys::Reflect::set(
|
||||||
|
&probe,
|
||||||
|
&JsValue::from_str("cols"),
|
||||||
|
&JsValue::from_f64(f64::from(event.cols)),
|
||||||
|
);
|
||||||
|
let _ = js_sys::Reflect::set(
|
||||||
|
&probe,
|
||||||
|
&JsValue::from_str("rows"),
|
||||||
|
&JsValue::from_f64(f64::from(event.rows)),
|
||||||
|
);
|
||||||
|
let _ = js_sys::Reflect::set(
|
||||||
|
&probe,
|
||||||
|
&JsValue::from_str("historyRows"),
|
||||||
|
&JsValue::from_f64(event.history_rows as f64),
|
||||||
|
);
|
||||||
|
let _ = js_sys::Reflect::set(
|
||||||
|
&probe,
|
||||||
|
&JsValue::from_str("screenRows"),
|
||||||
|
&JsValue::from_f64(event.screen_rows as f64),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn update_terminal_bench_probe(_event: TerminalRenderEvent) {}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"terminal:bench": "node scripts/terminal-bench.mjs"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/cli": "4.3.0",
|
"@tailwindcss/cli": "4.3.0",
|
||||||
|
"playwright": "1.61.1",
|
||||||
"tailwindcss": "4.3.0"
|
"tailwindcss": "4.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
360
scripts/terminal-bench.mjs
Normal file
360
scripts/terminal-bench.mjs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import http from "node:http";
|
||||||
|
import path from "node:path";
|
||||||
|
import { performance } from "node:perf_hooks";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const DEFAULT_URL = "http://127.0.0.1:3100/rustui/terminal";
|
||||||
|
const DEFAULT_OUTPUT = "target/terminal-bench";
|
||||||
|
const DEFAULT_LINES = 10000;
|
||||||
|
const DEFAULT_VIEWPORTS = [
|
||||||
|
{ name: "200x80", width: 1760, height: 1600 },
|
||||||
|
{ name: "240x100", width: 2080, height: 1960 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function parseArgs(argv) {
|
||||||
|
const options = {
|
||||||
|
url: DEFAULT_URL,
|
||||||
|
outputDir: DEFAULT_OUTPUT,
|
||||||
|
lines: DEFAULT_LINES,
|
||||||
|
viewports: [...DEFAULT_VIEWPORTS],
|
||||||
|
timeoutMs: 120000,
|
||||||
|
screenshot: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
const next = () => {
|
||||||
|
index += 1;
|
||||||
|
if (index >= argv.length) {
|
||||||
|
throw new Error(`Missing value for ${arg}`);
|
||||||
|
}
|
||||||
|
return argv[index];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (arg === "--url") {
|
||||||
|
options.url = next();
|
||||||
|
} else if (arg === "--output") {
|
||||||
|
options.outputDir = next();
|
||||||
|
} else if (arg === "--lines") {
|
||||||
|
options.lines = positiveInt(next(), "--lines");
|
||||||
|
} else if (arg === "--timeout-ms") {
|
||||||
|
options.timeoutMs = positiveInt(next(), "--timeout-ms");
|
||||||
|
} else if (arg === "--viewport") {
|
||||||
|
options.viewports = [parseViewport(next())];
|
||||||
|
} else if (arg === "--no-screenshot") {
|
||||||
|
options.screenshot = false;
|
||||||
|
} else if (arg === "--help" || arg === "-h") {
|
||||||
|
options.help = true;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseViewport(value) {
|
||||||
|
const match = /^(\d+)x(\d+)(?::(\d+)x(\d+))?$/.exec(value);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid viewport "${value}". Use colsxrows or colsxrows:widthxheight.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const cols = Number(match[1]);
|
||||||
|
const rows = Number(match[2]);
|
||||||
|
return {
|
||||||
|
name: `${cols}x${rows}`,
|
||||||
|
width: match[3] ? Number(match[3]) : Math.max(960, cols * 9 + 64),
|
||||||
|
height: match[4] ? Number(match[4]) : Math.max(720, rows * 19 + 160),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeSamples(samples) {
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return { count: 0, avg: 0, p95: 0, max: 0 };
|
||||||
|
}
|
||||||
|
const sorted = [...samples].sort((a, b) => a - b);
|
||||||
|
const sum = sorted.reduce((acc, value) => acc + value, 0);
|
||||||
|
const p95Index = Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1);
|
||||||
|
return {
|
||||||
|
count: sorted.length,
|
||||||
|
avg: round(sum / sorted.length),
|
||||||
|
p95: round(sorted[p95Index]),
|
||||||
|
max: round(sorted[sorted.length - 1]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pixelSmokeFromSamples(samples, width, height) {
|
||||||
|
const total = Math.max(1, width * height);
|
||||||
|
let nonTransparent = 0;
|
||||||
|
let nonBlack = 0;
|
||||||
|
for (let index = 0; index < samples.length; index += 4) {
|
||||||
|
const r = samples[index];
|
||||||
|
const g = samples[index + 1];
|
||||||
|
const b = samples[index + 2];
|
||||||
|
const a = samples[index + 3];
|
||||||
|
if (a > 0) {
|
||||||
|
nonTransparent += 1;
|
||||||
|
}
|
||||||
|
if (a > 0 && (r > 8 || g > 8 || b > 8)) {
|
||||||
|
nonBlack += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
nonTransparent,
|
||||||
|
nonBlack,
|
||||||
|
nonTransparentRatio: round(nonTransparent / total),
|
||||||
|
nonBlackRatio: round(nonBlack / total),
|
||||||
|
passed: nonTransparent > 0 && nonBlack > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildReport(results) {
|
||||||
|
const generatedAt = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
generatedAt,
|
||||||
|
scenarios: results,
|
||||||
|
passed: results.every((result) => result.pixelSmoke.passed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const options = parseArgs(process.argv.slice(2));
|
||||||
|
if (options.help) {
|
||||||
|
printHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertServerReachable(options.url, options.timeoutMs);
|
||||||
|
const { chromium } = await importPlaywright();
|
||||||
|
fs.mkdirSync(options.outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
for (const viewport of options.viewports) {
|
||||||
|
results.push(await runScenario(browser, options, viewport));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = buildReport(results);
|
||||||
|
const reportPath = path.join(options.outputDir, "terminal-bench.json");
|
||||||
|
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||||
|
console.log(JSON.stringify(report, null, 2));
|
||||||
|
if (!report.passed) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runScenario(browser, options, viewport) {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: viewport.width, height: viewport.height },
|
||||||
|
deviceScaleFactor: 1,
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
const frameSamples = [];
|
||||||
|
try {
|
||||||
|
await page.goto(options.url, { waitUntil: "networkidle", timeout: options.timeoutMs });
|
||||||
|
await page.waitForSelector(".terminal-screen", { timeout: options.timeoutMs });
|
||||||
|
await page.waitForSelector("canvas.terminal-canvas:not([hidden])", {
|
||||||
|
timeout: options.timeoutMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__terminalBenchFrames = [];
|
||||||
|
let previous = performance.now();
|
||||||
|
function sample(now) {
|
||||||
|
window.__terminalBenchFrames.push(now - previous);
|
||||||
|
previous = now;
|
||||||
|
if (window.__terminalBenchFrames.length < 360) {
|
||||||
|
requestAnimationFrame(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(sample);
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = `for i in $(seq 1 ${options.lines}); do printf 'bench-%05d abcdefghijklmnopqrstuvwxyz\\\\n' "$i"; done`;
|
||||||
|
const startedAt = performance.now();
|
||||||
|
await page.locator(".terminal-screen").click();
|
||||||
|
await page.keyboard.type(command);
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
await waitForBenchLine(page, options.lines, options.timeoutMs);
|
||||||
|
const elapsedMs = round(performance.now() - startedAt);
|
||||||
|
|
||||||
|
const probe = await page.evaluate(() => window.__terminalBench || {});
|
||||||
|
frameSamples.push(
|
||||||
|
...(await page.evaluate(() => window.__terminalBenchFrames || [])),
|
||||||
|
);
|
||||||
|
const canvasPixelSmoke = await page.evaluate(() => {
|
||||||
|
const canvas = document.querySelector("canvas.terminal-canvas:not([hidden])");
|
||||||
|
if (!canvas) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
|
if (!context) {
|
||||||
|
return {
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height,
|
||||||
|
samples: [],
|
||||||
|
readable: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const width = Math.min(canvas.width, 320);
|
||||||
|
const height = Math.min(canvas.height, 180);
|
||||||
|
const data = context.getImageData(0, 0, width, height).data;
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
samples: Array.from(data),
|
||||||
|
readable: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let screenshotPath = null;
|
||||||
|
const screenshot = await page.screenshot({ fullPage: false });
|
||||||
|
const screenshotSmoke = pngSmokeFromBuffer(screenshot);
|
||||||
|
if (options.screenshot) {
|
||||||
|
screenshotPath = path.join(options.outputDir, `terminal-${viewport.name}.png`);
|
||||||
|
fs.writeFileSync(screenshotPath, screenshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pixelResult = canvasPixelSmoke?.readable
|
||||||
|
? {
|
||||||
|
source: "canvas",
|
||||||
|
...pixelSmokeFromSamples(
|
||||||
|
canvasPixelSmoke.samples,
|
||||||
|
canvasPixelSmoke.width,
|
||||||
|
canvasPixelSmoke.height,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
source: "screenshot",
|
||||||
|
...screenshotSmoke,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewport,
|
||||||
|
lines: options.lines,
|
||||||
|
elapsedMs,
|
||||||
|
rowsPerSecond: round(options.lines / (elapsedMs / 1000)),
|
||||||
|
frameMs: summarizeSamples(frameSamples),
|
||||||
|
probe,
|
||||||
|
pixelSmoke: pixelResult,
|
||||||
|
screenshotPath,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pngSmokeFromBuffer(buffer) {
|
||||||
|
const signature = buffer.subarray(0, 8).toString("hex");
|
||||||
|
const isPng = signature === "89504e470d0a1a0a";
|
||||||
|
const distinctBytes = new Set(buffer).size;
|
||||||
|
return {
|
||||||
|
total: buffer.length,
|
||||||
|
nonTransparent: 0,
|
||||||
|
nonBlack: 0,
|
||||||
|
nonTransparentRatio: 0,
|
||||||
|
nonBlackRatio: 0,
|
||||||
|
distinctBytes,
|
||||||
|
passed: isPng && buffer.length > 1024 && distinctBytes > 16,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForBenchLine(page, lines, timeoutMs) {
|
||||||
|
const marker = `bench-${String(lines).padStart(5, "0")}`;
|
||||||
|
const minHistoryRows = Math.max(1, lines - 200);
|
||||||
|
await page.waitForFunction(
|
||||||
|
({ text, minHistoryRows }) => {
|
||||||
|
const probe = window.__terminalBench || {};
|
||||||
|
return (
|
||||||
|
document.body.innerText.includes(text) ||
|
||||||
|
(probe.historyRows || 0) >= minHistoryRows
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ text: marker, minHistoryRows },
|
||||||
|
{ timeout: timeoutMs },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importPlaywright() {
|
||||||
|
try {
|
||||||
|
return await import("playwright");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
"Playwright is required for terminal bench. Install it in this workspace, then rerun the script.",
|
||||||
|
{ cause: error },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertServerReachable(url, timeoutMs) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const request = http.get(parsed, (response) => {
|
||||||
|
response.resume();
|
||||||
|
if (response.statusCode && response.statusCode < 500) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Server returned HTTP ${response.statusCode}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request.setTimeout(Math.min(timeoutMs, 5000), () => {
|
||||||
|
request.destroy(new Error(`Timed out connecting to ${url}`));
|
||||||
|
});
|
||||||
|
request.on("error", reject);
|
||||||
|
}).catch((error) => {
|
||||||
|
throw new Error(
|
||||||
|
`Terminal bench expects an already-running dev server at ${url}. Start it yourself, then rerun.`,
|
||||||
|
{ cause: error },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function positiveInt(value, label) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||||
|
throw new Error(`${label} must be a positive integer`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function round(value) {
|
||||||
|
return Math.round(value * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Usage: node scripts/terminal-bench.mjs [options]
|
||||||
|
|
||||||
|
Connects to an already-running BASE_PATH demo server and runs terminal renderer
|
||||||
|
performance + pixel smoke checks. The script does not start or stop services.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--url <url> Default: ${DEFAULT_URL}
|
||||||
|
--output <dir> Default: ${DEFAULT_OUTPUT}
|
||||||
|
--lines <n> Default: ${DEFAULT_LINES}
|
||||||
|
--viewport <cols>x<rows> Run one scenario with inferred browser size
|
||||||
|
--viewport <c>x<r>:<w>x<h> Run one scenario with explicit browser size
|
||||||
|
--timeout-ms <n> Default: 120000
|
||||||
|
--no-screenshot Skip screenshot capture
|
||||||
|
--help Show this help
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
||||||
|
if (isCli) {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error.message);
|
||||||
|
if (error.cause) {
|
||||||
|
console.error(`Cause: ${error.cause.message}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user