1#![expect(
4 clippy::unwrap_used,
5 clippy::panic,
6 clippy::indexing_slicing,
7 clippy::arithmetic_side_effects,
8 reason = "benchmark harness: bad arguments or a parse failure abort the run"
9)]
10
11use formualizer_parse::{ASTNode, parse, parser::BatchParser};
12use std::{hint::black_box, time::Instant};
13
14#[expect(
18 dead_code,
19 reason = "fallback for the baseline builder; the guarded builder's inherent method shadows it"
20)]
21trait BaselineCacheCapacity: Sized {
22 fn cache_capacity(self, _entries: usize, _bytes: usize) -> Self {
23 self
24 }
25}
26impl<T> BaselineCacheCapacity for T {}
27
28struct Case {
29 name: &'static str,
30 formulas: Vec<String>,
31 iterations: usize,
32 batch: bool,
33}
34
35fn corpus() -> Vec<Case> {
36 let distinct = |f: fn(usize) -> String| (1..=1024).map(f).collect();
37 let mut nested = "A1".to_owned();
38 for n in 1..=12 {
39 nested = format!("IF(B{n}>0,{nested},0)");
40 }
41 let wide = format!(
42 "=SUM({})",
43 (1..=64)
44 .map(|i| format!("A{i}"))
45 .collect::<Vec<_>>()
46 .join(",")
47 );
48 let array = format!(
49 "={{{}}}",
50 (0..8)
51 .map(|r| (0..8)
52 .map(|c| (r * 8 + c).to_string())
53 .collect::<Vec<_>>()
54 .join(","))
55 .collect::<Vec<_>>()
56 .join(";")
57 );
58 let chain = format!(
59 "={}",
60 (1..=128)
61 .map(|i| format!("A{i}"))
62 .collect::<Vec<_>>()
63 .join("+")
64 );
65 vec![
66 Case {
67 name: "short_distinct",
68 formulas: distinct(|r| format!("=A{r}+B{r}*$C$1")),
69 iterations: 300_000,
70 batch: false,
71 },
72 Case {
73 name: "finance_arithmetic",
74 formulas: distinct(|r| format!("=($B$2*C{r}+D{r})*(1+$E$1)^$F$1")),
75 iterations: 200_000,
76 batch: false,
77 },
78 Case {
79 name: "lookup_aggregate",
80 formulas: distinct(|r| {
81 format!(
82 "=IFERROR(INDEX(Data!$D$2:$D$1000,MATCH(A{r},Data!$A$2:$A$1000,0)),SUMIFS($E$2:$E$1000,$B$2:$B$1000,B{r}))"
83 )
84 }),
85 iterations: 75_000,
86 batch: false,
87 },
88 Case {
89 name: "structured_quoted_unicode",
90 formulas: vec![
91 "=SUM(Table1[[#Data],[Amount]])+'Jan 24:Mar 24'!B5".into(),
92 "=IF(Table1[@[Amount]]>0,'Données été'!$A$1,0)".into(),
93 ],
94 iterations: 100_000,
95 batch: false,
96 },
97 Case {
98 name: "nested_if_12",
99 formulas: vec![format!("={nested}")],
100 iterations: 30_000,
101 batch: false,
102 },
103 Case {
104 name: "wide_sum_64",
105 formulas: vec![wide],
106 iterations: 30_000,
107 batch: false,
108 },
109 Case {
110 name: "array_8x8",
111 formulas: vec![array],
112 iterations: 30_000,
113 batch: false,
114 },
115 Case {
116 name: "left_chain_128",
117 formulas: vec![chain],
118 iterations: 10_000,
119 batch: false,
120 },
121 Case {
122 name: "powers_parenthesized",
123 formulas: vec!["=2^3^2+2^(3^2)+(-2)^2".into()],
124 iterations: 200_000,
125 batch: false,
126 },
127 Case {
128 name: "batch_repeat_64",
129 formulas: (1..=64).map(|r| format!("=A{r}+B{r}*$C$1")).collect(),
130 iterations: 300_000,
131 batch: true,
132 },
133 Case {
134 name: "batch_workingset_4096",
135 formulas: (1..=4096).map(|r| format!("=A{r}+B{r}*$C$1")).collect(),
136 iterations: 300_000,
137 batch: true,
138 },
139 Case {
140 name: "batch_unique_stream",
141 formulas: (1..=50_000).map(|r| format!("=A{r}+B{r}*$C$1")).collect(),
142 iterations: 50_000,
143 batch: true,
144 },
145 ]
146}
147
148fn semantic_digest(formulas: &[String]) -> u64 {
149 let mut h = 0xcbf2_9ce4_8422_2325_u64;
151 for formula in formulas {
152 let ast = parse(formula).unwrap_or_else(|e| panic!("{formula}: {e}"));
153 h ^= ast.fingerprint();
154 h = h.wrapping_mul(0x100_0000_01b3);
155 }
156 h
157}
158
159fn consume(ast: ASTNode) {
160 black_box(&ast);
161 drop(ast);
162}
163
164fn main() {
165 println!("workload,iterations,sources,elapsed_ns,ns_per_parse,digest");
166 for c in corpus() {
167 let digest = semantic_digest(&c.formulas);
168 let builder = BatchParser::builder();
169 let builder = if let Ok(entries) = std::env::var("BENCH_CACHE_ENTRIES") {
170 builder.cache_capacity(entries.parse().unwrap(), usize::MAX)
171 } else {
172 builder
173 };
174 let mut batch = builder.build();
175 let unique = c.name == "batch_unique_stream";
178 let warm_count = if unique {
179 4096
180 } else {
181 c.formulas.len().max(4096)
182 };
183 for i in 0..warm_count {
184 let other = unique.then(|| format!("=X{i}+Y{i}*$C$1"));
185 let f = other
186 .as_deref()
187 .unwrap_or(&c.formulas[i % c.formulas.len()]);
188 consume(if c.batch {
189 batch.parse(f).unwrap()
190 } else {
191 parse(f).unwrap()
192 });
193 }
194 let start = Instant::now();
195 for i in 0..c.iterations {
196 let f = black_box(&c.formulas[i % c.formulas.len()]);
197 consume(if c.batch {
198 batch.parse(f).unwrap()
199 } else {
200 parse(f).unwrap()
201 });
202 }
203 let elapsed = start.elapsed().as_nanos();
204 println!(
205 "{},{},{},{},{:.3},{:016x}",
206 c.name,
207 c.iterations,
208 c.formulas.len(),
209 elapsed,
210 elapsed as f64 / c.iterations as f64,
211 digest
212 );
213 }
214}