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

@@ -115,6 +115,7 @@ fn update_terminal_bench_probe(event: TerminalRenderEvent) {
"history"
};
let renderer = format!("{:?}", event.renderer);
let webgl_stats = event.webgl_stats;
let _ = js_sys::Reflect::set(
&probe,
@@ -152,6 +153,61 @@ fn update_terminal_bench_probe(event: TerminalRenderEvent) {
&JsValue::from_str("screenRows"),
&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"))]

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
use std::collections::HashMap;
use std::rc::Rc;
use js_sys::Float32Array;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure;
use web_sys::js_sys::Float32Array;
use web_sys::{
CanvasRenderingContext2d, Event, HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer,
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#"
attribute vec2 a_position;
attribute vec2 a_tex_coord;
@@ -660,11 +694,15 @@ pub(super) fn render_webgl(
canvas_state: &Rc<std::cell::RefCell<Option<CanvasDomState>>>,
webgl_state: &Rc<std::cell::RefCell<Option<WebGlRendererState>>>,
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()) {
return Err("WebGL context is lost".to_owned());
}
let sync_start = perf_now();
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();
if state_ref.is_none() {
*state_ref = Some(create_webgl_renderer(canvas)?);
@@ -678,23 +716,45 @@ pub(super) fn render_webgl(
return Err("WebGL context is lost".to_owned());
}
let backdrop_start = perf_now();
sync_canvas_bitmap(&state.source_canvas, frame.dom_state);
let _ = state
.source_ctx
.set_transform(frame.dpr, 0.0, 0.0, frame.dpr, 0.0, 0.0);
render_canvas_backdrop(&state.source_ctx, frame);
let Some(cells) = collect_glyph_cells(frame.visible, frame.options) else {
render_canvas(&state.source_ctx, frame);
draw_webgl_texture_frame(state, frame.dom_state)?;
return Ok(());
};
stats.backdrop_ms = perf_now() - backdrop_start;
draw_webgl_texture_frame(state, frame.dom_state)?;
if draw_webgl_glyphs(state, frame, &cells).is_err() {
let collect_start = perf_now();
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);
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;
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> {
@@ -957,9 +1017,13 @@ fn draw_webgl_glyphs(
state: &mut WebGlRendererState,
frame: CanvasSurfaceFrame<'_>,
cells: &[GlyphCell],
stats: &mut WebGlRenderStats,
) -> 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 {
let upload_start = perf_now();
state.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&state.atlas_texture),
@@ -976,8 +1040,10 @@ fn draw_webgl_glyphs(
)
.map_err(|_| "failed to upload glyph atlas texture".to_owned())?;
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);
for cell in cells {
let Some(entry) = state.atlas.get(&cell.key) else {
@@ -998,6 +1064,7 @@ fn draw_webgl_glyphs(
frame.dom_state,
);
}
stats.vertex_build_ms += perf_now() - vertex_start;
if vertices.is_empty() {
return Ok(());
}
@@ -1005,18 +1072,22 @@ fn draw_webgl_glyphs(
let gl = &state.gl;
gl.use_program(Some(&state.program));
bind_webgl_vertex_layout(gl, state);
let upload_start = perf_now();
upload_webgl_vertices(gl, &vertices);
stats.vertex_upload_ms += perf_now() - upload_start;
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&state.atlas_texture),
);
gl.uniform1i(state.sampler_location.as_ref(), 0);
let draw_start = perf_now();
gl.draw_arrays(
WebGl2RenderingContext::TRIANGLES,
0,
(vertices.len() / WEBGL_VERTEX_FLOATS) as i32,
);
stats.draw_ms += perf_now() - draw_start;
Ok(())
}
@@ -1024,10 +1095,12 @@ fn ensure_glyphs_in_atlas(
state: &mut WebGlRendererState,
frame: CanvasSurfaceFrame<'_>,
cells: &[GlyphCell],
stats: &mut WebGlRenderStats,
) -> Result<(), String> {
for attempt in 0..2 {
if attempt == 1 {
state.atlas.reset();
stats.atlas_resets += 1;
state.atlas_ctx.clear_rect(
0.0,
0.0,
@@ -1038,7 +1111,7 @@ fn ensure_glyphs_in_atlas(
let mut ok = true;
for cell in cells {
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;
break;
@@ -1055,6 +1128,7 @@ fn draw_glyph_into_atlas(
state: &mut WebGlRendererState,
frame: CanvasSurfaceFrame<'_>,
key: &GlyphKey,
stats: &mut WebGlRenderStats,
) -> Result<Option<GlyphAtlasEntry>, String> {
let dpr = frame.dpr.max(1.0);
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))
.map_err(|_| "failed to draw glyph into atlas".to_owned())?;
ctx.restore();
stats.atlas_insertions += 1;
Ok(Some(entry))
}