|
6 | 6 | //! `--runs N` with median selection by ops/sec. Emits a single line of JSON on |
7 | 7 | //! stdout, `{"parse": {<size>: [row, ...]}, "render": {<size>: [row, ...]}}`, |
8 | 8 | //! where each row is `{"name", "opsPerSec", "avgMs", "throughputMBs", |
9 | | -//! "samples": [...]}` — the same row shape the JS tables consume. |
| 9 | +//! "samples": [...]}`, the same row shape the JS tables consume. |
10 | 10 |
|
11 | 11 | // This standalone binary sits outside the root cargo workspace, but clippy |
12 | 12 | // still discovers the repository clippy.toml. Its disallowed std types, |
|
16 | 16 | // out wholesale here. |
17 | 17 | #![allow(clippy::disallowed_macros, clippy::disallowed_methods, clippy::disallowed_types)] |
18 | 18 |
|
19 | | -use pulldown_cmark::{html, Parser}; |
20 | | -use std::fmt::Write as _; |
| 19 | +mod bench; |
| 20 | +mod json; |
| 21 | + |
21 | 22 | use std::hint::black_box; |
22 | 23 | use std::process::ExitCode; |
23 | | -use std::time::Instant; |
| 24 | + |
| 25 | +use pulldown_cmark::{html, Parser}; |
| 26 | + |
| 27 | +use crate::bench::bench; |
| 28 | +use crate::json::{render_json, SuiteResults}; |
24 | 29 |
|
25 | 30 | /// Byte-for-byte copy of `sampleMarkdown` in `parse-benchmark-bun.mjs`, |
26 | 31 | /// including the leading and trailing newline. The JS harness derives |
@@ -67,30 +72,6 @@ Final paragraph with `inline code` and more text. |
67 | 72 | const SIZES: [(&str, usize, u32); 4] = |
68 | 73 | [("small", 1, 100), ("medium", 10, 50), ("large", 100, 20), ("huge", 2150, 5)]; |
69 | 74 |
|
70 | | -/// Untimed calls before each timed loop, matching the JS harness. |
71 | | -const WARMUP_CALLS: u32 = 5; |
72 | | - |
73 | | -/// One timed measurement, in the exact field units the JS harness reports. |
74 | | -#[derive(Clone, Copy)] |
75 | | -struct Measurement { |
76 | | - ops_per_sec: f64, |
77 | | - avg_ms: f64, |
78 | | - throughput_mbs: f64, |
79 | | -} |
80 | | - |
81 | | -/// One output row: the median measurement plus every per-run sample. |
82 | | -struct Row { |
83 | | - name: &'static str, |
84 | | - median: Measurement, |
85 | | - samples: Vec<Measurement>, |
86 | | -} |
87 | | - |
88 | | -/// Per-suite results as `(size name, rows)` in harness size order. |
89 | | -struct SuiteResults { |
90 | | - parse: Vec<(&'static str, Vec<Row>)>, |
91 | | - render: Vec<(&'static str, Vec<Row>)>, |
92 | | -} |
93 | | - |
94 | 75 | enum CliAction { |
95 | 76 | Run { runs: u32 }, |
96 | 77 | Help, |
@@ -221,114 +202,6 @@ fn run_benchmarks(sizes: &[(&'static str, usize, u32)], runs: u32) -> SuiteResul |
221 | 202 | SuiteResults { parse, render } |
222 | 203 | } |
223 | 204 |
|
224 | | -fn bench( |
225 | | - name: &'static str, |
226 | | - mut op: impl FnMut(), |
227 | | - iterations: u32, |
228 | | - runs: u32, |
229 | | - input_bytes: usize, |
230 | | -) -> Row { |
231 | | - let samples: Vec<Measurement> = |
232 | | - (0..runs).map(|_| measure_once(&mut op, iterations, input_bytes)).collect(); |
233 | | - Row { name, median: median_by_ops(&samples), samples } |
234 | | -} |
235 | | - |
236 | | -fn measure_once(op: &mut dyn FnMut(), iterations: u32, input_bytes: usize) -> Measurement { |
237 | | - for _ in 0..WARMUP_CALLS { |
238 | | - op(); |
239 | | - } |
240 | | - let start = Instant::now(); |
241 | | - for _ in 0..iterations { |
242 | | - op(); |
243 | | - } |
244 | | - let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0; |
245 | | - let avg_ms = elapsed_ms / f64::from(iterations); |
246 | | - let ops_per_sec = 1000.0 / avg_ms; |
247 | | - Measurement { |
248 | | - ops_per_sec, |
249 | | - avg_ms, |
250 | | - throughput_mbs: (input_bytes as f64 / 1024.0 / 1024.0) * ops_per_sec, |
251 | | - } |
252 | | -} |
253 | | - |
254 | | -/// Median by ops/sec with the JS harness' index choice — |
255 | | -/// `sorted[Math.floor(sorted.length / 2)]`, the upper middle for even counts. |
256 | | -fn median_by_ops(samples: &[Measurement]) -> Measurement { |
257 | | - let mut sorted: Vec<Measurement> = samples.to_vec(); |
258 | | - sorted.sort_by(|a, b| a.ops_per_sec.total_cmp(&b.ops_per_sec)); |
259 | | - sorted[sorted.len() / 2] |
260 | | -} |
261 | | - |
262 | | -fn render_json(results: &SuiteResults) -> String { |
263 | | - let mut out = String::new(); |
264 | | - out.push('{'); |
265 | | - for (suite_index, (suite_name, sizes)) in |
266 | | - [("parse", &results.parse), ("render", &results.render)].into_iter().enumerate() |
267 | | - { |
268 | | - if suite_index > 0 { |
269 | | - out.push(','); |
270 | | - } |
271 | | - let _ = write!(out, "\"{suite_name}\":{{"); |
272 | | - for (size_index, (size_name, rows)) in sizes.iter().enumerate() { |
273 | | - if size_index > 0 { |
274 | | - out.push(','); |
275 | | - } |
276 | | - let _ = write!(out, "\"{size_name}\":["); |
277 | | - for (row_index, row) in rows.iter().enumerate() { |
278 | | - if row_index > 0 { |
279 | | - out.push(','); |
280 | | - } |
281 | | - push_json_row(&mut out, row); |
282 | | - } |
283 | | - out.push(']'); |
284 | | - } |
285 | | - out.push('}'); |
286 | | - } |
287 | | - out.push('}'); |
288 | | - out |
289 | | -} |
290 | | - |
291 | | -fn push_json_row(out: &mut String, row: &Row) { |
292 | | - // Row names are fixed ASCII constants without `"` or `\`, so they embed |
293 | | - // into JSON without an escaping pass. |
294 | | - debug_assert!(!row.name.contains(['"', '\\'])); |
295 | | - out.push_str("{\"name\":\""); |
296 | | - out.push_str(row.name); |
297 | | - out.push_str("\","); |
298 | | - push_json_measurement_fields(out, &row.median); |
299 | | - out.push_str(",\"samples\":["); |
300 | | - for (index, sample) in row.samples.iter().enumerate() { |
301 | | - if index > 0 { |
302 | | - out.push(','); |
303 | | - } |
304 | | - out.push('{'); |
305 | | - push_json_measurement_fields(out, sample); |
306 | | - out.push('}'); |
307 | | - } |
308 | | - out.push_str("]}"); |
309 | | -} |
310 | | - |
311 | | -fn push_json_measurement_fields(out: &mut String, measurement: &Measurement) { |
312 | | - out.push_str("\"opsPerSec\":"); |
313 | | - push_json_number(out, measurement.ops_per_sec); |
314 | | - out.push_str(",\"avgMs\":"); |
315 | | - push_json_number(out, measurement.avg_ms); |
316 | | - out.push_str(",\"throughputMBs\":"); |
317 | | - push_json_number(out, measurement.throughput_mbs); |
318 | | -} |
319 | | - |
320 | | -fn push_json_number(out: &mut String, value: f64) { |
321 | | - // Rust's shortest-roundtrip float formatting is a valid JSON number for |
322 | | - // finite values (never scientific notation, no NaN/inf spellings). The |
323 | | - // non-finite arm is unreachable for real measurements but keeps the |
324 | | - // output parseable no matter what. |
325 | | - if value.is_finite() { |
326 | | - let _ = write!(out, "{value}"); |
327 | | - } else { |
328 | | - out.push('0'); |
329 | | - } |
330 | | -} |
331 | | - |
332 | 205 | #[cfg(test)] |
333 | 206 | mod tests { |
334 | 207 | use super::*; |
@@ -392,18 +265,6 @@ mod tests { |
392 | 265 | } |
393 | 266 | } |
394 | 267 |
|
395 | | - #[test] |
396 | | - fn median_matches_js_harness_selection() { |
397 | | - let measurement = |
398 | | - |ops: f64| Measurement { ops_per_sec: ops, avg_ms: 0.0, throughput_mbs: 0.0 }; |
399 | | - // Even count: sorted ops are [1, 2, 3, 4]; Math.floor(4 / 2) = index 2. |
400 | | - let even = [measurement(4.0), measurement(1.0), measurement(3.0), measurement(2.0)]; |
401 | | - assert_eq!(median_by_ops(&even).ops_per_sec, 3.0); |
402 | | - // Odd count: sorted ops are [1, 2, 3]; Math.floor(3 / 2) = index 1. |
403 | | - let odd = [measurement(3.0), measurement(1.0), measurement(2.0)]; |
404 | | - assert_eq!(median_by_ops(&odd).ops_per_sec, 2.0); |
405 | | - } |
406 | | - |
407 | 268 | #[test] |
408 | 269 | fn sample_matches_js_harness_byte_for_byte() { |
409 | 270 | // The whole protocol hangs on both harnesses timing the same bytes: |
|
0 commit comments