1use serde_json::json;
6
7use crate::types::NestedNode;
8
9pub fn bench_tree(n: usize) -> NestedNode {
11 let cards = n.div_ceil(7).max(1);
12 let mut children = Vec::with_capacity(cards + 1);
13 children.push(json!({"id": "heading", "component": "Text", "props": {"text": "Bench", "variant": "title"}}));
14 for i in 0..cards {
15 children.push(json!({
16 "id": format!("card-{i}"), "component": "Box",
17 "style": {"display": "flex", "flexDirection": "column", "gap": 8, "padding": 12, "borderRadius": 8, "backgroundColor": "$color.card"},
18 "children": [
19 {"id": format!("card-{i}-title"), "component": "Text", "props": {"text": format!("Card {i} · a title that may wrap"), "variant": "label", "lines": 1}},
20 {"id": format!("card-{i}-body"), "component": "Text", "props": {"text": "Body copy with enough words to wrap at a phone width and exercise the second measure phase.", "variant": "muted"}},
21 {"id": format!("card-{i}-row"), "component": "Box", "style": {"display": "flex", "flexDirection": "row", "gap": 8, "alignItems": "center", "flexWrap": "wrap"},
22 "children": [
23 {"id": format!("card-{i}-pill-a"), "component": "Button", "props": {"label": "Alpha", "variant": "outline", "size": "sm"}},
24 {"id": format!("card-{i}-pill-b"), "component": "Button", "props": {"label": "Beta", "variant": "outline", "size": "sm"}},
25 {"id": format!("card-{i}-go"), "component": "Button", "props": {"label": "Open", "variant": "default"}, "style": {"marginLeft": "auto"}}
26 ]}
27 ]
28 }));
29 }
30 let tree = json!({
31 "id": "root", "component": "Box",
32 "style": {"display": "grid", "gridTemplateColumns": "1fr", "gap": 12, "padding": 16, "width": "100%", "@media (min-width: 600px)": {"gridTemplateColumns": "repeat(3, minmax(0, 1fr))"}},
33 "children": children
34 });
35 serde_json::from_value::<NestedNode>(tree).expect("bench tree")
36}
37
38pub fn bench_list_tree(rows: usize) -> NestedNode {
42 let items: Vec<serde_json::Value> = (0..rows)
43 .map(|i| {
44 json!({
45 "id": format!("row-{i}"), "component": "Box", "props": {"pressable": true},
46 "style": {"display": "flex", "flexDirection": "row", "alignItems": "center", "gap": 8, "paddingHorizontal": 12, "paddingVertical": 8, ":hover": {"backgroundColor": "$color.accent"}},
47 "children": [
48 {"id": format!("row-{i}-icon"), "component": "Icon", "props": {"name": "ui-check", "size": "sm"}},
49 {"id": format!("row-{i}-text"), "component": "Box", "style": {"display": "flex", "flexDirection": "column", "flexGrow": 1, "minWidth": 0},
50 "children": [
51 {"id": format!("row-{i}-title"), "component": "Text", "props": {"text": format!("Issue {i}: a title long enough to wrap on a phone"), "variant": "label", "lines": 1}},
52 {"id": format!("row-{i}-sub"), "component": "Text", "props": {"text": "Updated a minute ago", "variant": "muted"}}
53 ]},
54 {"id": format!("row-{i}-badge"), "component": "Badge", "props": {"text": "Todo", "variant": "secondary"}}
55 ]
56 })
57 })
58 .collect();
59 let tree = json!({
60 "id": "root", "component": "Box", "style": {"display": "flex", "flexDirection": "column", "gap": 12, "padding": 16, "width": "100%"},
61 "children": [
62 {"id": "title", "component": "Text", "props": {"text": "Inbox", "variant": "title"}},
63 {"id": "filters", "component": "Box", "style": {"display": "flex", "flexDirection": "row", "gap": 8},
64 "children": [
65 {"id": "f-all", "component": "Button", "props": {"label": "All", "variant": "secondary", "size": "sm"}},
66 {"id": "f-mine", "component": "Button", "props": {"label": "Mine", "variant": "ghost", "size": "sm"}}
67 ]},
68 {"id": "list", "component": "List", "props": {"divided": true}, "style": {"height": 640}, "children": items}
69 ]
70 });
71 serde_json::from_value::<NestedNode>(tree).expect("bench list tree")
72}
73
74pub fn bench_tree_json(n: usize) -> String {
76 serde_json::to_string(&serde_json::to_value(bench_tree(n)).expect("json")).expect("json")
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::measure::FixedMeasure;
83 use crate::surface::{Surface, SurfaceOptions};
84
85 #[test]
86 fn two_hundred_nodes_lay_out_in_three_upcalls_and_well_under_two_milliseconds() {
87 let mut surface = Surface::new("bench", SurfaceOptions::default());
88 let outcome = surface.set_nested(bench_tree(200));
89 assert!(outcome.issues.is_empty(), "{:?}", outcome.issues);
90 surface.set_viewport(390.0, 0.0, None);
91 let mut measure = FixedMeasure { sizes: Default::default(), wrap: true };
92 let first = surface.layout(&mut measure);
93 assert!(surface.node_count() >= 200, "{} nodes", surface.node_count());
94 assert!(first.upcalls <= 3, "{} upcalls", first.upcalls);
95 surface.set_viewport(900.0, 0.0, None);
98 let wide = surface.layout(&mut measure);
99 assert!(wide.upcalls <= 2);
100 let mut best = u64::MAX;
101 for i in 0..20 {
102 surface.set_viewport(if i % 2 == 0 { 390.0 } else { 900.0 }, 0.0, None);
103 let out = surface.layout(&mut measure);
104 best = best.min(out.layout_ns);
105 }
106 assert!(best < 50_000_000, "best pass {best} ns");
109 eprintln!("bench 200 nodes: {} layout nodes, best pass {} µs (debug)", surface.node_count(), best / 1000);
110 }
111
112 #[test]
113 fn a_2000_row_list_scrolls_and_churns_hover_in_bounded_work() {
114 let mut surface = Surface::new("bench-list", SurfaceOptions::default());
115 assert!(surface.set_nested(bench_list_tree(2_000)).issues.is_empty());
116 surface.set_viewport(390.0, 844.0, None);
117 let mut measure = FixedMeasure { sizes: Default::default(), wrap: true };
118 for _ in 0..3 {
119 surface.layout(&mut measure);
120 }
121 let live = surface.node_count();
122 assert!(live < 600, "{live} live nodes for 2,000 rows");
123 let mut worst_restyle = 0;
124 for step in 0..40u32 {
125 surface.scroll("list", step as f32 * 300.0);
126 let out = surface.layout(&mut measure);
127 assert!(out.upcalls <= 3);
128 worst_restyle = worst_restyle.max(out.restyled);
129 let row = format!("row-{}", step * 5 + 3);
130 if surface.index_of(&row).is_some() {
131 surface.set_states(&row, vec!["hover".into()]);
132 let hover = surface.layout(&mut measure);
133 assert!(hover.restyled <= 2, "a hover restyled {}", hover.restyled);
134 assert!(!hover.rebuilt);
135 surface.set_states(&row, vec![]);
136 }
137 }
138 assert!(worst_restyle < 200, "a scroll step restyled {worst_restyle}");
139 }
140}