.
diff --git a/vello_gpu/src/lib.rs b/vello_gpu/src/lib.rs index 2e6b70a..65b6b02 100644 --- a/vello_gpu/src/lib.rs +++ b/vello_gpu/src/lib.rs
@@ -107,6 +107,29 @@ extern crate alloc; +#[cfg(feature = "std")] +#[doc(hidden)] +pub mod timings; + +/// The start of a rendering phase, for [`phase_end`]. +pub(crate) fn phase_start() -> f64 { + #[cfg(feature = "std")] + return timings::now_ms(); + #[cfg(not(feature = "std"))] + 0.0 +} + +/// Adds the time since `$start` to the given field of the render phase timings. +macro_rules! phase_end { + ($field:ident, $start:expr) => { + #[cfg(feature = "std")] + crate::timings::record(|t| t.$field += crate::timings::now_ms() - $start); + #[cfg(not(feature = "std"))] + let _ = $start; + }; +} +pub(crate) use phase_end; + pub(crate) mod blend; pub(crate) mod copy; pub(crate) mod filter;
diff --git a/vello_gpu/src/render/webgl/mod.rs b/vello_gpu/src/render/webgl/mod.rs index 6e3cfb9..1e7ed71 100644 --- a/vello_gpu/src/render/webgl/mod.rs +++ b/vello_gpu/src/render/webgl/mod.rs
@@ -597,6 +597,7 @@ } }; + let before_render_start = crate::phase_start(); #[cfg(feature = "text")] resources.before_render( self, @@ -619,6 +620,7 @@ }, )?; + crate::phase_end!(before_render_ms, before_render_start); let target_init = TargetInit::Clear(ClearSettings::Viewport { color: options.clear_color, }); @@ -696,10 +698,12 @@ } } - #[cfg(feature = "text")] // TODO: We should sort the rectangles once by atlas // page and then clear per atlas page instead of per rect. + let after_render_start = crate::phase_start(); + #[cfg(feature = "text")] resources.after_render(self, clear_atlas_region)?; + crate::phase_end!(after_render_ms, after_render_start); Ok(()) } @@ -822,12 +826,15 @@ let encoded_paints = &scene.encoded_paints; let use_depth_buffer = self.programs.resources.view_framebuffer.use_depth_buffer(); + let phase = crate::phase_start(); self.prepare_gpu_encoded_paints( encoded_paints, image_cache, texture_bindings, render_target_texture, )?; + crate::phase_end!(encode_paints_ms, phase); + let phase = crate::phase_start(); let mut required_texture_size = self .layers_config @@ -857,8 +864,14 @@ current_allocations, self.layers_config.max_textures, )?; + crate::phase_end!(schedule_ms, phase); + let phase = crate::phase_start(); self.programs .prepare_intermediate_textures(&self.gl, &schedule, required_texture_size)?; + crate::phase_end!(intermediate_textures_ms, phase); + let phase = crate::phase_start(); + #[cfg(feature = "std")] + let alpha_bytes = scene.strip_storage.borrow().alphas.len() as u64; // TODO: For the time being, we upload the entire alpha buffer as one big chunk. As a future // refinement, we could have a bounded alpha buffer, and break draws when the alpha @@ -874,6 +887,18 @@ &self.schedule_storage.filter_context, root_output_target, )?; + crate::phase_end!(upload_ms, phase); + #[cfg(feature = "std")] + let strip_count = (self + .schedule_storage + .buffers + .draw_buffers + .opaque + .strips() + .len() + + self.schedule_storage.buffers.draw_buffers.strips.len()) + as u64; + let phase = crate::phase_start(); self.programs.resources.depth_cleared_this_frame = false; let mut ctx = WebGlRendererContext { programs: &mut self.programs, @@ -891,6 +916,14 @@ schedule, root_output_target, )?; + crate::phase_end!(execute_ms, phase); + #[cfg(feature = "std")] + crate::timings::record(|t| { + t.renders += 1; + t.strips += strip_count; + t.alpha_bytes += alpha_bytes; + t.encoded_paints += encoded_paints.len() as u64; + }); self.gl.bind_framebuffer( WebGl2RenderingContext::FRAMEBUFFER,
diff --git a/vello_gpu/src/render/wgpu/mod.rs b/vello_gpu/src/render/wgpu/mod.rs index 80851a3..fac6abb 100644 --- a/vello_gpu/src/render/wgpu/mod.rs +++ b/vello_gpu/src/render/wgpu/mod.rs
@@ -412,6 +412,7 @@ } }; + let before_render_start = crate::phase_start(); #[cfg(feature = "text")] { resources.before_render( @@ -439,6 +440,7 @@ )?; } + crate::phase_end!(before_render_ms, before_render_start); let result = self.render_scene( scene, encoder, @@ -453,12 +455,14 @@ options.offset, ); + let after_render_start = crate::phase_start(); #[cfg(feature = "text")] resources.after_render(self, |renderer, rect| { clear_atlas_region(renderer, rect); Ok::<(), RenderError>(()) })?; + crate::phase_end!(after_render_ms, after_render_start); result } @@ -566,12 +570,15 @@ scene_offset: (i32, i32), ) -> Result<(), RenderError> { self.programs.depth_cleared_this_frame = false; + let phase = crate::phase_start(); self.prepare_gpu_encoded_paints( encoded_paints, image_cache, texture_bindings, view.texture(), )?; + crate::phase_end!(encode_paints_ms, phase); + let phase = crate::phase_start(); let required_texture_size = self .layers_config .required_intermediate_texture_size(&scene.recorder)?; @@ -597,8 +604,14 @@ current_allocations, self.layers_config.max_textures, )?; + crate::phase_end!(schedule_ms, phase); + let phase = crate::phase_start(); self.programs .prepare_intermediate_textures(&self.device, &schedule); + crate::phase_end!(intermediate_textures_ms, phase); + let phase = crate::phase_start(); + #[cfg(feature = "std")] + let alpha_bytes = scene.strip_storage.borrow().alphas.len() as u64; // TODO: For the time being, we upload the entire alpha buffer as one big chunk. As a future // refinement, we could have a bounded alpha buffer, and break draws when the alpha // buffer fills. @@ -625,6 +638,8 @@ self.programs .strips_arena .begin_frame(&self.device, (strip_count * size_of::<GpuStrip>()) as u64); + crate::phase_end!(upload_ms, phase); + let phase = crate::phase_start(); let mut ctx = RendererContext { programs: &mut self.programs, @@ -650,6 +665,14 @@ .unwrap_or_else(|error| match error {}); ctx.finish_root_clear(); + crate::phase_end!(execute_ms, phase); + #[cfg(feature = "std")] + crate::timings::record(|t| { + t.renders += 1; + t.strips += strip_count as u64; + t.alpha_bytes += alpha_bytes; + t.encoded_paints += encoded_paints.len() as u64; + }); self.gradient_cache.maintain();
diff --git a/vello_gpu/src/timings.rs b/vello_gpu/src/timings.rs new file mode 100644 index 0000000..746e013 --- /dev/null +++ b/vello_gpu/src/timings.rs
@@ -0,0 +1,81 @@ +//! Temporary instrumentation: accumulates the time spent in the phases of rendering. + +extern crate std; + +use core::cell::RefCell; +use std::thread_local; + +/// The accumulated time per rendering phase, in milliseconds, since the last [`take`]. +#[derive(Clone, Debug, Default)] +pub struct RenderPhaseTimings { + pub renders: u32, + pub before_render_ms: f64, + pub encode_paints_ms: f64, + pub schedule_ms: f64, + pub intermediate_textures_ms: f64, + pub upload_ms: f64, + pub execute_ms: f64, + pub after_render_ms: f64, + pub strips: u64, + pub alpha_bytes: u64, + pub encoded_paints: u64, +} + +impl core::fmt::Display for RenderPhaseTimings { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{} renders: before_render {:.2}ms, encode_paints {:.2}ms, schedule {:.2}ms, \ + intermediate_textures {:.2}ms, upload {:.2}ms, execute {:.2}ms, after_render {:.2}ms \ + | per render: {} strips, {} KiB alphas, {} paints", + self.renders, + self.before_render_ms, + self.encode_paints_ms, + self.schedule_ms, + self.intermediate_textures_ms, + self.upload_ms, + self.execute_ms, + self.after_render_ms, + self.strips / u64::from(self.renders.max(1)), + self.alpha_bytes / u64::from(self.renders.max(1)) / 1024, + self.encoded_paints / u64::from(self.renders.max(1)), + ) + } +} + +thread_local! { + static TIMINGS: RefCell<RenderPhaseTimings> = RefCell::new(RenderPhaseTimings::default()); +} + +/// Returns the timings accumulated on this thread and resets them. +pub fn take() -> RenderPhaseTimings { + TIMINGS.with(|timings| core::mem::take(&mut *timings.borrow_mut())) +} + +pub(crate) fn record(f: impl FnOnce(&mut RenderPhaseTimings)) { + TIMINGS.with(|timings| f(&mut timings.borrow_mut())); +} + +#[cfg(all(target_arch = "wasm32", feature = "webgl"))] +pub(crate) fn now_ms() -> f64 { + let performance = js_sys::Reflect::get(&js_sys::global(), &"performance".into()).unwrap(); + let now = js_sys::Reflect::get(&performance, &"now".into()).unwrap(); + js_sys::Function::from(now) + .call0(&performance) + .unwrap() + .as_f64() + .unwrap() +} + +#[cfg(all(target_arch = "wasm32", not(feature = "webgl")))] +pub(crate) fn now_ms() -> f64 { + 0.0 +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn now_ms() -> f64 { + use std::sync::OnceLock; + use std::time::Instant; + static START: OnceLock<Instant> = OnceLock::new(); + START.get_or_init(Instant::now).elapsed().as_secs_f64() * 1000.0 +}