Skip to main content

zdc_bench/
lib.rs

1#![forbid(unsafe_code)]
2
3//! The benchmark suite §14A.4 makes a deliverable.
4//!
5//! **What this measures.** Operation counts, not time. The workload runs in
6//! a pure-Rust JavaScript interpreter embedded in a `cargo test`, and a
7//! wall-clock number from there is not comparable to a browser — reporting
8//! one as if it were would be dishonest. What *is* comparable is how many
9//! times each arm crosses into the DOM, how many nodes it allocates, how
10//! many effects it creates, and how many times those effects re-run. Those
11//! counts are the same in this interpreter as in V8, because they are a
12//! property of the emitted code rather than of the engine.
13//!
14//! **What it cannot measure.** React and SolidJS. §14A.4 asks for both, and
15//! both need a package manager: CI has no network and §8 forbids a Node
16//! dependency. The arms that stand in their place are a *direct-emission*
17//! generator (the design §16.1 rejected) and two hand-written vanilla
18//! implementations, one naive and one tuned. Nothing here should be read as
19//! a measurement against React or Solid.
20//!
21//! **The gap.** `each` in the view is refused by this compiler (§16.5,
22//! M5b), so the workload's list cannot be written in ZDeceptron today. The
23//! row body in `js/benchmark.js` is the compiler's own emission for
24//! `bench/row.zd` — `tests/fidelity.rs` proves it, and fails the build if
25//! it drifts — but the `eachInto` around it is written by hand. See
26//! `BENCHMARKS.md`.
27
28use std::collections::BTreeMap;
29
30use boa_engine::{Context, Source};
31
32mod scaling;
33mod shape;
34mod sizes;
35mod table;
36
37pub use scaling::{
38    build, code_lines, deepest_fold, linked_runtime_bytes, linked_runtime_bytes_in,
39    program_with_components, program_with_depth, program_with_roots, program_with_signals,
40    program_without_components, runtime_js_bytes, survey, template_bytes, time_graph_passes,
41    Emitted, GraphTimes, FOREIGN_VIEW_PROGRAM, NULL_PROGRAM, SMALLEST_PROGRAM,
42    SWIFT_BYTES_PER_LINE, SWIFT_LARGEST_APP_JS, SWIFT_LARGEST_APP_LINES, SWIFT_NULL_PROGRAM_JS,
43    SWIFT_NULL_PROGRAM_LINES,
44};
45pub use shape::{benchmark_row, emitted_row, RowShape};
46pub use sizes::{bundle_sizes, compile, repository_path, try_compile, BundleSize};
47pub use table::{generated_section, END_MARKER, START_MARKER};
48
49/// Counting instrumentation layered over the runtime's DOM shim.
50pub const INSTRUMENT_JS: &str = include_str!("../js/instrument.js");
51
52/// The workload: five arms, ten operations, one DOM.
53pub const BENCHMARK_JS: &str = include_str!("../js/benchmark.js");
54
55/// Reordering, counted: two reconcilers, four shapes, three sizes.
56///
57/// Separate from the workload above because it answers a different
58/// question. The workload asks what a list operation costs; this asks what
59/// the cost is a *function of*, which is the only form a claim about a
60/// reconciler's order of growth can take (§16.10, issue #207).
61pub const REORDER_JS: &str = include_str!("../js/reorder.js");
62
63/// The minimal DOM the runtime's own tests run against.
64///
65/// Re-exported from where it lives rather than copied. A second copy with
66/// counters in it would drift, and the benchmark would then be measuring a
67/// DOM nothing else in the repository runs against.
68pub use zdc_runtime::DOM_SHIM_JS;
69
70/// One js-framework-benchmark row, in ZDeceptron.
71pub const ROW_ZD: &str = include_str!("../bench/row.zd");
72
73/// One arm's counts for one operation.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Measurement {
76    pub arm: String,
77    pub step: String,
78    pub fields: BTreeMap<String, i64>,
79}
80
81impl Measurement {
82    /// A counter's value, or zero — the report omits zeroes so a hundred
83    /// columns of nothing do not drown the numbers that matter.
84    pub fn get(&self, key: &str) -> i64 {
85        self.fields.get(key).copied().unwrap_or(0)
86    }
87}
88
89/// Every measurement, in the order the workload produced them.
90pub struct Report(pub Vec<Measurement>);
91
92impl Report {
93    pub fn find(&self, arm: &str, step: &str) -> &Measurement {
94        self.0
95            .iter()
96            .find(|m| m.arm == arm && m.step == step)
97            .unwrap_or_else(|| panic!("no measurement for `{arm}` / `{step}`"))
98    }
99
100    pub fn arms(&self) -> Vec<&str> {
101        let mut out: Vec<&str> = Vec::new();
102        for measurement in &self.0 {
103            if !out.contains(&measurement.arm.as_str()) {
104                out.push(&measurement.arm);
105            }
106        }
107        out
108    }
109
110    pub fn steps(&self) -> Vec<&str> {
111        let mut out: Vec<&str> = Vec::new();
112        for measurement in &self.0 {
113            if !out.contains(&measurement.step.as_str()) {
114                out.push(&measurement.step);
115            }
116        }
117        out
118    }
119}
120
121/// Remove ES module syntax so the shipped sources can be evaluated as one
122/// script, exactly as the runtime's own tests do.
123fn flatten(source: &str) -> String {
124    source
125        .lines()
126        .filter(|line| !line.trim_start().starts_with("import "))
127        .map(|line| line.strip_prefix("export ").unwrap_or(line))
128        .collect::<Vec<_>>()
129        .join("\n")
130}
131
132/// Evaluate the runtime and the counters, then one measuring script.
133fn measure(what: &str, script: &str) -> Report {
134    let mut context = Context::default();
135    let sources = [
136        ("dom shim", DOM_SHIM_JS.to_string()),
137        ("signal.js", flatten(zdc_runtime::SIGNAL_JS)),
138        ("dom.js", flatten(zdc_runtime::DOM_JS)),
139        ("markup.js", flatten(zdc_runtime::MARKUP_JS)),
140        ("list.js", flatten(zdc_runtime::LIST_JS)),
141        ("elements.js", flatten(zdc_runtime::ELEMENTS_JS)),
142        ("instrument.js", INSTRUMENT_JS.to_string()),
143    ];
144    for (name, source) in sources {
145        context
146            .eval(Source::from_bytes(source.as_bytes()))
147            .unwrap_or_else(|e| panic!("{name} failed to evaluate: {e}"));
148    }
149
150    let report = context
151        .eval(Source::from_bytes(script.as_bytes()))
152        .unwrap_or_else(|e| panic!("{what} failed: {e}"))
153        .to_string(&mut context)
154        .expect("a measuring script returns a string")
155        .to_std_string_escaped();
156
157    Report(parse(&report))
158}
159
160/// Run the workload and collect every arm's counts.
161pub fn run() -> Report {
162    measure("the workload", BENCHMARK_JS)
163}
164
165/// Count the moves a reorder costs, at three sizes and in four shapes.
166///
167/// Its own context rather than a further arm of [`run`]: the workload's
168/// arms all render the same row and are compared against each other, and
169/// an arm that reordered a different list at a different size would not be
170/// comparable to any of them.
171pub fn run_reorder() -> Report {
172    measure("the reorder measurement", REORDER_JS)
173}
174
175fn parse(report: &str) -> Vec<Measurement> {
176    let mut out = Vec::new();
177    for line in report.lines() {
178        let Some(rest) = line.strip_prefix("RESULT\t") else {
179            continue;
180        };
181        let mut parts = rest.split('\t');
182        let arm = parts.next().expect("a result line names its arm");
183        let step = parts.next().expect("a result line names its step");
184        let fields = parts.next().expect("a result line carries fields");
185
186        let mut counts = BTreeMap::new();
187        for field in fields.split(',') {
188            let (key, value) = field
189                .split_once('=')
190                .unwrap_or_else(|| panic!("malformed field `{field}`"));
191            let value: i64 = value
192                .parse()
193                .unwrap_or_else(|e| panic!("`{field}` is not a number: {e}"));
194            counts.insert(key.to_string(), value);
195        }
196        out.push(Measurement {
197            arm: arm.to_string(),
198            step: step.to_string(),
199            fields: counts,
200        });
201    }
202    assert!(
203        !out.is_empty(),
204        "the workload produced no measurements — it probably did not run"
205    );
206    out
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn a_result_line_parses_into_counters() {
215        let parsed = parse("RESULT\tzd\tcreate\trows=2,cross.cloneNode=2\nnoise\n");
216        assert_eq!(parsed.len(), 1);
217        assert_eq!(parsed[0].arm, "zd");
218        assert_eq!(parsed[0].step, "create");
219        assert_eq!(parsed[0].get("cross.cloneNode"), 2);
220        assert_eq!(parsed[0].get("absent"), 0);
221    }
222
223    #[test]
224    fn flatten_strips_module_syntax_and_nothing_else() {
225        let flattened = flatten("import { a } from './b.js';\nexport function c() {}\n  indented");
226        assert_eq!(flattened, "function c() {}\n  indented");
227    }
228}