From 8922e7454f174d55f758b74ca81a3c2381375747 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Wed, 4 Jun 2025 13:53:27 +0200 Subject: [PATCH] :recycle: Refactor some allocations --- frontend/src/app/render_wasm/api.cljs | 11 ++- frontend/src/app/render_wasm/api.js | 7 ++ frontend/src/app/render_wasm/performance.cljc | 29 ++++++ render-wasm/Cargo.lock | 89 +++++++++++++++++++ render-wasm/Cargo.toml | 4 +- render-wasm/_build_env | 21 ++++- render-wasm/src/render.rs | 3 + render-wasm/src/shapes.rs | 6 +- render-wasm/src/state.rs | 25 +++++- 9 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 frontend/src/app/render_wasm/api.js diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index dbaac9252e..c11799dce6 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -778,7 +778,13 @@ (defn set-objects [objects] (perf/begin-measure "set-objects") - (let [shapes (into [] (vals objects)) + #_(do + (api-js/setObjects objects set-object) + (clear-drawing-cache) + (request-render "set-objects") + (perf/end-measure "set-objects")) + (let [get-memory-measure (perf/memory-measure) + shapes (into [] (vals objects)) total-shapes (count shapes) pending (loop [index 0 pending []] @@ -786,7 +792,8 @@ (let [shape (nth shapes index) pending' (set-object objects shape)] (recur (inc index) (into pending pending'))) - pending))] + pending)) + _ (js/console.log (clj->js (get-memory-measure)))] (perf/end-measure "set-objects") (clear-drawing-cache) (request-render "set-objects") diff --git a/frontend/src/app/render_wasm/api.js b/frontend/src/app/render_wasm/api.js new file mode 100644 index 0000000000..0667a34fda --- /dev/null +++ b/frontend/src/app/render_wasm/api.js @@ -0,0 +1,7 @@ +export function setObject(object) { + console.log(object.id) +} + +export function setObjects(objects, fn) { + objects.forEach((object) => fn(objects, object)) +} 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..9d786c471a 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -7,7 +7,7 @@ license-file = "../LICENSE" description = "Wasm-based canvas renderer for Penpot" [features] -default = [] +default = ["profile"] profile = ["profile-macros", "profile-raf"] profile-macros = [] profile-raf = [] @@ -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..6eb76d5f43 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -572,6 +572,9 @@ 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; 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..0e5c9691e9 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,29 @@ 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;