1pub mod recipe;
52pub mod util;
53
54pub use recipe::{benchmark, BenchParams, Recipe};
55pub use util::Lcg;
56
57use std::collections::BTreeMap;
58use std::fmt::Write as _;
59use std::io::{self, Write};
60use std::time::{Instant, SystemTime, UNIX_EPOCH};
61
62pub struct Stage {
64 name: String,
65 samples: Vec<u64>,
66}
67
68impl Stage {
69 fn new(name: &str, capacity: usize) -> Self {
70 Self {
71 name: name.to_string(),
72 samples: Vec::with_capacity(capacity),
73 }
74 }
75 pub fn record(&mut self, ns: u64) {
77 self.samples.push(ns);
78 }
79 pub fn time<F: FnOnce() -> R, R>(&mut self, f: F) -> R {
81 let t0 = Instant::now();
82 let r = f();
83 self.samples.push(t0.elapsed().as_nanos() as u64);
84 r
85 }
86 pub fn name(&self) -> &str {
87 &self.name
88 }
89 pub fn samples(&self) -> &[u64] {
90 &self.samples
91 }
92}
93
94pub struct PerfHarness {
96 workload: String,
97 lang: String,
98 inputs: BTreeMap<String, String>,
99 meta: BTreeMap<String, String>,
100 stages: Vec<Stage>,
101}
102
103impl PerfHarness {
104 pub fn new(workload: &str, lang: &str) -> Self {
105 Self {
106 workload: workload.to_string(),
107 lang: lang.to_string(),
108 inputs: BTreeMap::new(),
109 meta: BTreeMap::new(),
110 stages: Vec::new(),
111 }
112 }
113
114 pub fn input(&mut self, key: &str, value: &str) -> &mut Self {
116 self.inputs.insert(key.to_string(), value.to_string());
117 self
118 }
119
120 pub fn meta(&mut self, key: &str, value: &str) -> &mut Self {
122 self.meta.insert(key.to_string(), value.to_string());
123 self
124 }
125
126 pub fn stage(&mut self, name: &str, capacity: usize) -> &mut Stage {
130 self.stages.push(Stage::new(name, capacity));
131 self.stages.last_mut().unwrap()
132 }
133
134 pub fn stage_mut(&mut self, name: &str) -> Option<&mut Stage> {
136 self.stages.iter_mut().find(|s| s.name == name)
137 }
138
139 pub fn write_json<W: Write>(&self, out: &mut W) -> io::Result<()> {
141 let mut s = String::with_capacity(64 * 1024);
142 s.push('{');
143 json_kv_str(&mut s, "workload", &self.workload);
144 s.push(',');
145 json_kv_str(&mut s, "lang", &self.lang);
146 s.push(',');
147 json_kv_str(&mut s, "timestamp", &iso8601_now());
148 s.push(',');
149 s.push_str("\"inputs\":");
150 json_map(&mut s, &self.inputs);
151 s.push(',');
152 s.push_str("\"meta\":");
153 json_map(&mut s, &self.meta);
154 s.push(',');
155 s.push_str("\"stages\":{");
156 for (i, stage) in self.stages.iter().enumerate() {
157 if i > 0 {
158 s.push(',');
159 }
160 json_str(&mut s, &stage.name);
161 s.push(':');
162 json_stage(&mut s, &stage.samples);
163 }
164 s.push_str("}}");
165 out.write_all(s.as_bytes())?;
166 out.write_all(b"\n")?;
167 Ok(())
168 }
169
170 pub fn discard_stage(&mut self, name: &str) {
172 self.stages.retain(|s| s.name != name);
173 }
174}
175
176fn json_str(out: &mut String, s: &str) {
177 out.push('"');
178 for c in s.chars() {
179 match c {
180 '"' => out.push_str("\\\""),
181 '\\' => out.push_str("\\\\"),
182 '\n' => out.push_str("\\n"),
183 '\r' => out.push_str("\\r"),
184 '\t' => out.push_str("\\t"),
185 c if (c as u32) < 0x20 => {
186 let _ = write!(out, "\\u{:04x}", c as u32);
187 }
188 c => out.push(c),
189 }
190 }
191 out.push('"');
192}
193
194fn json_kv_str(out: &mut String, k: &str, v: &str) {
195 json_str(out, k);
196 out.push(':');
197 json_str(out, v);
198}
199
200fn json_map(out: &mut String, m: &BTreeMap<String, String>) {
201 out.push('{');
202 for (i, (k, v)) in m.iter().enumerate() {
203 if i > 0 {
204 out.push(',');
205 }
206 json_kv_str(out, k, v);
207 }
208 out.push('}');
209}
210
211fn json_stage(out: &mut String, samples: &[u64]) {
212 let mut sorted = samples.to_vec();
213 sorted.sort_unstable();
214 let n = sorted.len();
215 let p = |q: f64| -> u64 {
216 if n == 0 {
217 return 0;
218 }
219 let idx = ((q * n as f64) as usize).min(n - 1);
220 sorted[idx]
221 };
222 let max = sorted.last().copied().unwrap_or(0);
223 let mean = if n == 0 {
224 0
225 } else {
226 sorted.iter().sum::<u64>() / n as u64
227 };
228
229 out.push('{');
230 let _ = write!(out, "\"count\":{},", n);
231 let _ = write!(out, "\"p50_ns\":{},", p(0.50));
232 let _ = write!(out, "\"p99_ns\":{},", p(0.99));
233 let _ = write!(out, "\"p999_ns\":{},", p(0.999));
234 let _ = write!(out, "\"max_ns\":{},", max);
235 let _ = write!(out, "\"mean_ns\":{},", mean);
236 out.push_str("\"samples_ns\":[");
239 let step = (n / 500).max(1);
240 let mut first = true;
241 for i in (0..n).step_by(step) {
242 if !first {
243 out.push(',');
244 }
245 first = false;
246 let _ = write!(out, "{}", samples[i]);
247 }
248 out.push_str("]}");
249}
250
251fn iso8601_now() -> String {
252 let d = SystemTime::now()
253 .duration_since(UNIX_EPOCH)
254 .unwrap_or_default();
255 let secs = d.as_secs() as i64;
256 let mut year = 1970i64;
257 let mut days = secs / 86_400;
258 let rem = secs % 86_400;
259 let hour = rem / 3600;
260 let minute = (rem % 3600) / 60;
261 let second = rem % 60;
262 while days >= year_days(year) {
263 days -= year_days(year);
264 year += 1;
265 }
266 let mut month = 1u32;
267 for m in 1..=12 {
268 let dm = month_days(year, m);
269 if days < dm as i64 {
270 month = m;
271 break;
272 }
273 days -= dm as i64;
274 }
275 let day = (days + 1) as u32;
276 format!(
277 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
278 year, month, day, hour, minute, second
279 )
280}
281
282fn year_days(y: i64) -> i64 {
283 if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
284 366
285 } else {
286 365
287 }
288}
289fn month_days(y: i64, m: u32) -> u32 {
290 match m {
291 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
292 4 | 6 | 9 | 11 => 30,
293 2 => {
294 if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
295 29
296 } else {
297 28
298 }
299 }
300 _ => 0,
301 }
302}
303
304pub fn read_stdin_kv() -> BTreeMap<String, String> {
306 use std::io::BufRead;
307 let mut m = BTreeMap::new();
308 let stdin = io::stdin();
309 for line in stdin.lock().lines().map_while(Result::ok) {
310 let line = line.trim();
311 if line.is_empty() || line.starts_with('#') {
312 continue;
313 }
314 if let Some((k, v)) = line.split_once('=') {
315 m.insert(k.trim().to_string(), v.trim().to_string());
316 }
317 }
318 m
319}