test(terminal): profile webgl render stages

This commit is contained in:
zhangheng
2026-06-24 17:46:54 +08:00
parent 7593561761
commit a039f7fd5f
7 changed files with 204 additions and 20 deletions

View File

@@ -63,6 +63,7 @@ web-sys = { version = "0.3", default-features = false, features = [
"MessageEvent", "MessageEvent",
"MouseEvent", "MouseEvent",
"Navigator", "Navigator",
"Performance",
"ResizeObserver", "ResizeObserver",
"ResizeObserverEntry", "ResizeObserverEntry",
"WebGl2RenderingContext", "WebGl2RenderingContext",

View File

@@ -251,6 +251,10 @@
- `240x100`WebGL `92.48 rows/sec`frame p95 `399.9ms`Canvas `157.65 rows/sec`frame p95 `116.8ms` - `240x100`WebGL `92.48 rows/sec`frame p95 `399.9ms`Canvas `157.65 rows/sec`frame p95 `116.8ms`
- 两种 renderer 的 pixel smoke 均通过Canvas 可直接读取 canvas 像素WebGL 走 screenshot smoke。 - 两种 renderer 的 pixel smoke 均通过Canvas 可直接读取 canvas 像素WebGL 走 screenshot smoke。
- 当前策略:默认 CanvasWebGL 继续通过页面切换、`?renderer=webgl``--renderer webgl` 保留优化回归入口。 - 当前策略:默认 CanvasWebGL 继续通过页面切换、`?renderer=webgl``--renderer webgl` 保留优化回归入口。
5. **WebGL renderer profiling / 慢路径拆解**
- WebGL render path 记录阶段耗时DOM/canvas 同步、backdrop 绘制、glyph collect、backdrop texture pass、glyph atlas、atlas upload、vertex build/upload、draw call。
- 记录计数cells、atlas entries、atlas insertions/resets、full-canvas fallback 次数、glyph draw failed 次数。
- `window.__terminalBench.webglFrames` 暴露每帧采样,`scripts/terminal-bench.mjs` 汇总为 `rendererStats`,用于判断慢点是整屏 backdrop texture、atlas、vertex buffer还是 fallback。
--- ---

View File

@@ -115,6 +115,7 @@ fn update_terminal_bench_probe(event: TerminalRenderEvent) {
"history" "history"
}; };
let renderer = format!("{:?}", event.renderer); let renderer = format!("{:?}", event.renderer);
let webgl_stats = event.webgl_stats;
let _ = js_sys::Reflect::set( let _ = js_sys::Reflect::set(
&probe, &probe,
@@ -152,6 +153,61 @@ fn update_terminal_bench_probe(event: TerminalRenderEvent) {
&JsValue::from_str("screenRows"), &JsValue::from_str("screenRows"),
&JsValue::from_f64(event.screen_rows as f64), &JsValue::from_f64(event.screen_rows as f64),
); );
if let Some(stats) = webgl_stats {
append_webgl_stats(&probe, stats);
}
}
#[cfg(target_arch = "wasm32")]
fn append_webgl_stats(probe: &wasm_bindgen::JsValue, stats: crate::terminal::WebGlRenderStats) {
use wasm_bindgen::JsValue;
let frames = match js_sys::Reflect::get(probe, &JsValue::from_str("webglFrames")) {
Ok(value) if js_sys::Array::is_array(&value) => js_sys::Array::from(&value),
_ => {
let array = js_sys::Array::new();
let _ = js_sys::Reflect::set(probe, &JsValue::from_str("webglFrames"), &array);
array
}
};
let frame = js_sys::Object::new();
set_f64(&frame, "totalMs", stats.total_ms);
set_f64(&frame, "syncDomMs", stats.sync_dom_ms);
set_f64(&frame, "backdropMs", stats.backdrop_ms);
set_f64(&frame, "collectMs", stats.collect_ms);
set_f64(&frame, "backdropTextureMs", stats.backdrop_texture_ms);
set_f64(&frame, "fallbackCanvasMs", stats.fallback_canvas_ms);
set_f64(&frame, "atlasMs", stats.atlas_ms);
set_f64(&frame, "atlasUploadMs", stats.atlas_upload_ms);
set_f64(&frame, "vertexBuildMs", stats.vertex_build_ms);
set_f64(&frame, "vertexUploadMs", stats.vertex_upload_ms);
set_f64(&frame, "drawMs", stats.draw_ms);
set_f64(&frame, "cells", stats.cells as f64);
set_f64(&frame, "atlasEntries", stats.atlas_entries as f64);
set_f64(&frame, "atlasInsertions", stats.atlas_insertions as f64);
set_f64(&frame, "atlasResets", stats.atlas_resets as f64);
set_bool(&frame, "fallbackFullCanvas", stats.fallback_full_canvas);
set_bool(&frame, "glyphDrawFailed", stats.glyph_draw_failed);
frames.push(&frame);
while frames.length() > 720 {
frames.shift();
}
}
#[cfg(target_arch = "wasm32")]
fn set_f64(target: &js_sys::Object, key: &str, value: f64) {
use wasm_bindgen::JsValue;
let _ = js_sys::Reflect::set(target, &JsValue::from_str(key), &JsValue::from_f64(value));
}
#[cfg(target_arch = "wasm32")]
fn set_bool(target: &js_sys::Object, key: &str, value: bool) {
use wasm_bindgen::JsValue;
let _ = js_sys::Reflect::set(target, &JsValue::from_str(key), &JsValue::from_bool(value));
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]

View File

@@ -28,7 +28,7 @@ use super::mouse::{
use super::protocol::{ClientTerminalMessage, ServerTerminalMessage}; use super::protocol::{ClientTerminalMessage, ServerTerminalMessage};
use super::renderer::{ use super::renderer::{
CanvasDomState, CanvasPlanInput, CanvasSurfaceFrame, CanvasSurfaceMode, CanvasDomState, CanvasPlanInput, CanvasSurfaceFrame, CanvasSurfaceMode,
RenderCanvasSurfaceRequest, WebGlContextLifecycle, WebGlRendererState, RenderCanvasSurfaceRequest, WebGlContextLifecycle, WebGlRenderStats, WebGlRendererState,
attach_webgl_context_lifecycle, canvas_render_plan, render_canvas_surface, render_webgl, attach_webgl_context_lifecycle, canvas_render_plan, render_canvas_surface, render_webgl,
set_canvas_surface_mode, webgl_surface_mode, set_canvas_surface_mode, webgl_surface_mode,
}; };
@@ -235,7 +235,7 @@ pub struct TerminalSelectionEvent {
pub end: Option<(usize, usize)>, pub end: Option<(usize, usize)>,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct TerminalRenderEvent { pub struct TerminalRenderEvent {
pub cols: u16, pub cols: u16,
pub rows: u16, pub rows: u16,
@@ -243,6 +243,7 @@ pub struct TerminalRenderEvent {
pub screen_rows: usize, pub screen_rows: usize,
pub mode: ViewMode, pub mode: ViewMode,
pub renderer: Renderer, pub renderer: Renderer,
pub webgl_stats: Option<WebGlRenderStats>,
} }
type SerializeFn = Arc<dyn Fn() -> String + Send + Sync>; type SerializeFn = Arc<dyn Fn() -> String + Send + Sync>;
@@ -514,6 +515,7 @@ pub fn TerminalPanel(
let selecting = RwSignal::new(false); let selecting = RwSignal::new(false);
let search_match = RwSignal::new(None::<TerminalSearchMatch>); let search_match = RwSignal::new(None::<TerminalSearchMatch>);
let search_query = RwSignal::new(String::new()); let search_query = RwSignal::new(String::new());
let last_webgl_stats = RwSignal::new(None::<WebGlRenderStats>);
// IME composition state: when true, regular `input` events are ignored and // IME composition state: when true, regular `input` events are ignored and
// the final composed string is sent on `compositionend`. // the final composed string is sent on `compositionend`.
@@ -624,6 +626,7 @@ pub fn TerminalPanel(
screen_rows: core.screen_rows().len(), screen_rows: core.screen_rows().len(),
mode, mode,
renderer, renderer,
webgl_stats: last_webgl_stats.get_untracked(),
}) { }) {
on_render.run(event); on_render.run(event);
} }
@@ -921,15 +924,14 @@ pub fn TerminalPanel(
}; };
match renderer { match renderer {
Renderer::WebGl => { Renderer::WebGl => {
if render_webgl( if let Ok(stats) = render_webgl(
canvas_el, canvas_el,
frame, frame,
&canvas_state, &canvas_state,
&webgl_state, &webgl_state,
&webgl_context_lost, &webgl_context_lost,
) ) {
.is_ok() last_webgl_stats.set(Some(stats));
{
set_canvas_surface_mode( set_canvas_surface_mode(
canvas_el, canvas_el,
fallback_canvas_el, fallback_canvas_el,
@@ -941,6 +943,7 @@ pub fn TerminalPanel(
fallback_canvas_el, fallback_canvas_el,
webgl_surface_mode(false), webgl_surface_mode(false),
); );
last_webgl_stats.set(None);
render_canvas_surface(RenderCanvasSurfaceRequest { render_canvas_surface(RenderCanvasSurfaceRequest {
canvas_el: fallback_canvas_el, canvas_el: fallback_canvas_el,
frame, frame,
@@ -949,6 +952,7 @@ pub fn TerminalPanel(
} }
} }
Renderer::Canvas => { Renderer::Canvas => {
last_webgl_stats.set(None);
set_canvas_surface_mode(canvas_el, fallback_canvas_el, CanvasSurfaceMode::WebGl); set_canvas_surface_mode(canvas_el, fallback_canvas_el, CanvasSurfaceMode::WebGl);
render_canvas_surface(RenderCanvasSurfaceRequest { render_canvas_surface(RenderCanvasSurfaceRequest {
canvas_el, canvas_el,
@@ -956,7 +960,9 @@ pub fn TerminalPanel(
canvas_state: &canvas_state, canvas_state: &canvas_state,
}); });
} }
Renderer::Dom => {} Renderer::Dom => {
last_webgl_stats.set(None);
}
} }
}); });

View File

@@ -14,3 +14,4 @@ pub use component::{
TerminalSelectionEvent, TerminalTheme, TerminalSelectionEvent, TerminalTheme,
}; };
pub use core::{SearchDirection, TerminalLink, TerminalSearchMatch}; pub use core::{SearchDirection, TerminalLink, TerminalSearchMatch};
pub use renderer::WebGlRenderStats;

View File

@@ -1,11 +1,11 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use js_sys::Float32Array;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure; use wasm_bindgen::closure::Closure;
use web_sys::js_sys::Float32Array;
use web_sys::{ use web_sys::{
CanvasRenderingContext2d, Event, HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer, CanvasRenderingContext2d, Event, HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer,
WebGlProgram, WebGlShader, WebGlTexture, WebGlUniformLocation, WebGlProgram, WebGlShader, WebGlTexture, WebGlUniformLocation,
@@ -364,6 +364,40 @@ pub(super) fn attach_webgl_context_lifecycle(
}) })
} }
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct WebGlRenderStats {
pub total_ms: f64,
pub sync_dom_ms: f64,
pub backdrop_ms: f64,
pub collect_ms: f64,
pub backdrop_texture_ms: f64,
pub fallback_canvas_ms: f64,
pub atlas_ms: f64,
pub atlas_upload_ms: f64,
pub vertex_build_ms: f64,
pub vertex_upload_ms: f64,
pub draw_ms: f64,
pub cells: usize,
pub atlas_entries: usize,
pub atlas_insertions: usize,
pub atlas_resets: usize,
pub fallback_full_canvas: bool,
pub glyph_draw_failed: bool,
}
#[cfg(target_arch = "wasm32")]
fn perf_now() -> f64 {
web_sys::window()
.and_then(|window| window.performance())
.map(|performance| performance.now())
.unwrap_or(0.0)
}
#[cfg(not(target_arch = "wasm32"))]
fn perf_now() -> f64 {
0.0
}
const WEBGL_VERTEX_SHADER: &str = r#" const WEBGL_VERTEX_SHADER: &str = r#"
attribute vec2 a_position; attribute vec2 a_position;
attribute vec2 a_tex_coord; attribute vec2 a_tex_coord;
@@ -660,11 +694,15 @@ pub(super) fn render_webgl(
canvas_state: &Rc<std::cell::RefCell<Option<CanvasDomState>>>, canvas_state: &Rc<std::cell::RefCell<Option<CanvasDomState>>>,
webgl_state: &Rc<std::cell::RefCell<Option<WebGlRendererState>>>, webgl_state: &Rc<std::cell::RefCell<Option<WebGlRendererState>>>,
context_lost: &Rc<std::cell::Cell<bool>>, context_lost: &Rc<std::cell::Cell<bool>>,
) -> Result<(), String> { ) -> Result<WebGlRenderStats, String> {
let total_start = perf_now();
let mut stats = WebGlRenderStats::default();
if !should_attempt_webgl_render(context_lost.get()) { if !should_attempt_webgl_render(context_lost.get()) {
return Err("WebGL context is lost".to_owned()); return Err("WebGL context is lost".to_owned());
} }
let sync_start = perf_now();
sync_canvas_dom(canvas, frame.dom_state, canvas_state); sync_canvas_dom(canvas, frame.dom_state, canvas_state);
stats.sync_dom_ms += perf_now() - sync_start;
let mut state_ref = webgl_state.borrow_mut(); let mut state_ref = webgl_state.borrow_mut();
if state_ref.is_none() { if state_ref.is_none() {
*state_ref = Some(create_webgl_renderer(canvas)?); *state_ref = Some(create_webgl_renderer(canvas)?);
@@ -678,23 +716,45 @@ pub(super) fn render_webgl(
return Err("WebGL context is lost".to_owned()); return Err("WebGL context is lost".to_owned());
} }
let backdrop_start = perf_now();
sync_canvas_bitmap(&state.source_canvas, frame.dom_state); sync_canvas_bitmap(&state.source_canvas, frame.dom_state);
let _ = state let _ = state
.source_ctx .source_ctx
.set_transform(frame.dpr, 0.0, 0.0, frame.dpr, 0.0, 0.0); .set_transform(frame.dpr, 0.0, 0.0, frame.dpr, 0.0, 0.0);
render_canvas_backdrop(&state.source_ctx, frame); render_canvas_backdrop(&state.source_ctx, frame);
let Some(cells) = collect_glyph_cells(frame.visible, frame.options) else { stats.backdrop_ms = perf_now() - backdrop_start;
render_canvas(&state.source_ctx, frame);
draw_webgl_texture_frame(state, frame.dom_state)?;
return Ok(());
};
draw_webgl_texture_frame(state, frame.dom_state)?; let collect_start = perf_now();
if draw_webgl_glyphs(state, frame, &cells).is_err() { let Some(cells) = collect_glyph_cells(frame.visible, frame.options) else {
stats.collect_ms = perf_now() - collect_start;
stats.fallback_full_canvas = true;
let fallback_canvas_start = perf_now();
render_canvas(&state.source_ctx, frame); render_canvas(&state.source_ctx, frame);
stats.fallback_canvas_ms += perf_now() - fallback_canvas_start;
let texture_start = perf_now();
draw_webgl_texture_frame(state, frame.dom_state)?; draw_webgl_texture_frame(state, frame.dom_state)?;
stats.backdrop_texture_ms += perf_now() - texture_start;
stats.total_ms = perf_now() - total_start;
return Ok(stats);
};
stats.collect_ms = perf_now() - collect_start;
stats.cells = cells.len();
let texture_start = perf_now();
draw_webgl_texture_frame(state, frame.dom_state)?;
stats.backdrop_texture_ms += perf_now() - texture_start;
if draw_webgl_glyphs(state, frame, &cells, &mut stats).is_err() {
stats.glyph_draw_failed = true;
let fallback_canvas_start = perf_now();
render_canvas(&state.source_ctx, frame);
stats.fallback_canvas_ms += perf_now() - fallback_canvas_start;
let texture_start = perf_now();
draw_webgl_texture_frame(state, frame.dom_state)?;
stats.backdrop_texture_ms += perf_now() - texture_start;
} }
Ok(()) stats.atlas_entries = state.atlas.entries.len();
stats.total_ms = perf_now() - total_start;
Ok(stats)
} }
fn create_webgl_renderer(canvas: &HtmlCanvasElement) -> Result<WebGlRendererState, String> { fn create_webgl_renderer(canvas: &HtmlCanvasElement) -> Result<WebGlRendererState, String> {
@@ -957,9 +1017,13 @@ fn draw_webgl_glyphs(
state: &mut WebGlRendererState, state: &mut WebGlRendererState,
frame: CanvasSurfaceFrame<'_>, frame: CanvasSurfaceFrame<'_>,
cells: &[GlyphCell], cells: &[GlyphCell],
stats: &mut WebGlRenderStats,
) -> Result<(), String> { ) -> Result<(), String> {
ensure_glyphs_in_atlas(state, frame, cells)?; let atlas_start = perf_now();
ensure_glyphs_in_atlas(state, frame, cells, stats)?;
stats.atlas_ms += perf_now() - atlas_start;
if state.atlas.dirty { if state.atlas.dirty {
let upload_start = perf_now();
state.gl.bind_texture( state.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D, WebGl2RenderingContext::TEXTURE_2D,
Some(&state.atlas_texture), Some(&state.atlas_texture),
@@ -976,8 +1040,10 @@ fn draw_webgl_glyphs(
) )
.map_err(|_| "failed to upload glyph atlas texture".to_owned())?; .map_err(|_| "failed to upload glyph atlas texture".to_owned())?;
state.atlas.dirty = false; state.atlas.dirty = false;
stats.atlas_upload_ms += perf_now() - upload_start;
} }
let vertex_start = perf_now();
let mut vertices = Vec::with_capacity(cells.len() * WEBGL_VERTEX_FLOATS * 6); let mut vertices = Vec::with_capacity(cells.len() * WEBGL_VERTEX_FLOATS * 6);
for cell in cells { for cell in cells {
let Some(entry) = state.atlas.get(&cell.key) else { let Some(entry) = state.atlas.get(&cell.key) else {
@@ -998,6 +1064,7 @@ fn draw_webgl_glyphs(
frame.dom_state, frame.dom_state,
); );
} }
stats.vertex_build_ms += perf_now() - vertex_start;
if vertices.is_empty() { if vertices.is_empty() {
return Ok(()); return Ok(());
} }
@@ -1005,18 +1072,22 @@ fn draw_webgl_glyphs(
let gl = &state.gl; let gl = &state.gl;
gl.use_program(Some(&state.program)); gl.use_program(Some(&state.program));
bind_webgl_vertex_layout(gl, state); bind_webgl_vertex_layout(gl, state);
let upload_start = perf_now();
upload_webgl_vertices(gl, &vertices); upload_webgl_vertices(gl, &vertices);
stats.vertex_upload_ms += perf_now() - upload_start;
gl.active_texture(WebGl2RenderingContext::TEXTURE0); gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture( gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D, WebGl2RenderingContext::TEXTURE_2D,
Some(&state.atlas_texture), Some(&state.atlas_texture),
); );
gl.uniform1i(state.sampler_location.as_ref(), 0); gl.uniform1i(state.sampler_location.as_ref(), 0);
let draw_start = perf_now();
gl.draw_arrays( gl.draw_arrays(
WebGl2RenderingContext::TRIANGLES, WebGl2RenderingContext::TRIANGLES,
0, 0,
(vertices.len() / WEBGL_VERTEX_FLOATS) as i32, (vertices.len() / WEBGL_VERTEX_FLOATS) as i32,
); );
stats.draw_ms += perf_now() - draw_start;
Ok(()) Ok(())
} }
@@ -1024,10 +1095,12 @@ fn ensure_glyphs_in_atlas(
state: &mut WebGlRendererState, state: &mut WebGlRendererState,
frame: CanvasSurfaceFrame<'_>, frame: CanvasSurfaceFrame<'_>,
cells: &[GlyphCell], cells: &[GlyphCell],
stats: &mut WebGlRenderStats,
) -> Result<(), String> { ) -> Result<(), String> {
for attempt in 0..2 { for attempt in 0..2 {
if attempt == 1 { if attempt == 1 {
state.atlas.reset(); state.atlas.reset();
stats.atlas_resets += 1;
state.atlas_ctx.clear_rect( state.atlas_ctx.clear_rect(
0.0, 0.0,
0.0, 0.0,
@@ -1038,7 +1111,7 @@ fn ensure_glyphs_in_atlas(
let mut ok = true; let mut ok = true;
for cell in cells { for cell in cells {
if state.atlas.get(&cell.key).is_none() if state.atlas.get(&cell.key).is_none()
&& draw_glyph_into_atlas(state, frame, &cell.key)?.is_none() && draw_glyph_into_atlas(state, frame, &cell.key, stats)?.is_none()
{ {
ok = false; ok = false;
break; break;
@@ -1055,6 +1128,7 @@ fn draw_glyph_into_atlas(
state: &mut WebGlRendererState, state: &mut WebGlRendererState,
frame: CanvasSurfaceFrame<'_>, frame: CanvasSurfaceFrame<'_>,
key: &GlyphKey, key: &GlyphKey,
stats: &mut WebGlRenderStats,
) -> Result<Option<GlyphAtlasEntry>, String> { ) -> Result<Option<GlyphAtlasEntry>, String> {
let dpr = frame.dpr.max(1.0); let dpr = frame.dpr.max(1.0);
let width = (key.cols as f64 * frame.cell_size.0 * dpr).ceil() as u32; let width = (key.cols as f64 * frame.cell_size.0 * dpr).ceil() as u32;
@@ -1096,6 +1170,7 @@ fn draw_glyph_into_atlas(
ctx.fill_text(&key.text, f64::from(entry.x), f64::from(entry.y)) ctx.fill_text(&key.text, f64::from(entry.x), f64::from(entry.y))
.map_err(|_| "failed to draw glyph into atlas".to_owned())?; .map_err(|_| "failed to draw glyph into atlas".to_owned())?;
ctx.restore(); ctx.restore();
stats.atlas_insertions += 1;
Ok(Some(entry)) Ok(Some(entry))
} }

View File

@@ -222,6 +222,9 @@ async function runScenario(browser, options, viewport) {
frameSamples.push( frameSamples.push(
...(await page.evaluate(() => window.__terminalBenchFrames || [])), ...(await page.evaluate(() => window.__terminalBenchFrames || [])),
); );
const rendererStats = await page.evaluate(() => window.__terminalBench?.webglFrames || []);
const cleanProbe = { ...probe };
delete cleanProbe.webglFrames;
const canvasPixelSmoke = await page.evaluate(() => { const canvasPixelSmoke = await page.evaluate(() => {
const canvas = document.querySelector("canvas.terminal-canvas:not([hidden])"); const canvas = document.querySelector("canvas.terminal-canvas:not([hidden])");
if (!canvas) { if (!canvas) {
@@ -279,7 +282,8 @@ async function runScenario(browser, options, viewport) {
elapsedMs, elapsedMs,
rowsPerSecond: round(options.lines / (elapsedMs / 1000)), rowsPerSecond: round(options.lines / (elapsedMs / 1000)),
frameMs: summarizeSamples(frameSamples), frameMs: summarizeSamples(frameSamples),
probe, rendererStats: summarizeRendererStats(rendererStats),
probe: cleanProbe,
pixelSmoke: pixelResult, pixelSmoke: pixelResult,
screenshotPath, screenshotPath,
}; };
@@ -288,6 +292,43 @@ async function runScenario(browser, options, viewport) {
} }
} }
export function summarizeRendererStats(frames) {
if (!Array.isArray(frames) || frames.length === 0) {
return { count: 0 };
}
const numericFields = [
"totalMs",
"syncDomMs",
"backdropMs",
"collectMs",
"backdropTextureMs",
"fallbackCanvasMs",
"atlasMs",
"atlasUploadMs",
"vertexBuildMs",
"vertexUploadMs",
"drawMs",
"cells",
"atlasEntries",
"atlasInsertions",
"atlasResets",
];
const summary = { count: frames.length };
for (const field of numericFields) {
const samples = frames
.map((frame) => Number(frame[field] || 0))
.filter((value) => Number.isFinite(value));
summary[field] = summarizeSamples(samples);
}
summary.fallbackFullCanvasCount = frames.filter(
(frame) => Boolean(frame.fallbackFullCanvas),
).length;
summary.glyphDrawFailedCount = frames.filter(
(frame) => Boolean(frame.glyphDrawFailed),
).length;
return summary;
}
function benchUrl(options) { function benchUrl(options) {
const url = new URL(options.url); const url = new URL(options.url);
url.searchParams.set("renderer", options.renderer); url.searchParams.set("renderer", options.renderer);