Skip to main content

zdc_bench/
table.rs

1//! The committed results table.
2//!
3//! Generated from the measurements rather than typed, and compared against
4//! `BENCHMARKS.md` by a test, so a number in the repository that disagrees
5//! with the code is a build failure (§14A.4). Regenerate with
6//! `ZDC_BLESS=1 cargo test -p zdc-bench`.
7
8use 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
38/// A table of one counter across every arm and every step.
39fn 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(&divider(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
57/// The counter-by-counter breakdown of one step.
58fn 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(&divider(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
102/// What one row costs, which is the number that scales.
103fn 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(&divider(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            // One decimal place, formatted by hand so the table stays
132            // integer-exact where the number is an integer.
133            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(&divider(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(&divider(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
179/// Moves per reorder, both reconcilers, at every shape and size.
180///
181/// One counter and two arms, laid out the other way round from the tables
182/// above: the row names the shape and the size, because what is in
183/// question here is how the count grows with the list rather than how two
184/// emissions compare on one list.
185fn 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(&divider(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
214/// The whole generated region of `BENCHMARKS.md`.
215pub 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}