diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 475504ef12..b98aaa98f2 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -874,22 +874,37 @@ (def render-finish (letfn [(do-render [ts] + (perf/begin-measure "render-finish") (h/call wasm/internal-module "_set_view_end") - (render ts))] + (render ts) + (perf/end-measure "render-finish"))] (fns/debounce do-render DEBOUNCE_DELAY_MS))) (def render-pan - (fns/throttle render THROTTLE_DELAY_MS)) + (letfn [(do-render-pan [ts] + (perf/begin-measure "render-pan") + (render ts) + (perf/end-measure "render-pan"))] + (fns/throttle do-render-pan THROTTLE_DELAY_MS))) (defn set-view-box [prev-zoom zoom vbox] - (h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) + (let [is-pan (mth/close? prev-zoom zoom)] + (perf/begin-measure "set-view-box") + (h/call wasm/internal-module "_set_view_start") + (h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) - (if (mth/close? prev-zoom zoom) - (do (render-pan) - (render-finish)) - (do (h/call wasm/internal-module "_render_from_cache" 0) - (render-finish)))) + (if is-pan + (do (perf/end-measure "set-view-box") + (perf/begin-measure "set-view-box::pan") + (render-pan) + (render-finish) + (perf/end-measure "set-view-box::pan")) + (do (perf/end-measure "set-view-box") + (perf/begin-measure "set-view-box::zoom") + (h/call wasm/internal-module "_render_from_cache" 0) + (render-finish) + (perf/end-measure "set-view-box::zoom"))))) (defn set-object [objects shape] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 68a318675e..1280028997 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -230,20 +230,62 @@ pub extern "C" fn resize_viewbox(width: i32, height: i32) { #[no_mangle] pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) { with_state_mut!(state, { + performance::begin_measure!("set_view"); let render_state = state.render_state_mut(); render_state.set_view(zoom, x, y); + performance::end_measure!("set_view"); + }); +} + +#[cfg(feature = "profile-macros")] +static mut VIEW_INTERACTION_START: i32 = 0; + +#[no_mangle] +pub extern "C" fn set_view_start() { + with_state_mut!(state, { + #[cfg(feature = "profile-macros")] + unsafe { + VIEW_INTERACTION_START = performance::get_time(); + } + performance::begin_measure!("set_view_start"); + state.render_state.options.set_fast_mode(true); + performance::end_measure!("set_view_start"); }); } #[no_mangle] pub extern "C" fn set_view_end() { with_state_mut!(state, { - // We can have renders in progress + let _end_start = performance::begin_timed_log!("set_view_end"); + performance::begin_measure!("set_view_end"); + state.render_state.options.set_fast_mode(false); state.render_state.cancel_animation_frame(); - if state.render_state.options.is_profile_rebuild_tiles() { - state.rebuild_tiles(); - } else { - state.rebuild_tiles_shallow(); + + let zoom_changed = state.render_state.zoom_changed(); + // Only rebuild tile indices when zoom has changed. + // During pan-only operations, shapes stay in the same tiles + // because tile_size = 1/scale * TILE_SIZE (depends only on zoom). + if zoom_changed { + let _rebuild_start = performance::begin_timed_log!("rebuild_tiles"); + performance::begin_measure!("set_view_end::rebuild_tiles"); + if state.render_state.options.is_profile_rebuild_tiles() { + state.rebuild_tiles(); + } else { + state.rebuild_tiles_shallow(); + } + performance::end_measure!("set_view_end::rebuild_tiles"); + performance::end_timed_log!("rebuild_tiles", _rebuild_start); + } + performance::end_measure!("set_view_end"); + performance::end_timed_log!("set_view_end", _end_start); + #[cfg(feature = "profile-macros")] + { + let total_time = performance::get_time() - unsafe { VIEW_INTERACTION_START }; + performance::console_log!( + "[PERF] view_interaction (zoom_changed={}): {}ms", + zoom_changed, + total_time + ); } }); } diff --git a/render-wasm/src/options.rs b/render-wasm/src/options.rs index 37afacfc21..1267c900ea 100644 --- a/render-wasm/src/options.rs +++ b/render-wasm/src/options.rs @@ -1,2 +1,3 @@ pub const DEBUG_VISIBLE: u32 = 0x01; pub const PROFILE_REBUILD_TILES: u32 = 0x02; +pub const FAST_MODE: u32 = 0x04; diff --git a/render-wasm/src/performance.rs b/render-wasm/src/performance.rs index 0d508b1b89..6f9eb233de 100644 --- a/render-wasm/src/performance.rs +++ b/render-wasm/src/performance.rs @@ -1,7 +1,3 @@ -#[allow(unused_imports)] -#[cfg(target_arch = "wasm32")] -use crate::get_now; - #[allow(dead_code)] #[cfg(target_arch = "wasm32")] pub fn get_time() -> i32 { @@ -15,6 +11,68 @@ pub fn get_time() -> i32 { now.elapsed().as_millis() as i32 } +/// Log a message to the browser console (only when profile-macros feature is enabled) +#[macro_export] +macro_rules! console_log { + ($($arg:tt)*) => { + #[cfg(all(feature = "profile-macros", target_arch = "wasm32"))] + { + use $crate::run_script; + run_script!(format!("console.log('{}')", format!($($arg)*))); + } + #[cfg(all(feature = "profile-macros", not(target_arch = "wasm32")))] + { + println!($($arg)*); + } + }; +} + +/// Begin a timed section with logging (only when profile-macros feature is enabled) +/// Returns the start time - store it and pass to end_timed_log! +#[macro_export] +macro_rules! begin_timed_log { + ($name:expr) => {{ + #[cfg(feature = "profile-macros")] + { + $crate::performance::get_time() + } + #[cfg(not(feature = "profile-macros"))] + { + 0.0 + } + }}; +} + +/// End a timed section and log the duration (only when profile-macros feature is enabled) +#[macro_export] +macro_rules! end_timed_log { + ($name:expr, $start:expr) => {{ + #[cfg(all(feature = "profile-macros", target_arch = "wasm32"))] + { + let duration = $crate::performance::get_time() - $start; + use $crate::run_script; + run_script!(format!( + "console.log('[PERF] {}: {:.2}ms')", + $name, duration + )); + } + #[cfg(all(feature = "profile-macros", not(target_arch = "wasm32")))] + { + let duration = $crate::performance::get_time() - $start; + println!("[PERF] {}: {:.2}ms", $name, duration); + } + }}; +} + +#[allow(unused_imports)] +pub use console_log; + +#[allow(unused_imports)] +pub use begin_timed_log; + +#[allow(unused_imports)] +pub use end_timed_log; + #[macro_export] macro_rules! mark { ($name:expr) => { diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 2b3038999d..86e338f7c2 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -928,6 +928,8 @@ impl RenderState { } pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) { + let _start = performance::begin_timed_log!("render_from_cache"); + performance::begin_measure!("render_from_cache"); let scale = self.get_cached_scale(); if let Some(snapshot) = &self.cached_target_snapshot { let canvas = self.surfaces.canvas(SurfaceId::Target); @@ -965,6 +967,8 @@ impl RenderState { self.flush_and_submit(); } + performance::end_measure!("render_from_cache"); + performance::end_timed_log!("render_from_cache", _start); } pub fn start_render_loop( @@ -974,6 +978,7 @@ impl RenderState { timestamp: i32, sync_render: bool, ) -> Result<(), String> { + let _start = performance::begin_timed_log!("start_render_loop"); let scale = self.get_scale(); self.tile_viewbox.update(self.viewbox, scale); @@ -1004,10 +1009,12 @@ impl RenderState { // FIXME - review debug // debug::render_debug_tiles_for_viewbox(self); + let _tile_start = performance::begin_timed_log!("tile_cache_update"); performance::begin_measure!("tile_cache"); self.pending_tiles .update(&self.tile_viewbox, &self.surfaces); performance::end_measure!("tile_cache"); + performance::end_timed_log!("tile_cache_update", _tile_start); self.pending_nodes.clear(); if self.pending_nodes.capacity() < tree.len() { @@ -1031,6 +1038,7 @@ impl RenderState { } performance::end_measure!("start_render_loop"); + performance::end_timed_log!("start_render_loop", _start); Ok(()) } @@ -1479,8 +1487,11 @@ impl RenderState { .surfaces .get_render_context_translation(self.render_area, scale); + // Skip expensive drop shadow rendering in fast mode (during pan/zoom) + let skip_shadows = self.options.is_fast_mode(); + // For text shapes, render drop shadow using text rendering logic - if !matches!(element.shape_type, Type::Text(_)) { + if !skip_shadows && !matches!(element.shape_type, Type::Text(_)) { // Shadow rendering technique: Two-pass approach for proper opacity handling // // The shadow rendering uses a two-pass technique to ensure that overlapping @@ -2054,6 +2065,10 @@ impl RenderState { self.cached_viewbox.zoom() * self.options.dpr() } + pub fn zoom_changed(&self) -> bool { + (self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON + } + pub fn mark_touched(&mut self, uuid: Uuid) { self.touched_ids.insert(uuid); } diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index 74fb1cf70c..9dd57309e7 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -15,6 +15,19 @@ impl RenderOptions { self.flags & options::PROFILE_REBUILD_TILES == options::PROFILE_REBUILD_TILES } + /// Use fast mode to enable / disable expensive operations + pub fn is_fast_mode(&self) -> bool { + self.flags & options::FAST_MODE == options::FAST_MODE + } + + pub fn set_fast_mode(&mut self, enabled: bool) { + if enabled { + self.flags |= options::FAST_MODE; + } else { + self.flags &= !options::FAST_MODE; + } + } + pub fn dpr(&self) -> f32 { self.dpr.unwrap_or(1.0) }