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