diff --git a/frontend/src/app/render_wasm/performance.cljc b/frontend/src/app/render_wasm/performance.cljc index 95c39c4468..762ecbb823 100644 --- a/frontend/src/app/render_wasm/performance.cljc +++ b/frontend/src/app/render_wasm/performance.cljc @@ -14,6 +14,35 @@ #?(:clj (= (System/getProperty "penpot.wasm.profile-marks") "true") :cljs false)) +(defn create-memory + [used total] + #?(:clj {:used used :total total} + :cljs #js {:used used :total total})) + +(defn get-memory + [] + #?(:clj (create-memory -1 -1) + :cljs (create-memory + (.-usedJSHeapSize (.-memory js/performance)) + (.-totalJSHeapSize (.-memory js/performance))))) + +(defn memory-measure + [] + #?(:clj (fn [] + {:begin (create-memory -1 -1) + :end (create-memory -1 -1) + :delta (create-memory -1 -1)}) + :cljs (let [begin-memory (get-memory)] + (fn [] + (let [end-memory (get-memory)] + #js {:begin begin-memory + :end end-memory + :delta (create-memory + (- (.-used end-memory) + (.-used begin-memory)) + (- (.-total end-memory) + (.-total begin-memory)))}))))) + (defmacro begin-measure [measure-name] (when enabled? diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index c9f243d161..77fc2b7044 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -49,6 +49,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "cc" version = "1.1.31" @@ -144,8 +150,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -211,6 +219,16 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "khronos_api" version = "3.1.0" @@ -293,6 +311,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -395,6 +419,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.18" @@ -558,6 +588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", + "wasm-bindgen", ] [[package]] @@ -566,6 +597,64 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index 1968d4d6aa..55f41c1e2d 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -26,7 +26,7 @@ skia-safe = { version = "0.81.0", default-features = false, features = [ "textlayout", "binary-cache", ] } -uuid = { version = "1.11.0", features = ["v4"] } +uuid = { version = "1.11.0", features = ["v4", "js"] } [profile.release] opt-level = "s" diff --git a/render-wasm/_build_env b/render-wasm/_build_env index ec85421aca..0ddd949727 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -6,11 +6,30 @@ else export _BUILD_MODE=${1:-debug}; fi +# 256 MB of initial heap to perform less +# initial calls to memory grow. +EM_INITIAL_HEAP=$((256 * 1024 * 1024)) + +# 1.0 doubles the heap on every growth. +EM_MEMORY_GROWTH_GEOMETRIC_STEP="0.8" + +# Malloc implementation to use. +# - dlmalloc: a powerful general-purpose malloc. +# - emmalloc: a simple and compact malloc designed for emscripten. +# - emmalloc-debug: use emmalloc and add extra assertion checks. +# - emmalloc-memvalidate: use emmalloc with assertions+heap consistency checking. +# - emmalloc-verbose: use emmalloc with assertions + verbose logging. +# - emmalloc-memvalidate-verbose: use emmalloc with assertions + heap consistency checking + verbose logging. +# Default: dlmalloc +EM_MALLOC="dlmalloc" + EMCC_CFLAGS="--no-entry \ --js-library src/js/wapi.js \ -sASSERTIONS=1 \ -sALLOW_TABLE_GROWTH=1 \ -sALLOW_MEMORY_GROWTH=1 \ + -sINITIAL_HEAP=$EM_INITIAL_HEAP \ + -sMEMORY_GROWTH_GEOMETRIC_STEP=$EM_MEMORY_GROWTH_GEOMETRIC_STEP \ -sENVIRONMENT=web \ -sERROR_ON_UNDEFINED_SYMBOLS=0 \ -sMAX_WEBGL_VERSION=2 \ @@ -31,7 +50,7 @@ else # -gseparate-dwarf # -gsplit-dwarf # -gsource-map - EMCC_CFLAGS="-g $EMCC_CFLAGS -sVERBOSE=1 -sMALLOC=emmalloc-debug" + EMCC_CFLAGS="-g $EMCC_CFLAGS -sVERBOSE=1 -sMALLOC=$EM_MALLOC" fi export EMCC_CFLAGS; diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index eed6883112..67fab0e98c 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -279,18 +279,14 @@ impl RenderState { Some(&skia::Paint::default()), ); } + let surface_ids = SurfaceId::Strokes as u32 + | SurfaceId::Fills as u32 + | SurfaceId::DropShadows as u32 + | SurfaceId::InnerShadows as u32; - self.surfaces.apply_mut( - &[ - SurfaceId::DropShadows, - SurfaceId::InnerShadows, - SurfaceId::Fills, - SurfaceId::Strokes, - ], - |s| { - s.canvas().clear(skia::Color::TRANSPARENT); - }, - ); + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().clear(skia::Color::TRANSPARENT); + }); } pub fn render_shape( @@ -306,12 +302,10 @@ impl RenderState { shape }; - let surface_ids = &[ - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::DropShadows, - SurfaceId::InnerShadows, - ]; + let surface_ids = SurfaceId::Strokes as u32 + | SurfaceId::Fills as u32 + | SurfaceId::DropShadows as u32 + | SurfaceId::InnerShadows as u32; self.surfaces.apply_mut(surface_ids, |s| { s.canvas().save(); }); @@ -391,17 +385,13 @@ impl RenderState { } Type::Text(text_content) => { - self.surfaces.apply_mut( - &[ - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::DropShadows, - SurfaceId::InnerShadows, - ], - |s| { - s.canvas().concat(&matrix); - }, - ); + let surface_ids = SurfaceId::Strokes as u32 + | SurfaceId::Fills as u32 + | SurfaceId::DropShadows as u32 + | SurfaceId::InnerShadows as u32; + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().concat(&matrix); + }); let text_content = text_content.new_bounds(shape.selrect()); let paragraphs = text_content.get_skia_paragraphs(self.fonts.font_collection()); @@ -437,17 +427,13 @@ impl RenderState { shadows::render_text_inner_shadows(self, &shape, ¶graphs, antialias); } _ => { - self.surfaces.apply_mut( - &[ - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::DropShadows, - SurfaceId::InnerShadows, - ], - |s| { - s.canvas().concat(&matrix); - }, - ); + let surface_ids = SurfaceId::Strokes as u32 + | SurfaceId::Fills as u32 + | SurfaceId::DropShadows as u32 + | SurfaceId::InnerShadows as u32; + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().concat(&matrix); + }); for fill in shape.fills().rev() { fills::render(self, &shape, fill, antialias); @@ -465,17 +451,13 @@ impl RenderState { }; self.apply_drawing_to_render_canvas(Some(&shape)); - self.surfaces.apply_mut( - &[ - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::DropShadows, - SurfaceId::InnerShadows, - ], - |s| { - s.canvas().restore(); - }, - ); + let surface_ids = SurfaceId::Strokes as u32 + | SurfaceId::Fills as u32 + | SurfaceId::DropShadows as u32 + | SurfaceId::InnerShadows as u32; + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().restore(); + }); } pub fn update_render_context(&mut self, tile: tiles::Tile) { @@ -543,17 +525,13 @@ impl RenderState { performance::begin_measure!("start_render_loop"); self.reset_canvas(); - self.surfaces.apply_mut( - &[ - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::DropShadows, - SurfaceId::InnerShadows, - ], - |s| { - s.canvas().scale((scale, scale)); - }, - ); + let surface_ids = SurfaceId::Strokes as u32 + | SurfaceId::Fills as u32 + | SurfaceId::DropShadows as u32 + | SurfaceId::InnerShadows as u32; + self.surfaces.apply_mut(surface_ids, |s| { + s.canvas().scale((scale, scale)); + }); let viewbox_cache_size = get_cache_size(self.viewbox, scale); let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox, scale); @@ -572,6 +550,10 @@ impl RenderState { performance::end_measure!("tile_cache"); self.pending_nodes.clear(); + if self.pending_nodes.capacity() < tree.len() { + self.pending_nodes + .reserve(tree.len() - self.pending_nodes.capacity()); + } // reorder by distance to the center. self.current_tile = None; self.render_in_progress = true; @@ -884,7 +866,7 @@ impl RenderState { if !is_empty { self.apply_render_to_final_canvas(tile_rect); } else { - self.surfaces.apply_mut(&[SurfaceId::Target], |s| { + self.surfaces.apply_mut(SurfaceId::Target as u32, |s| { let mut paint = skia::Paint::default(); paint.set_color(self.background_color); s.canvas().draw_rect(tile_rect, &paint); diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index d1c1b513f9..ef479d9d9b 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -1,4 +1,6 @@ +use crate::performance; use crate::shapes::Shape; + use skia_safe::{self as skia, IRect, Paint, RRect}; use super::{gpu_state::GpuState, tiles::Tile, tiles::TileViewbox, tiles::TILE_SIZE}; @@ -12,16 +14,17 @@ const TEXTURES_BATCH_DELETE: usize = 32; // If it's too big it could affect performance. const TILE_SIZE_MULTIPLIER: i32 = 2; +#[repr(u32)] #[derive(Debug, PartialEq, Clone, Copy)] pub enum SurfaceId { - Target, - Cache, - Current, - Fills, - Strokes, - DropShadows, - InnerShadows, - Debug, + Target = 0b0000_0001, + Cache = 0b0000_0010, + Current = 0b0000_0100, + Fills = 0b0000_1000, + Strokes = 0b0001_0000, + DropShadows = 0b0010_0000, + InnerShadows = 0b0100_0000, + Debug = 0b1000_0000, } pub struct Surfaces { @@ -137,11 +140,33 @@ impl Surfaces { .draw(self.canvas(to), (0.0, 0.0), sampling_options, paint); } - pub fn apply_mut(&mut self, ids: &[SurfaceId], mut f: impl FnMut(&mut skia::Surface)) { - for id in ids { - let surface = self.get_mut(*id); - f(surface); + pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) { + performance::begin_measure!("apply_mut::flags"); + if ids & SurfaceId::Target as u32 != 0 { + f(self.get_mut(SurfaceId::Target)); } + if ids & SurfaceId::Current as u32 != 0 { + f(self.get_mut(SurfaceId::Current)); + } + if ids & SurfaceId::Cache as u32 != 0 { + f(self.get_mut(SurfaceId::Cache)); + } + if ids & SurfaceId::Fills as u32 != 0 { + f(self.get_mut(SurfaceId::Fills)); + } + if ids & SurfaceId::Strokes as u32 != 0 { + f(self.get_mut(SurfaceId::Strokes)); + } + if ids & SurfaceId::InnerShadows as u32 != 0 { + f(self.get_mut(SurfaceId::InnerShadows)); + } + if ids & SurfaceId::DropShadows as u32 != 0 { + f(self.get_mut(SurfaceId::DropShadows)); + } + if ids & SurfaceId::Debug as u32 != 0 { + f(self.get_mut(SurfaceId::Debug)); + } + performance::begin_measure!("apply_mut::flags"); } pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) { @@ -150,12 +175,10 @@ impl Surfaces { -render_area.top() + self.margins.height as f32 / scale, ); self.apply_mut( - &[ - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::DropShadows, - SurfaceId::InnerShadows, - ], + SurfaceId::Fills as u32 + | SurfaceId::Strokes as u32 + | SurfaceId::DropShadows as u32 + | SurfaceId::InnerShadows as u32, |s| { s.canvas().restore(); s.canvas().save(); @@ -164,6 +187,7 @@ impl Surfaces { ); } + #[inline] fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface { match id { SurfaceId::Target => &mut self.target, @@ -224,13 +248,11 @@ impl Surfaces { self.canvas(SurfaceId::Strokes).restore_to_count(1); self.canvas(SurfaceId::Current).restore_to_count(1); self.apply_mut( - &[ - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::Current, - SurfaceId::DropShadows, - SurfaceId::InnerShadows, - ], + SurfaceId::Fills as u32 + | SurfaceId::Strokes as u32 + | SurfaceId::Current as u32 + | SurfaceId::DropShadows as u32 + | SurfaceId::InnerShadows as u32, |s| { s.canvas().clear(color).reset_matrix(); }, diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index b719718c51..a51159362b 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -218,15 +218,15 @@ impl Shape { constraint_h: None, constraint_v: None, clip_content: true, - fills: vec![], - strokes: vec![], + fills: Vec::with_capacity(1), + strokes: Vec::with_capacity(1), blend_mode: BlendMode::default(), opacity: 1., hidden: false, blur: Blur::default(), svg: None, svg_attrs: HashMap::new(), - shadows: vec![], + shadows: Vec::with_capacity(1), layout_item: None, extrect: OnceCell::new(), } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 94f8f99734..4607bea8e9 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -1,13 +1,17 @@ use std::collections::{hash_map::Entry, HashMap}; +use std::iter; use skia_safe as skia; +use crate::performance; use crate::render::RenderState; use crate::shapes::Shape; use crate::shapes::StructureEntry; use crate::tiles; use crate::uuid::Uuid; +const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3; + /// A pool allocator for `Shape` objects that attempts to minimize memory reallocations. /// /// `ShapesPool` pre-allocates a contiguous vector of boxed `Shape` instances, @@ -40,16 +44,24 @@ impl ShapesPool { } pub fn initialize(&mut self, capacity: usize) { + performance::begin_measure!("shapes_pool_initialize"); self.counter = 0; - self.shapes = Vec::with_capacity(capacity); - for _ in 0..capacity { - self.shapes.push(Box::new(Shape::new(Uuid::nil()))); + let additional = capacity as i32 - self.shapes.len() as i32; + if additional <= 0 { + return; } + + self.shapes.extend( + iter::repeat_with(|| Box::new(Shape::new(Uuid::nil()))).take(additional as usize), + ); + performance::end_measure!("shapes_pool_initialize"); } pub fn add_shape(&mut self, id: Uuid) -> &mut Shape { if self.counter >= self.shapes.len() { - self.shapes.push(Box::new(Shape::new(Uuid::nil()))); + let additional = (self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize; + self.shapes + .extend(iter::repeat_with(|| Box::new(Shape::new(Uuid::nil()))).take(additional)); } let new_shape = &mut self.shapes[self.counter]; new_shape.id = id;