test(terminal): profile webgl render stages
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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。
|
||||||
- 当前策略:默认 Canvas;WebGL 继续通过页面切换、`?renderer=webgl` 和 `--renderer webgl` 保留优化回归入口。
|
- 当前策略:默认 Canvas;WebGL 继续通过页面切换、`?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。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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"))]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user