feat(terminal): add runtime renderer switch
This commit is contained in:
@@ -38,5 +38,11 @@ instead:
|
||||
npm run terminal:bench -- --url http://127.0.0.1:3100/rustui/terminal --browser-executable /usr/bin/google-chrome
|
||||
```
|
||||
|
||||
Compare renderer modes with:
|
||||
|
||||
```bash
|
||||
npm run terminal:bench -- --url http://127.0.0.1:3100/rustui/terminal --renderer canvas --browser-executable /usr/bin/google-chrome
|
||||
```
|
||||
|
||||
It writes `target/terminal-bench/terminal-bench.json` plus screenshots when
|
||||
Playwright is available in the workspace.
|
||||
|
||||
@@ -241,7 +241,10 @@
|
||||
- 固定覆盖 `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** ✅
|
||||
- `/rustui/terminal` 页面提供 WebGL / Canvas / DOM runtime 切换。
|
||||
- 支持 `?renderer=webgl|canvas|dom` 初始化,便于浏览器回归和 benchmark 固定模式。
|
||||
- `scripts/terminal-bench.mjs --renderer webgl|canvas|dom` 复用同一入口跑对比,并将截图按 renderer 命名。
|
||||
4. 与 Canvas 2D 做 benchmark 对比,按 viewport size / output rate 自动选择 renderer。
|
||||
|
||||
---
|
||||
|
||||
@@ -1,22 +1,91 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
use crate::components::layout::{AdminShell, Breadcrumb};
|
||||
use crate::terminal::{TerminalPanel, TerminalRenderEvent};
|
||||
use crate::terminal::{Renderer, TerminalOptions, TerminalPanel, TerminalRenderEvent};
|
||||
|
||||
#[component]
|
||||
pub fn TerminalPage() -> impl IntoView {
|
||||
let renderer = RwSignal::new(initial_terminal_renderer());
|
||||
let terminal_options = Signal::derive(move || TerminalOptions {
|
||||
renderer: renderer.get(),
|
||||
..TerminalOptions::default()
|
||||
});
|
||||
let on_render = Callback::new(update_terminal_bench_probe);
|
||||
|
||||
view! {
|
||||
<AdminShell>
|
||||
<Breadcrumb current="浏览器终端" />
|
||||
<section class="cowa-work-surface terminal-page-shell">
|
||||
<TerminalPanel on_render=on_render />
|
||||
<TerminalRendererSwitch renderer=renderer />
|
||||
<TerminalPanel options=terminal_options on_render=on_render />
|
||||
</section>
|
||||
</AdminShell>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TerminalRendererSwitch(renderer: RwSignal<Renderer>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="terminal-page-toolbar" role="toolbar" aria-label="终端渲染器">
|
||||
<span class="terminal-renderer-label">"Renderer"</span>
|
||||
<RendererButton renderer=renderer value=Renderer::WebGl label="WebGL" />
|
||||
<RendererButton renderer=renderer value=Renderer::Canvas label="Canvas" />
|
||||
<RendererButton renderer=renderer value=Renderer::Dom label="DOM" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn RendererButton(
|
||||
renderer: RwSignal<Renderer>,
|
||||
value: Renderer,
|
||||
label: &'static str,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<button
|
||||
type="button"
|
||||
class="terminal-renderer-option"
|
||||
class=("is-active", move || renderer.get() == value)
|
||||
aria-pressed=move || (renderer.get() == value).to_string()
|
||||
on:click=move |_| renderer.set(value)
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn initial_terminal_renderer() -> Renderer {
|
||||
web_sys::window()
|
||||
.and_then(|window| window.location().search().ok())
|
||||
.and_then(|query| renderer_from_query(&query))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn initial_terminal_renderer() -> Renderer {
|
||||
Renderer::default()
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn renderer_from_query(query: &str) -> Option<Renderer> {
|
||||
query
|
||||
.trim_start_matches('?')
|
||||
.split('&')
|
||||
.filter_map(|part| part.split_once('='))
|
||||
.find_map(|(key, value)| (key == "renderer").then(|| parse_renderer(value)).flatten())
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn parse_renderer(value: &str) -> Option<Renderer> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"webgl" | "web-gl" => Some(Renderer::WebGl),
|
||||
"canvas" | "canvas2d" | "canvas-2d" => Some(Renderer::Canvas),
|
||||
"dom" => Some(Renderer::Dom),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn update_terminal_bench_probe(event: TerminalRenderEvent) {
|
||||
use wasm_bindgen::JsValue;
|
||||
@@ -87,3 +156,28 @@ fn update_terminal_bench_probe(event: TerminalRenderEvent) {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn update_terminal_bench_probe(_event: TerminalRenderEvent) {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn renderer_query_parses_supported_values() {
|
||||
assert_eq!(
|
||||
renderer_from_query("?renderer=webgl"),
|
||||
Some(Renderer::WebGl)
|
||||
);
|
||||
assert_eq!(
|
||||
renderer_from_query("?foo=1&renderer=canvas2d"),
|
||||
Some(Renderer::Canvas)
|
||||
);
|
||||
assert_eq!(renderer_from_query("?renderer=dom"), Some(Renderer::Dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renderer_query_ignores_unknown_values() {
|
||||
assert_eq!(renderer_from_query("?renderer=auto"), None);
|
||||
assert_eq!(renderer_from_query("?other=webgl"), None);
|
||||
assert_eq!(renderer_from_query(""), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export function parseArgs(argv) {
|
||||
screenshot: true,
|
||||
browserChannel: process.env.PLAYWRIGHT_BROWSER_CHANNEL || "",
|
||||
browserExecutable: process.env.PLAYWRIGHT_BROWSER_EXECUTABLE || "",
|
||||
renderer: "webgl",
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
@@ -50,6 +51,8 @@ export function parseArgs(argv) {
|
||||
options.browserChannel = next();
|
||||
} else if (arg === "--browser-executable") {
|
||||
options.browserExecutable = next();
|
||||
} else if (arg === "--renderer") {
|
||||
options.renderer = parseRenderer(next());
|
||||
} else if (arg === "--no-screenshot") {
|
||||
options.screenshot = false;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
@@ -173,11 +176,18 @@ async function runScenario(browser, options, viewport) {
|
||||
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])", {
|
||||
await page.goto(benchUrl(options), {
|
||||
waitUntil: "networkidle",
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
await page.waitForSelector(".terminal-screen", { timeout: options.timeoutMs });
|
||||
if (options.renderer === "dom") {
|
||||
await page.waitForSelector(".terminal-row", { timeout: options.timeoutMs });
|
||||
} else {
|
||||
await page.waitForSelector("canvas.terminal-canvas:not([hidden])", {
|
||||
timeout: options.timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.__terminalBenchFrames = [];
|
||||
@@ -236,7 +246,10 @@ async function runScenario(browser, options, viewport) {
|
||||
const screenshot = await page.screenshot({ fullPage: false });
|
||||
const screenshotSmoke = pngSmokeFromBuffer(screenshot);
|
||||
if (options.screenshot) {
|
||||
screenshotPath = path.join(options.outputDir, `terminal-${viewport.name}.png`);
|
||||
screenshotPath = path.join(
|
||||
options.outputDir,
|
||||
`terminal-${options.renderer}-${viewport.name}.png`,
|
||||
);
|
||||
fs.writeFileSync(screenshotPath, screenshot);
|
||||
}
|
||||
|
||||
@@ -256,6 +269,7 @@ async function runScenario(browser, options, viewport) {
|
||||
|
||||
return {
|
||||
viewport,
|
||||
requestedRenderer: options.renderer,
|
||||
lines: options.lines,
|
||||
elapsedMs,
|
||||
rowsPerSecond: round(options.lines / (elapsedMs / 1000)),
|
||||
@@ -269,6 +283,12 @@ async function runScenario(browser, options, viewport) {
|
||||
}
|
||||
}
|
||||
|
||||
function benchUrl(options) {
|
||||
const url = new URL(options.url);
|
||||
url.searchParams.set("renderer", options.renderer);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function browserLaunchOptions(options) {
|
||||
if (options.browserChannel) {
|
||||
return { channel: options.browserChannel };
|
||||
@@ -393,6 +413,14 @@ function positiveInt(value, label) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseRenderer(value) {
|
||||
const normalized = value.toLowerCase();
|
||||
if (["webgl", "canvas", "dom"].includes(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
throw new Error(`--renderer must be one of: webgl, canvas, dom`);
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
@@ -409,6 +437,7 @@ Options:
|
||||
--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
|
||||
--renderer <name> webgl, canvas, or dom. Default: webgl
|
||||
--browser-channel <name> Use an installed browser channel, e.g. chrome
|
||||
--browser-executable <path> Use a local Chromium/Chrome executable path
|
||||
--timeout-ms <n> Default: 120000
|
||||
|
||||
@@ -538,6 +538,38 @@
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.terminal-page-toolbar {
|
||||
@apply mb-3 flex shrink-0 items-center gap-1;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.terminal-renderer-label {
|
||||
color: var(--cowa-text-tertiary);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.terminal-renderer-option {
|
||||
@apply h-8 rounded border bg-white px-3;
|
||||
border-color: var(--cowa-border);
|
||||
color: var(--cowa-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.terminal-renderer-option:hover {
|
||||
border-color: var(--cowa-primary);
|
||||
color: var(--cowa-primary);
|
||||
}
|
||||
|
||||
.terminal-renderer-option.is-active {
|
||||
border-color: var(--cowa-primary);
|
||||
background: var(--cowa-primary-soft);
|
||||
color: var(--cowa-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.terminal-shell-card {
|
||||
@apply flex flex-1 flex-col gap-4 rounded border bg-white;
|
||||
border-color: var(--cowa-border);
|
||||
|
||||
Reference in New Issue
Block a user