use anyhow::Context;
use perfgate_types::{RunReceipt, Sample, Stats, U64Summary};
use regex::Regex;
use super::make_receipt;
#[derive(Debug)]
struct GoBenchLine {
name: String,
iterations: u64,
ns_per_op: f64,
bytes_per_op: Option<u64>,
allocs_per_op: Option<u64>,
}
pub fn parse_gobench(input: &str, name: Option<&str>) -> anyhow::Result<RunReceipt> {
let lines = parse_gobench_lines(input)?;
let first = lines
.first()
.context("no benchmark results found in Go bench output")?;
let bench_name = name
.map(|n| n.to_string())
.unwrap_or_else(|| first.name.clone());
let wall_ms = ns_to_ms(first.ns_per_op);
let sample = Sample {
wall_ms,
exit_code: 0,
warmup: false,
timed_out: false,
cpu_ms: None,
page_faults: None,
ctx_switches: None,
max_rss_kb: None,
io_read_bytes: None,
io_write_bytes: None,
network_packets: None,
energy_uj: None,
binary_bytes: None,
stdout: None,
stderr: None,
};
let wall_stats = U64Summary {
median: wall_ms,
min: wall_ms,
max: wall_ms,
mean: Some(first.ns_per_op / 1_000_000.0),
stddev: None,
};
let max_rss_kb = first.bytes_per_op.map(|b| {
let kb = b.div_ceil(1024);
U64Summary {
median: kb,
min: kb,
max: kb,
mean: Some(b as f64 / 1024.0),
stddev: None,
}
});
let stats = Stats {
wall_ms: wall_stats,
cpu_ms: None,
page_faults: None,
ctx_switches: None,
max_rss_kb,
io_read_bytes: None,
io_write_bytes: None,
network_packets: None,
energy_uj: None,
binary_bytes: None,
throughput_per_s: None,
};
let mut receipt = make_receipt(&bench_name, vec![sample], stats);
if let Some(allocs) = first.allocs_per_op {
receipt.bench.command = vec![
format!(
"(go bench: {} iterations, {} ns/op",
first.iterations, first.ns_per_op
),
format!("{} allocs/op)", allocs),
];
} else {
receipt.bench.command = vec![format!(
"(go bench: {} iterations, {} ns/op)",
first.iterations, first.ns_per_op
)];
}
Ok(receipt)
}
fn parse_gobench_lines(input: &str) -> anyhow::Result<Vec<GoBenchLine>> {
let re = Regex::new(
r"(?m)^(Benchmark\S+)\s+(\d+)\s+([\d.]+)\s+ns/op(?:\s+(\d+)\s+B/op)?(?:\s+(\d+)\s+allocs/op)?",
)?;
let mut lines = Vec::new();
for cap in re.captures_iter(input) {
let name = cap[1].to_string();
let iterations: u64 = cap[2].parse().context("invalid iteration count")?;
let ns_per_op: f64 = cap[3].parse().context("invalid ns/op value")?;
let bytes_per_op = cap
.get(4)
.map(|m| m.as_str().parse::<u64>())
.transpose()
.context("invalid B/op value")?;
let allocs_per_op = cap
.get(5)
.map(|m| m.as_str().parse::<u64>())
.transpose()
.context("invalid allocs/op value")?;
lines.push(GoBenchLine {
name,
iterations,
ns_per_op,
bytes_per_op,
allocs_per_op,
});
}
Ok(lines)
}
fn ns_to_ms(ns: f64) -> u64 {
let ms = ns / 1_000_000.0;
if ms < 1.0 && ms > 0.0 {
1
} else {
ms.round() as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
use perfgate_types::RUN_SCHEMA_V1;
#[test]
fn parse_gobench_basic() {
let input = "BenchmarkFoo-8\t 1000\t 50000000 ns/op\t 567 B/op\t 3 allocs/op\n";
let receipt = parse_gobench(input, Some("foo-bench")).unwrap();
assert_eq!(receipt.schema, RUN_SCHEMA_V1);
assert_eq!(receipt.bench.name, "foo-bench");
assert_eq!(receipt.samples.len(), 1);
assert_eq!(receipt.stats.wall_ms.median, 50);
assert!(receipt.stats.max_rss_kb.is_some());
assert_eq!(receipt.stats.max_rss_kb.unwrap().median, 1);
}
#[test]
fn parse_gobench_default_name() {
let input = "BenchmarkBar-4\t 500\t 2000000 ns/op\n";
let receipt = parse_gobench(input, None).unwrap();
assert_eq!(receipt.bench.name, "BenchmarkBar-4");
}
#[test]
fn parse_gobench_no_memory() {
let input = "BenchmarkSimple-8\t 10000\t 500 ns/op\n";
let receipt = parse_gobench(input, None).unwrap();
assert!(receipt.stats.max_rss_kb.is_none());
assert_eq!(receipt.stats.wall_ms.median, 1);
}
#[test]
fn parse_gobench_multiple_benchmarks() {
let input = "\
BenchmarkA-8\t 1000\t 100000 ns/op\n\
BenchmarkB-8\t 2000\t 200000 ns/op\n";
let receipt = parse_gobench(input, None).unwrap();
assert_eq!(receipt.bench.name, "BenchmarkA-8");
}
#[test]
fn parse_gobench_with_surrounding_text() {
let input = "\
goos: linux
goarch: amd64
pkg: example.com/mypackage
BenchmarkHash-8\t 5000\t 300000 ns/op\t 128 B/op\t 2 allocs/op
PASS
ok \texample.com/mypackage\t1.523s
";
let receipt = parse_gobench(input, None).unwrap();
assert_eq!(receipt.bench.name, "BenchmarkHash-8");
assert_eq!(receipt.stats.wall_ms.median, 1);
}
#[test]
fn parse_gobench_empty_input() {
let result = parse_gobench("no benchmark lines here", None);
assert!(result.is_err());
}
#[test]
fn parse_gobench_fractional_ns() {
let input = "BenchmarkFrac-8\t 1000\t 1234.56 ns/op\n";
let receipt = parse_gobench(input, None).unwrap();
assert_eq!(receipt.stats.wall_ms.median, 1);
}
}