Merge pull request #6643 from penpot/azazeln28-refactor-performance-improvements

♻️ Minor performance improvements
This commit is contained in:
Alejandro Alonso 2025-06-06 11:38:53 +02:00 committed by GitHub
commit 5e254ff3f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 249 additions and 96 deletions

View File

@ -14,6 +14,35 @@
#?(:clj (= (System/getProperty "penpot.wasm.profile-marks") "true") #?(:clj (= (System/getProperty "penpot.wasm.profile-marks") "true")
:cljs false)) :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 (defmacro begin-measure
[measure-name] [measure-name]
(when enabled? (when enabled?

89
render-wasm/Cargo.lock generated
View File

@ -49,6 +49,12 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.31" version = "1.1.31"
@ -144,8 +150,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -211,6 +219,16 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 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]] [[package]]
name = "khronos_api" name = "khronos_api"
version = "3.1.0" version = "3.1.0"
@ -293,6 +311,12 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -395,6 +419,12 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -558,6 +588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -566,6 +597,64 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"

View File

@ -26,7 +26,7 @@ skia-safe = { version = "0.81.0", default-features = false, features = [
"textlayout", "textlayout",
"binary-cache", "binary-cache",
] } ] }
uuid = { version = "1.11.0", features = ["v4"] } uuid = { version = "1.11.0", features = ["v4", "js"] }
[profile.release] [profile.release]
opt-level = "s" opt-level = "s"

View File

@ -6,11 +6,30 @@ else
export _BUILD_MODE=${1:-debug}; export _BUILD_MODE=${1:-debug};
fi 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 \ EMCC_CFLAGS="--no-entry \
--js-library src/js/wapi.js \ --js-library src/js/wapi.js \
-sASSERTIONS=1 \ -sASSERTIONS=1 \
-sALLOW_TABLE_GROWTH=1 \ -sALLOW_TABLE_GROWTH=1 \
-sALLOW_MEMORY_GROWTH=1 \ -sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_HEAP=$EM_INITIAL_HEAP \
-sMEMORY_GROWTH_GEOMETRIC_STEP=$EM_MEMORY_GROWTH_GEOMETRIC_STEP \
-sENVIRONMENT=web \ -sENVIRONMENT=web \
-sERROR_ON_UNDEFINED_SYMBOLS=0 \ -sERROR_ON_UNDEFINED_SYMBOLS=0 \
-sMAX_WEBGL_VERSION=2 \ -sMAX_WEBGL_VERSION=2 \
@ -31,7 +50,7 @@ else
# -gseparate-dwarf # -gseparate-dwarf
# -gsplit-dwarf # -gsplit-dwarf
# -gsource-map # -gsource-map
EMCC_CFLAGS="-g $EMCC_CFLAGS -sVERBOSE=1 -sMALLOC=emmalloc-debug" EMCC_CFLAGS="-g $EMCC_CFLAGS -sVERBOSE=1 -sMALLOC=$EM_MALLOC"
fi fi
export EMCC_CFLAGS; export EMCC_CFLAGS;

View File

@ -279,18 +279,14 @@ impl RenderState {
Some(&skia::Paint::default()), 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( self.surfaces.apply_mut(surface_ids, |s| {
&[ s.canvas().clear(skia::Color::TRANSPARENT);
SurfaceId::DropShadows, });
SurfaceId::InnerShadows,
SurfaceId::Fills,
SurfaceId::Strokes,
],
|s| {
s.canvas().clear(skia::Color::TRANSPARENT);
},
);
} }
pub fn render_shape( pub fn render_shape(
@ -306,12 +302,10 @@ impl RenderState {
shape shape
}; };
let surface_ids = &[ let surface_ids = SurfaceId::Strokes as u32
SurfaceId::Fills, | SurfaceId::Fills as u32
SurfaceId::Strokes, | SurfaceId::DropShadows as u32
SurfaceId::DropShadows, | SurfaceId::InnerShadows as u32;
SurfaceId::InnerShadows,
];
self.surfaces.apply_mut(surface_ids, |s| { self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().save(); s.canvas().save();
}); });
@ -391,17 +385,13 @@ impl RenderState {
} }
Type::Text(text_content) => { Type::Text(text_content) => {
self.surfaces.apply_mut( let surface_ids = SurfaceId::Strokes as u32
&[ | SurfaceId::Fills as u32
SurfaceId::Fills, | SurfaceId::DropShadows as u32
SurfaceId::Strokes, | SurfaceId::InnerShadows as u32;
SurfaceId::DropShadows, self.surfaces.apply_mut(surface_ids, |s| {
SurfaceId::InnerShadows, s.canvas().concat(&matrix);
], });
|s| {
s.canvas().concat(&matrix);
},
);
let text_content = text_content.new_bounds(shape.selrect()); let text_content = text_content.new_bounds(shape.selrect());
let paragraphs = text_content.get_skia_paragraphs(self.fonts.font_collection()); let paragraphs = text_content.get_skia_paragraphs(self.fonts.font_collection());
@ -437,17 +427,13 @@ impl RenderState {
shadows::render_text_inner_shadows(self, &shape, &paragraphs, antialias); shadows::render_text_inner_shadows(self, &shape, &paragraphs, antialias);
} }
_ => { _ => {
self.surfaces.apply_mut( let surface_ids = SurfaceId::Strokes as u32
&[ | SurfaceId::Fills as u32
SurfaceId::Fills, | SurfaceId::DropShadows as u32
SurfaceId::Strokes, | SurfaceId::InnerShadows as u32;
SurfaceId::DropShadows, self.surfaces.apply_mut(surface_ids, |s| {
SurfaceId::InnerShadows, s.canvas().concat(&matrix);
], });
|s| {
s.canvas().concat(&matrix);
},
);
for fill in shape.fills().rev() { for fill in shape.fills().rev() {
fills::render(self, &shape, fill, antialias); fills::render(self, &shape, fill, antialias);
@ -465,17 +451,13 @@ impl RenderState {
}; };
self.apply_drawing_to_render_canvas(Some(&shape)); self.apply_drawing_to_render_canvas(Some(&shape));
self.surfaces.apply_mut( let surface_ids = SurfaceId::Strokes as u32
&[ | SurfaceId::Fills as u32
SurfaceId::Fills, | SurfaceId::DropShadows as u32
SurfaceId::Strokes, | SurfaceId::InnerShadows as u32;
SurfaceId::DropShadows, self.surfaces.apply_mut(surface_ids, |s| {
SurfaceId::InnerShadows, s.canvas().restore();
], });
|s| {
s.canvas().restore();
},
);
} }
pub fn update_render_context(&mut self, tile: tiles::Tile) { pub fn update_render_context(&mut self, tile: tiles::Tile) {
@ -543,17 +525,13 @@ impl RenderState {
performance::begin_measure!("start_render_loop"); performance::begin_measure!("start_render_loop");
self.reset_canvas(); self.reset_canvas();
self.surfaces.apply_mut( let surface_ids = SurfaceId::Strokes as u32
&[ | SurfaceId::Fills as u32
SurfaceId::Fills, | SurfaceId::DropShadows as u32
SurfaceId::Strokes, | SurfaceId::InnerShadows as u32;
SurfaceId::DropShadows, self.surfaces.apply_mut(surface_ids, |s| {
SurfaceId::InnerShadows, s.canvas().scale((scale, scale));
], });
|s| {
s.canvas().scale((scale, scale));
},
);
let viewbox_cache_size = get_cache_size(self.viewbox, scale); let viewbox_cache_size = get_cache_size(self.viewbox, scale);
let cached_viewbox_cache_size = get_cache_size(self.cached_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"); performance::end_measure!("tile_cache");
self.pending_nodes.clear(); 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. // reorder by distance to the center.
self.current_tile = None; self.current_tile = None;
self.render_in_progress = true; self.render_in_progress = true;
@ -884,7 +866,7 @@ impl RenderState {
if !is_empty { if !is_empty {
self.apply_render_to_final_canvas(tile_rect); self.apply_render_to_final_canvas(tile_rect);
} else { } else {
self.surfaces.apply_mut(&[SurfaceId::Target], |s| { self.surfaces.apply_mut(SurfaceId::Target as u32, |s| {
let mut paint = skia::Paint::default(); let mut paint = skia::Paint::default();
paint.set_color(self.background_color); paint.set_color(self.background_color);
s.canvas().draw_rect(tile_rect, &paint); s.canvas().draw_rect(tile_rect, &paint);

View File

@ -1,4 +1,6 @@
use crate::performance;
use crate::shapes::Shape; use crate::shapes::Shape;
use skia_safe::{self as skia, IRect, Paint, RRect}; use skia_safe::{self as skia, IRect, Paint, RRect};
use super::{gpu_state::GpuState, tiles::Tile, tiles::TileViewbox, tiles::TILE_SIZE}; 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. // If it's too big it could affect performance.
const TILE_SIZE_MULTIPLIER: i32 = 2; const TILE_SIZE_MULTIPLIER: i32 = 2;
#[repr(u32)]
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId { pub enum SurfaceId {
Target, Target = 0b0000_0001,
Cache, Cache = 0b0000_0010,
Current, Current = 0b0000_0100,
Fills, Fills = 0b0000_1000,
Strokes, Strokes = 0b0001_0000,
DropShadows, DropShadows = 0b0010_0000,
InnerShadows, InnerShadows = 0b0100_0000,
Debug, Debug = 0b1000_0000,
} }
pub struct Surfaces { pub struct Surfaces {
@ -137,11 +140,33 @@ impl Surfaces {
.draw(self.canvas(to), (0.0, 0.0), sampling_options, paint); .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)) { pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
for id in ids { performance::begin_measure!("apply_mut::flags");
let surface = self.get_mut(*id); if ids & SurfaceId::Target as u32 != 0 {
f(surface); 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) { 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, -render_area.top() + self.margins.height as f32 / scale,
); );
self.apply_mut( self.apply_mut(
&[ SurfaceId::Fills as u32
SurfaceId::Fills, | SurfaceId::Strokes as u32
SurfaceId::Strokes, | SurfaceId::DropShadows as u32
SurfaceId::DropShadows, | SurfaceId::InnerShadows as u32,
SurfaceId::InnerShadows,
],
|s| { |s| {
s.canvas().restore(); s.canvas().restore();
s.canvas().save(); s.canvas().save();
@ -164,6 +187,7 @@ impl Surfaces {
); );
} }
#[inline]
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface { fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
match id { match id {
SurfaceId::Target => &mut self.target, SurfaceId::Target => &mut self.target,
@ -224,13 +248,11 @@ impl Surfaces {
self.canvas(SurfaceId::Strokes).restore_to_count(1); self.canvas(SurfaceId::Strokes).restore_to_count(1);
self.canvas(SurfaceId::Current).restore_to_count(1); self.canvas(SurfaceId::Current).restore_to_count(1);
self.apply_mut( self.apply_mut(
&[ SurfaceId::Fills as u32
SurfaceId::Fills, | SurfaceId::Strokes as u32
SurfaceId::Strokes, | SurfaceId::Current as u32
SurfaceId::Current, | SurfaceId::DropShadows as u32
SurfaceId::DropShadows, | SurfaceId::InnerShadows as u32,
SurfaceId::InnerShadows,
],
|s| { |s| {
s.canvas().clear(color).reset_matrix(); s.canvas().clear(color).reset_matrix();
}, },

View File

@ -218,15 +218,15 @@ impl Shape {
constraint_h: None, constraint_h: None,
constraint_v: None, constraint_v: None,
clip_content: true, clip_content: true,
fills: vec![], fills: Vec::with_capacity(1),
strokes: vec![], strokes: Vec::with_capacity(1),
blend_mode: BlendMode::default(), blend_mode: BlendMode::default(),
opacity: 1., opacity: 1.,
hidden: false, hidden: false,
blur: Blur::default(), blur: Blur::default(),
svg: None, svg: None,
svg_attrs: HashMap::new(), svg_attrs: HashMap::new(),
shadows: vec![], shadows: Vec::with_capacity(1),
layout_item: None, layout_item: None,
extrect: OnceCell::new(), extrect: OnceCell::new(),
} }

View File

@ -1,13 +1,17 @@
use std::collections::{hash_map::Entry, HashMap}; use std::collections::{hash_map::Entry, HashMap};
use std::iter;
use skia_safe as skia; use skia_safe as skia;
use crate::performance;
use crate::render::RenderState; use crate::render::RenderState;
use crate::shapes::Shape; use crate::shapes::Shape;
use crate::shapes::StructureEntry; use crate::shapes::StructureEntry;
use crate::tiles; use crate::tiles;
use crate::uuid::Uuid; use crate::uuid::Uuid;
const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3;
/// A pool allocator for `Shape` objects that attempts to minimize memory reallocations. /// A pool allocator for `Shape` objects that attempts to minimize memory reallocations.
/// ///
/// `ShapesPool` pre-allocates a contiguous vector of boxed `Shape` instances, /// `ShapesPool` pre-allocates a contiguous vector of boxed `Shape` instances,
@ -40,16 +44,24 @@ impl ShapesPool {
} }
pub fn initialize(&mut self, capacity: usize) { pub fn initialize(&mut self, capacity: usize) {
performance::begin_measure!("shapes_pool_initialize");
self.counter = 0; self.counter = 0;
self.shapes = Vec::with_capacity(capacity); let additional = capacity as i32 - self.shapes.len() as i32;
for _ in 0..capacity { if additional <= 0 {
self.shapes.push(Box::new(Shape::new(Uuid::nil()))); 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 { pub fn add_shape(&mut self, id: Uuid) -> &mut Shape {
if self.counter >= self.shapes.len() { 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]; let new_shape = &mut self.shapes[self.counter];
new_shape.id = id; new_shape.id = id;