Skip to content

Commit 2979381

Browse files
refactor(bench): split native-competitors main.rs under file line limit (#514)
The file line limit CI check caps new source files at 350 lines, and benchmarks/native-competitors/src/main.rs landed at 426. Extract the timing engine into bench.rs and the JSON serialization into json.rs as sibling modules (mod.rs is disallowed), leaving main.rs at 287 lines. Pure move: no behavior change; cargo test/clippy/fmt all pass. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
1 parent eb661ae commit 2979381

3 files changed

Lines changed: 168 additions & 148 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! Timing engine shared by every competitor row: warmup, timed loops, and
2+
//! per-run median selection, in the exact field units the JS harness reports.
3+
4+
use std::time::Instant;
5+
6+
/// Untimed calls before each timed loop, matching the JS harness.
7+
const WARMUP_CALLS: u32 = 5;
8+
9+
/// One timed measurement, in the exact field units the JS harness reports.
10+
#[derive(Clone, Copy)]
11+
pub struct Measurement {
12+
pub ops_per_sec: f64,
13+
pub avg_ms: f64,
14+
pub throughput_mbs: f64,
15+
}
16+
17+
/// One output row: the median measurement plus every per-run sample.
18+
pub struct Row {
19+
pub name: &'static str,
20+
pub median: Measurement,
21+
pub samples: Vec<Measurement>,
22+
}
23+
24+
pub fn bench(
25+
name: &'static str,
26+
mut op: impl FnMut(),
27+
iterations: u32,
28+
runs: u32,
29+
input_bytes: usize,
30+
) -> Row {
31+
let samples: Vec<Measurement> =
32+
(0..runs).map(|_| measure_once(&mut op, iterations, input_bytes)).collect();
33+
Row { name, median: median_by_ops(&samples), samples }
34+
}
35+
36+
fn measure_once(op: &mut dyn FnMut(), iterations: u32, input_bytes: usize) -> Measurement {
37+
for _ in 0..WARMUP_CALLS {
38+
op();
39+
}
40+
let start = Instant::now();
41+
for _ in 0..iterations {
42+
op();
43+
}
44+
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
45+
let avg_ms = elapsed_ms / f64::from(iterations);
46+
let ops_per_sec = 1000.0 / avg_ms;
47+
Measurement {
48+
ops_per_sec,
49+
avg_ms,
50+
throughput_mbs: (input_bytes as f64 / 1024.0 / 1024.0) * ops_per_sec,
51+
}
52+
}
53+
54+
/// Median by ops/sec with the JS harness' index choice:
55+
/// `sorted[Math.floor(sorted.length / 2)]`, the upper middle for even counts.
56+
fn median_by_ops(samples: &[Measurement]) -> Measurement {
57+
let mut sorted: Vec<Measurement> = samples.to_vec();
58+
sorted.sort_by(|a, b| a.ops_per_sec.total_cmp(&b.ops_per_sec));
59+
sorted[sorted.len() / 2]
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use super::*;
65+
66+
#[test]
67+
fn median_matches_js_harness_selection() {
68+
let measurement =
69+
|ops: f64| Measurement { ops_per_sec: ops, avg_ms: 0.0, throughput_mbs: 0.0 };
70+
// Even count: sorted ops are [1, 2, 3, 4]; Math.floor(4 / 2) = index 2.
71+
let even = [measurement(4.0), measurement(1.0), measurement(3.0), measurement(2.0)];
72+
assert_eq!(median_by_ops(&even).ops_per_sec, 3.0);
73+
// Odd count: sorted ops are [1, 2, 3]; Math.floor(3 / 2) = index 1.
74+
let odd = [measurement(3.0), measurement(1.0), measurement(2.0)];
75+
assert_eq!(median_by_ops(&odd).ops_per_sec, 2.0);
76+
}
77+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! JSON serialization of the suite results into the exact row shape the JS
2+
//! benchmark tables consume: `{"parse": {<size>: [row, ...]}, "render": ...}`.
3+
4+
use std::fmt::Write as _;
5+
6+
use crate::bench::{Measurement, Row};
7+
8+
/// Per-suite results as `(size name, rows)` in harness size order.
9+
pub struct SuiteResults {
10+
pub parse: Vec<(&'static str, Vec<Row>)>,
11+
pub render: Vec<(&'static str, Vec<Row>)>,
12+
}
13+
14+
pub fn render_json(results: &SuiteResults) -> String {
15+
let mut out = String::new();
16+
out.push('{');
17+
for (suite_index, (suite_name, sizes)) in
18+
[("parse", &results.parse), ("render", &results.render)].into_iter().enumerate()
19+
{
20+
if suite_index > 0 {
21+
out.push(',');
22+
}
23+
let _ = write!(out, "\"{suite_name}\":{{");
24+
for (size_index, (size_name, rows)) in sizes.iter().enumerate() {
25+
if size_index > 0 {
26+
out.push(',');
27+
}
28+
let _ = write!(out, "\"{size_name}\":[");
29+
for (row_index, row) in rows.iter().enumerate() {
30+
if row_index > 0 {
31+
out.push(',');
32+
}
33+
push_json_row(&mut out, row);
34+
}
35+
out.push(']');
36+
}
37+
out.push('}');
38+
}
39+
out.push('}');
40+
out
41+
}
42+
43+
fn push_json_row(out: &mut String, row: &Row) {
44+
// Row names are fixed ASCII constants without `"` or `\`, so they embed
45+
// into JSON without an escaping pass.
46+
debug_assert!(!row.name.contains(['"', '\\']));
47+
out.push_str("{\"name\":\"");
48+
out.push_str(row.name);
49+
out.push_str("\",");
50+
push_json_measurement_fields(out, &row.median);
51+
out.push_str(",\"samples\":[");
52+
for (index, sample) in row.samples.iter().enumerate() {
53+
if index > 0 {
54+
out.push(',');
55+
}
56+
out.push('{');
57+
push_json_measurement_fields(out, sample);
58+
out.push('}');
59+
}
60+
out.push_str("]}");
61+
}
62+
63+
fn push_json_measurement_fields(out: &mut String, measurement: &Measurement) {
64+
out.push_str("\"opsPerSec\":");
65+
push_json_number(out, measurement.ops_per_sec);
66+
out.push_str(",\"avgMs\":");
67+
push_json_number(out, measurement.avg_ms);
68+
out.push_str(",\"throughputMBs\":");
69+
push_json_number(out, measurement.throughput_mbs);
70+
}
71+
72+
fn push_json_number(out: &mut String, value: f64) {
73+
// Rust's shortest-roundtrip float formatting is a valid JSON number for
74+
// finite values (never scientific notation, no NaN/inf spellings). The
75+
// non-finite arm is unreachable for real measurements but keeps the
76+
// output parseable no matter what.
77+
if value.is_finite() {
78+
let _ = write!(out, "{value}");
79+
} else {
80+
out.push('0');
81+
}
82+
}

benchmarks/native-competitors/src/main.rs

Lines changed: 9 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! `--runs N` with median selection by ops/sec. Emits a single line of JSON on
77
//! stdout, `{"parse": {<size>: [row, ...]}, "render": {<size>: [row, ...]}}`,
88
//! 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.
1010
1111
// This standalone binary sits outside the root cargo workspace, but clippy
1212
// still discovers the repository clippy.toml. Its disallowed std types,
@@ -16,11 +16,16 @@
1616
// out wholesale here.
1717
#![allow(clippy::disallowed_macros, clippy::disallowed_methods, clippy::disallowed_types)]
1818

19-
use pulldown_cmark::{html, Parser};
20-
use std::fmt::Write as _;
19+
mod bench;
20+
mod json;
21+
2122
use std::hint::black_box;
2223
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};
2429

2530
/// Byte-for-byte copy of `sampleMarkdown` in `parse-benchmark-bun.mjs`,
2631
/// including the leading and trailing newline. The JS harness derives
@@ -67,30 +72,6 @@ Final paragraph with `inline code` and more text.
6772
const SIZES: [(&str, usize, u32); 4] =
6873
[("small", 1, 100), ("medium", 10, 50), ("large", 100, 20), ("huge", 2150, 5)];
6974

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-
9475
enum CliAction {
9576
Run { runs: u32 },
9677
Help,
@@ -221,114 +202,6 @@ fn run_benchmarks(sizes: &[(&'static str, usize, u32)], runs: u32) -> SuiteResul
221202
SuiteResults { parse, render }
222203
}
223204

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-
332205
#[cfg(test)]
333206
mod tests {
334207
use super::*;
@@ -392,18 +265,6 @@ mod tests {
392265
}
393266
}
394267

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-
407268
#[test]
408269
fn sample_matches_js_harness_byte_for_byte() {
409270
// The whole protocol hangs on both harnesses timing the same bytes:

0 commit comments

Comments
 (0)