1use crate::sizes::{bundle_sizes, runtime_sizes};
9use crate::Report;
10
11pub const START_MARKER: &str = "<!-- generated: benchmark results -->";
12pub const END_MARKER: &str = "<!-- end generated -->";
13
14const ARM_LABELS: &[(&str, &str)] = &[
15 ("zd-positional", "ZDeceptron (positional keys, today)"),
16 ("zd-identity", "ZDeceptron (identity keys, with `unique`)"),
17 ("direct", "Direct emission (rejected design)"),
18 ("vanilla", "Vanilla JS (node by node)"),
19 ("vanilla-tuned", "Vanilla JS (hand-tuned)"),
20];
21
22fn label(arm: &str) -> &str {
23 ARM_LABELS
24 .iter()
25 .find(|(name, _)| *name == arm)
26 .map(|(_, label)| *label)
27 .unwrap_or(arm)
28}
29
30fn row(cells: &[String]) -> String {
31 format!("| {} |\n", cells.join(" | "))
32}
33
34fn divider(columns: usize) -> String {
35 format!("|{}\n", "---|".repeat(columns))
36}
37
38fn matrix(report: &Report, title: &str, note: &str, key: &str) -> String {
40 let arms = report.arms();
41 let mut out = format!("### {title}\n\n{note}\n\n");
42 let mut header = vec!["Operation".to_string()];
43 header.extend(arms.iter().map(|arm| label(arm).to_string()));
44 out.push_str(&row(&header));
45 out.push_str(÷r(header.len()));
46 for step in report.steps() {
47 let mut cells = vec![step.to_string()];
48 for arm in &arms {
49 cells.push(report.find(arm, step).get(key).to_string());
50 }
51 out.push_str(&row(&cells));
52 }
53 out.push('\n');
54 out
55}
56
57fn breakdown(report: &Report, step: &str) -> String {
59 let arms = report.arms();
60 let counters = [
61 ("cloneNode", "cross.cloneNode"),
62 ("createElement", "cross.createElement"),
63 ("createTextNode", "cross.createTextNode"),
64 ("createComment", "cross.createComment"),
65 ("insertBefore", "cross.insertBefore"),
66 ("removeChild", "cross.removeChild"),
67 ("replaceChildren", "cross.replaceChildren"),
68 ("setAttribute", "cross.setAttribute"),
69 ("addEventListener", "cross.addEventListener"),
70 ("text writes", "cross.textWrite"),
71 ("**crossings, total**", "crossings"),
72 ("nodes allocated", "work.createElement"),
73 ("effects created", "reactive.effect"),
74 ("effect runs", "reactive.effectRun"),
75 ("signals created", "reactive.signal"),
76 ];
77
78 let mut out = format!("### `{step}` — every counter\n\n");
79 let mut header = vec!["Counter".to_string()];
80 header.extend(arms.iter().map(|arm| label(arm).to_string()));
81 out.push_str(&row(&header));
82 out.push_str(÷r(header.len()));
83 for (title, key) in counters {
84 let mut cells = vec![title.to_string()];
85 for arm in &arms {
86 let measurement = report.find(arm, step);
87 let value = if key == "work.createElement" {
88 measurement.get("work.createElement")
89 + measurement.get("work.createTextNode")
90 + measurement.get("work.createComment")
91 } else {
92 measurement.get(key)
93 };
94 cells.push(value.to_string());
95 }
96 out.push_str(&row(&cells));
97 }
98 out.push('\n');
99 out
100}
101
102fn per_row(report: &Report, step: &str) -> String {
104 let arms = report.arms();
105 let mut out = format!("### What one row costs, at `{step}`\n\n");
106 let mut header = vec!["Per row".to_string()];
107 header.extend(arms.iter().map(|arm| label(arm).to_string()));
108 out.push_str(&row(&header));
109 out.push_str(÷r(header.len()));
110
111 let rows = [
112 ("DOM crossings", "crossings"),
113 ("nodes allocated", "nodes"),
114 ("effects created", "reactive.effect"),
115 ("event listeners", "cross.addEventListener"),
116 ("attribute writes", "cross.setAttribute"),
117 ("text writes", "cross.textWrite"),
118 ];
119 for (title, key) in rows {
120 let mut cells = vec![title.to_string()];
121 for arm in &arms {
122 let measurement = report.find(arm, step);
123 let count = measurement.get("rows").max(1);
124 let value = if key == "nodes" {
125 measurement.get("work.createElement")
126 + measurement.get("work.createTextNode")
127 + measurement.get("work.createComment")
128 } else {
129 measurement.get(key)
130 };
131 let tenths = (value * 10 + count / 2) / count;
134 cells.push(if tenths % 10 == 0 {
135 (tenths / 10).to_string()
136 } else {
137 format!("{}.{}", tenths / 10, tenths % 10)
138 });
139 }
140 out.push_str(&row(&cells));
141 }
142 out.push('\n');
143 out
144}
145
146fn sizes() -> String {
147 let mut out = String::from("### Bundle size, in bytes\n\n");
148 out.push_str(&row(&[
149 "Program".to_string(),
150 "client.js".to_string(),
151 "boot.js".to_string(),
152 "styles.css".to_string(),
153 "index.html".to_string(),
154 "manifest.json".to_string(),
155 "total".to_string(),
156 ]));
157 out.push_str(÷r(7));
158 for size in bundle_sizes() {
159 out.push_str(&row(&[
160 format!("`{}`", size.name),
161 size.client_js.to_string(),
162 size.boot_js.to_string(),
163 size.styles_css.to_string(),
164 size.index_html.to_string(),
165 size.manifest_json.to_string(),
166 size.total().to_string(),
167 ]));
168 }
169 out.push('\n');
170 out.push_str(&row(&["Runtime file".to_string(), "bytes".to_string()]));
171 out.push_str(÷r(2));
172 for (name, bytes) in runtime_sizes() {
173 out.push_str(&row(&[format!("`{name}`"), bytes.to_string()]));
174 }
175 out.push('\n');
176 out
177}
178
179fn reorders(reorder: &Report) -> String {
186 let mut out = String::from("### Moves per reorder\n\n");
187 out.push_str(
188 "`insertBefore` calls one reorder makes. Every row in this measurement has exactly one \
189 root, so a move is one call and the count is the size of the move set rather than a \
190 proxy for it. **`cursor walk`** is the placement pass `eachInto` used before the \
191 longest-increasing-subsequence reconciler landed; it is kept as an arm so that the \
192 change is measured rather than remembered, and the two arms are checked for having \
193 produced the same order.\n\n",
194 );
195 out.push_str(&row(&[
196 "Reorder".to_string(),
197 "moves, LIS reconciler".to_string(),
198 "moves, cursor walk (before)".to_string(),
199 "rows retired".to_string(),
200 ]));
201 out.push_str(÷r(4));
202 for step in reorder.steps() {
203 out.push_str(&row(&[
204 step.to_string(),
205 reorder.find("lis", step).get("moves").to_string(),
206 reorder.find("cursor", step).get("moves").to_string(),
207 reorder.find("lis", step).get("removals").to_string(),
208 ]));
209 }
210 out.push('\n');
211 out
212}
213
214pub fn generated_section(report: &Report, reorder: &Report) -> String {
216 let mut out = String::new();
217 out.push_str(&matrix(
218 report,
219 "DOM crossings per operation",
220 "Calls from JavaScript into the DOM. Work performed *inside* one call \
221 — the subtree `cloneNode(true)` allocates, the children inserting a \
222 fragment links, the removals `replaceChildren()` performs — is not a \
223 further crossing; it is counted as work below.",
224 "crossings",
225 ));
226 out.push_str(&matrix(
227 report,
228 "Effect runs per operation",
229 "A binding re-running. Zero for the vanilla arms, which have no \
230 bindings. This is the number that says whether a list operation \
231 touched only what changed.",
232 "reactive.effectRun",
233 ));
234 out.push_str(&matrix(
235 report,
236 "Text-node writes per operation",
237 "`nodeValue` writes that actually reached a text node. `bindText` \
238 compares before writing (§16.2 R7), so a re-run that computes the \
239 same string costs an effect run and no write.",
240 "cross.textWrite",
241 ));
242 out.push_str(&reorders(reorder));
243 out.push_str(&per_row(report, "create 10,000 rows"));
244 out.push_str(&breakdown(report, "create 10,000 rows"));
245 out.push_str(&breakdown(report, "update every 10th row"));
246 out.push_str(&sizes());
247 out
248}