Skip to content

Commit acd0c11

Browse files
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>
1 parent 43c970d commit acd0c11

2 files changed

Lines changed: 103 additions & 68 deletions

File tree

‎.jules/bolt.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@
3232
## 2026-09-22 - String allocation optimization in std.web DSL rendering
3333
**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.
3434
**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.
35+
36+
## 2024-05-24 - Avoid Repeated Array Searches in Rust
37+
**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()`).
38+
**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.

‎stdlib/src/canvas.rs‎

Lines changed: 99 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,29 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) {
2121
if is_dragon {
2222
match dsl.kind.as_str() {
2323
"logo" => {
24-
let text = dsl
25-
.properties
26-
.iter()
27-
.find(|p| p.name == "text")
24+
let mut text_prop = None;
25+
let mut color_prop = None;
26+
let mut size_prop = None;
27+
for p in &dsl.properties {
28+
match p.name.as_str() {
29+
"text" if text_prop.is_none() => text_prop = Some(p),
30+
"color" if color_prop.is_none() => color_prop = Some(p),
31+
"size" if size_prop.is_none() => size_prop = Some(p),
32+
_ => {}
33+
}
34+
if text_prop.is_some() && color_prop.is_some() && size_prop.is_some() {
35+
break;
36+
}
37+
}
38+
let text = text_prop
2839
.and_then(|p| p.value.as_ref())
2940
.map(|v| v.to_string())
3041
.unwrap_or_else(|| "Logo".to_string());
31-
let color = dsl
32-
.properties
33-
.iter()
34-
.find(|p| p.name == "color")
42+
let color = color_prop
3543
.and_then(|p| p.value.as_ref())
3644
.map(|v| v.to_string())
3745
.unwrap_or_else(|| "#00d4ff".to_string());
38-
let size = dsl
39-
.properties
40-
.iter()
41-
.find(|p| p.name == "size")
46+
let size = size_prop
4247
.and_then(|p| p.value.as_ref())
4348
.and_then(|v| v.try_into_int().ok())
4449
.unwrap_or(48);
@@ -82,24 +87,29 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) {
8287
// RENDER OLD GEOMETRIC LOGO
8388
match dsl.kind.as_str() {
8489
"logo" => {
85-
let text = dsl
86-
.properties
87-
.iter()
88-
.find(|p| p.name == "text")
90+
let mut text_prop = None;
91+
let mut color_prop = None;
92+
let mut size_prop = None;
93+
for p in &dsl.properties {
94+
match p.name.as_str() {
95+
"text" if text_prop.is_none() => text_prop = Some(p),
96+
"color" if color_prop.is_none() => color_prop = Some(p),
97+
"size" if size_prop.is_none() => size_prop = Some(p),
98+
_ => {}
99+
}
100+
if text_prop.is_some() && color_prop.is_some() && size_prop.is_some() {
101+
break;
102+
}
103+
}
104+
let text = text_prop
89105
.and_then(|p| p.value.as_ref())
90106
.map(|v| v.to_string())
91107
.unwrap_or_else(|| "Logo".to_string());
92-
let color = dsl
93-
.properties
94-
.iter()
95-
.find(|p| p.name == "color")
108+
let color = color_prop
96109
.and_then(|p| p.value.as_ref())
97110
.map(|v| v.to_string())
98111
.unwrap_or_else(|| "#00d4ff".to_string());
99-
let size = dsl
100-
.properties
101-
.iter()
102-
.find(|p| p.name == "size")
112+
let size = size_prop
103113
.and_then(|p| p.value.as_ref())
104114
.and_then(|v| v.try_into_int().ok())
105115
.unwrap_or(48);
@@ -110,31 +120,35 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) {
110120
);
111121
}
112122
"rings" => {
113-
let count = dsl
114-
.properties
115-
.iter()
116-
.find(|p| p.name == "count")
123+
let mut count_prop = None;
124+
let mut color_prop = None;
125+
let mut size_prop = None;
126+
let mut thickness_prop = None;
127+
for p in &dsl.properties {
128+
match p.name.as_str() {
129+
"count" if count_prop.is_none() => count_prop = Some(p),
130+
"color" if color_prop.is_none() => color_prop = Some(p),
131+
"size" if size_prop.is_none() => size_prop = Some(p),
132+
"thickness" if thickness_prop.is_none() => thickness_prop = Some(p),
133+
_ => {}
134+
}
135+
if count_prop.is_some() && color_prop.is_some() && size_prop.is_some() && thickness_prop.is_some() {
136+
break;
137+
}
138+
}
139+
let count = count_prop
117140
.and_then(|p| p.value.as_ref())
118141
.and_then(|v| v.try_into_int().ok())
119142
.unwrap_or(3);
120-
let color = dsl
121-
.properties
122-
.iter()
123-
.find(|p| p.name == "color")
143+
let color = color_prop
124144
.and_then(|p| p.value.as_ref())
125145
.map(|v| v.to_string())
126146
.unwrap_or_else(|| "#00d4ff".to_string());
127-
let size = dsl
128-
.properties
129-
.iter()
130-
.find(|p| p.name == "size")
147+
let size = size_prop
131148
.and_then(|p| p.value.as_ref())
132149
.and_then(|v| v.try_into_int().ok())
133150
.unwrap_or(40);
134-
let thickness = dsl
135-
.properties
136-
.iter()
137-
.find(|p| p.name == "thickness")
151+
let thickness = thickness_prop
138152
.and_then(|p| p.value.as_ref())
139153
.and_then(|v| v.try_into_int().ok())
140154
.unwrap_or(3);
@@ -149,17 +163,23 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) {
149163
}
150164
}
151165
"emblem" => {
152-
let color = dsl
153-
.properties
154-
.iter()
155-
.find(|p| p.name == "color")
166+
let mut color_prop = None;
167+
let mut size_prop = None;
168+
for p in &dsl.properties {
169+
match p.name.as_str() {
170+
"color" if color_prop.is_none() => color_prop = Some(p),
171+
"size" if size_prop.is_none() => size_prop = Some(p),
172+
_ => {}
173+
}
174+
if color_prop.is_some() && size_prop.is_some() {
175+
break;
176+
}
177+
}
178+
let color = color_prop
156179
.and_then(|p| p.value.as_ref())
157180
.map(|v| v.to_string())
158181
.unwrap_or_else(|| "#0088cc".to_string());
159-
let size = dsl
160-
.properties
161-
.iter()
162-
.find(|p| p.name == "size")
182+
let size = size_prop
163183
.and_then(|p| p.value.as_ref())
164184
.and_then(|v| v.try_into_int().ok())
165185
.unwrap_or(120);
@@ -177,24 +197,29 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) {
177197
);
178198
}
179199
"letter" => {
180-
let ch = dsl
181-
.properties
182-
.iter()
183-
.find(|p| p.name == "char")
200+
let mut char_prop = None;
201+
let mut color_prop = None;
202+
let mut size_prop = None;
203+
for p in &dsl.properties {
204+
match p.name.as_str() {
205+
"char" if char_prop.is_none() => char_prop = Some(p),
206+
"color" if color_prop.is_none() => color_prop = Some(p),
207+
"size" if size_prop.is_none() => size_prop = Some(p),
208+
_ => {}
209+
}
210+
if char_prop.is_some() && color_prop.is_some() && size_prop.is_some() {
211+
break;
212+
}
213+
}
214+
let ch = char_prop
184215
.and_then(|p| p.value.as_ref())
185216
.map(|v| v.to_string())
186217
.unwrap_or_else(|| "T".to_string());
187-
let color = dsl
188-
.properties
189-
.iter()
190-
.find(|p| p.name == "color")
218+
let color = color_prop
191219
.and_then(|p| p.value.as_ref())
192220
.map(|v| v.to_string())
193221
.unwrap_or_else(|| "#0a0e27".to_string());
194-
let size = dsl
195-
.properties
196-
.iter()
197-
.find(|p| p.name == "size")
222+
let size = size_prop
198223
.and_then(|p| p.value.as_ref())
199224
.and_then(|v| v.try_into_int().ok())
200225
.unwrap_or(32);
@@ -205,17 +230,23 @@ fn render_dsl_to_svg(svg: &mut String, val: &RuntimeValue, is_dragon: bool) {
205230
);
206231
}
207232
"core" => {
208-
let color = dsl
209-
.properties
210-
.iter()
211-
.find(|p| p.name == "color")
233+
let mut color_prop = None;
234+
let mut size_prop = None;
235+
for p in &dsl.properties {
236+
match p.name.as_str() {
237+
"color" if color_prop.is_none() => color_prop = Some(p),
238+
"size" if size_prop.is_none() => size_prop = Some(p),
239+
_ => {}
240+
}
241+
if color_prop.is_some() && size_prop.is_some() {
242+
break;
243+
}
244+
}
245+
let color = color_prop
212246
.and_then(|p| p.value.as_ref())
213247
.map(|v| v.to_string())
214248
.unwrap_or_else(|| "#66e0ff".to_string());
215-
let size = dsl
216-
.properties
217-
.iter()
218-
.find(|p| p.name == "size")
249+
let size = size_prop
219250
.and_then(|p| p.value.as_ref())
220251
.and_then(|v| v.try_into_int().ok())
221252
.unwrap_or(40);

0 commit comments

Comments
 (0)