From acd0c116bbf8f3e33b8a7ffbbb68b163d48cb226 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 07:51:12 +0000 Subject: [PATCH] perf: optimize canvas DSL properties lookup Replaces multiple $O(N)$ `.find()` iterations over the properties array with a single pass loop for each canvas element type (logo, rings, emblem, etc.). Preserves correct semantics by stopping at the first match. Co-authored-by: Tcode-Motion <188012755+Tcode-Motion@users.noreply.github.com> --- .jules/bolt.md | 4 ++ stdlib/src/canvas.rs | 167 +++++++++++++++++++++++++------------------ 2 files changed, 103 insertions(+), 68 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 817bbbb5..f86351c7 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -32,3 +32,7 @@ ## 2026-09-22 - String allocation optimization in std.web DSL rendering **Learning:** Generating deep HTML structures in `std.web` heavily penalized performance because `dsl_to_html` allocated and returned a new `String` for every child DSL node. This causes `O(N)` heap allocations and redundant copying in the render tree. By passing a mutable `&mut String` buffer recursively downwards, we avoid all intermediate string heap allocations and significantly improve serialization speed. **Action:** Always prefer using a recursive builder pattern passing a single mutable `&mut String` buffer to `write!` or `push_str` when rendering nested tree structures (like HTML, JSON, or ASTs) instead of returning newly allocated strings at each layer. + +## 2024-05-24 - Avoid Repeated Array Searches in Rust +**Learning:** Multiple `.find()` calls on the same array to extract different properties result in repeated O(N) iterations. Combining them into a single `for` loop that captures all required values significantly improves performance, particularly in hot paths like DSL rendering. However, to preserve the exact semantic behavior of `.find()` (which returns the *first* matching element), you must only assign to variables if they are currently `None` (`if my_prop.is_none()`). +**Action:** When extracting multiple attributes from a collection (like DSL block properties), use a single pass loop and early exit instead of chained `.find()` methods. Always ensure original match semantics are preserved. diff --git a/stdlib/src/canvas.rs b/stdlib/src/canvas.rs index 3c4b3302..d4b1fcaa 100644 --- a/stdlib/src/canvas.rs +++ b/stdlib/src/canvas.rs @@ -21,24 +21,29 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) { if is_dragon { match dsl.kind.as_str() { "logo" => { - let text = dsl - .properties - .iter() - .find(|p| p.name == "text") + let mut text_prop = None; + let mut color_prop = None; + let mut size_prop = None; + for p in &dsl.properties { + match p.name.as_str() { + "text" if text_prop.is_none() => text_prop = Some(p), + "color" if color_prop.is_none() => color_prop = Some(p), + "size" if size_prop.is_none() => size_prop = Some(p), + _ => {} + } + if text_prop.is_some() && color_prop.is_some() && size_prop.is_some() { + break; + } + } + let text = text_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "Logo".to_string()); - let color = dsl - .properties - .iter() - .find(|p| p.name == "color") + let color = color_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "#00d4ff".to_string()); - let size = dsl - .properties - .iter() - .find(|p| p.name == "size") + let size = size_prop .and_then(|p| p.value.as_ref()) .and_then(|v| v.try_into_int().ok()) .unwrap_or(48); @@ -82,24 +87,29 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) { // RENDER OLD GEOMETRIC LOGO match dsl.kind.as_str() { "logo" => { - let text = dsl - .properties - .iter() - .find(|p| p.name == "text") + let mut text_prop = None; + let mut color_prop = None; + let mut size_prop = None; + for p in &dsl.properties { + match p.name.as_str() { + "text" if text_prop.is_none() => text_prop = Some(p), + "color" if color_prop.is_none() => color_prop = Some(p), + "size" if size_prop.is_none() => size_prop = Some(p), + _ => {} + } + if text_prop.is_some() && color_prop.is_some() && size_prop.is_some() { + break; + } + } + let text = text_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "Logo".to_string()); - let color = dsl - .properties - .iter() - .find(|p| p.name == "color") + let color = color_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "#00d4ff".to_string()); - let size = dsl - .properties - .iter() - .find(|p| p.name == "size") + let size = size_prop .and_then(|p| p.value.as_ref()) .and_then(|v| v.try_into_int().ok()) .unwrap_or(48); @@ -110,31 +120,35 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) { ); } "rings" => { - let count = dsl - .properties - .iter() - .find(|p| p.name == "count") + let mut count_prop = None; + let mut color_prop = None; + let mut size_prop = None; + let mut thickness_prop = None; + for p in &dsl.properties { + match p.name.as_str() { + "count" if count_prop.is_none() => count_prop = Some(p), + "color" if color_prop.is_none() => color_prop = Some(p), + "size" if size_prop.is_none() => size_prop = Some(p), + "thickness" if thickness_prop.is_none() => thickness_prop = Some(p), + _ => {} + } + if count_prop.is_some() && color_prop.is_some() && size_prop.is_some() && thickness_prop.is_some() { + break; + } + } + let count = count_prop .and_then(|p| p.value.as_ref()) .and_then(|v| v.try_into_int().ok()) .unwrap_or(3); - let color = dsl - .properties - .iter() - .find(|p| p.name == "color") + let color = color_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "#00d4ff".to_string()); - let size = dsl - .properties - .iter() - .find(|p| p.name == "size") + let size = size_prop .and_then(|p| p.value.as_ref()) .and_then(|v| v.try_into_int().ok()) .unwrap_or(40); - let thickness = dsl - .properties - .iter() - .find(|p| p.name == "thickness") + let thickness = thickness_prop .and_then(|p| p.value.as_ref()) .and_then(|v| v.try_into_int().ok()) .unwrap_or(3); @@ -149,17 +163,23 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) { } } "emblem" => { - let color = dsl - .properties - .iter() - .find(|p| p.name == "color") + let mut color_prop = None; + let mut size_prop = None; + for p in &dsl.properties { + match p.name.as_str() { + "color" if color_prop.is_none() => color_prop = Some(p), + "size" if size_prop.is_none() => size_prop = Some(p), + _ => {} + } + if color_prop.is_some() && size_prop.is_some() { + break; + } + } + let color = color_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "#0088cc".to_string()); - let size = dsl - .properties - .iter() - .find(|p| p.name == "size") + let size = size_prop .and_then(|p| p.value.as_ref()) .and_then(|v| v.try_into_int().ok()) .unwrap_or(120); @@ -177,24 +197,29 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) { ); } "letter" => { - let ch = dsl - .properties - .iter() - .find(|p| p.name == "char") + let mut char_prop = None; + let mut color_prop = None; + let mut size_prop = None; + for p in &dsl.properties { + match p.name.as_str() { + "char" if char_prop.is_none() => char_prop = Some(p), + "color" if color_prop.is_none() => color_prop = Some(p), + "size" if size_prop.is_none() => size_prop = Some(p), + _ => {} + } + if char_prop.is_some() && color_prop.is_some() && size_prop.is_some() { + break; + } + } + let ch = char_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "T".to_string()); - let color = dsl - .properties - .iter() - .find(|p| p.name == "color") + let color = color_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "#0a0e27".to_string()); - let size = dsl - .properties - .iter() - .find(|p| p.name == "size") + let size = size_prop .and_then(|p| p.value.as_ref()) .and_then(|v| v.try_into_int().ok()) .unwrap_or(32); @@ -205,17 +230,23 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) { ); } "core" => { - let color = dsl - .properties - .iter() - .find(|p| p.name == "color") + let mut color_prop = None; + let mut size_prop = None; + for p in &dsl.properties { + match p.name.as_str() { + "color" if color_prop.is_none() => color_prop = Some(p), + "size" if size_prop.is_none() => size_prop = Some(p), + _ => {} + } + if color_prop.is_some() && size_prop.is_some() { + break; + } + } + let color = color_prop .and_then(|p| p.value.as_ref()) .map(|v| v.to_string()) .unwrap_or_else(|| "#66e0ff".to_string()); - let size = dsl - .properties - .iter() - .find(|p| p.name == "size") + let size = size_prop .and_then(|p| p.value.as_ref()) .and_then(|v| v.try_into_int().ok()) .unwrap_or(40);