Skip to content

Commit ea9d4ca

Browse files
committed
feat: parse and convert raw results into walltime results
1 parent 8b0f45e commit ea9d4ca

File tree

3 files changed

+310
-0
lines changed

3 files changed

+310
-0
lines changed

go-runner/src/results/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod raw_result;
2+
pub mod walltime_results;

go-runner/src/results/raw_result.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use std::path::Path;
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
use crate::results::walltime_results::WalltimeBenchmark;
6+
7+
// WARN: Keep in sync with Golang "testing" fork (benchmark.go)
8+
#[derive(Debug, Clone, Serialize, Deserialize)]
9+
pub struct RawResult {
10+
pub benchmark_name: String,
11+
pub pid: u32,
12+
pub codspeed_time_per_round_ns: Vec<u64>,
13+
14+
#[serde(default)]
15+
pub codspeed_iters_per_round: Vec<u64>,
16+
}
17+
18+
impl RawResult {
19+
pub fn parse(content: &str) -> anyhow::Result<Self> {
20+
serde_json::from_str(content)
21+
.map_err(|e| anyhow::anyhow!("Failed to parse raw result: {}", e))
22+
}
23+
24+
pub fn parse_folder<P: AsRef<Path>>(folder: P) -> anyhow::Result<Vec<Self>> {
25+
let glob_pattern = folder.as_ref().join("raw_results").join("*.json");
26+
Ok(glob::glob(&glob_pattern.to_string_lossy())?
27+
.filter_map(Result::ok)
28+
.filter_map(|path| {
29+
let content = std::fs::read_to_string(&path).ok()?;
30+
Self::parse(&content).ok()
31+
})
32+
.collect())
33+
}
34+
35+
pub fn into_walltime_benchmark(self, file_path: Option<String>) -> WalltimeBenchmark {
36+
let name = self.benchmark_name;
37+
38+
let file = file_path.as_deref().unwrap_or("unknown");
39+
let uri = format!("{file}::{name}");
40+
41+
let times_per_round_ns = self
42+
.codspeed_time_per_round_ns
43+
.iter()
44+
.map(|t| *t as u128)
45+
.collect::<Vec<_>>();
46+
let iters_per_round = if self.codspeed_iters_per_round.is_empty() {
47+
vec![1; times_per_round_ns.len()]
48+
} else {
49+
self.codspeed_iters_per_round
50+
.iter()
51+
.map(|i| *i as u128)
52+
.collect()
53+
};
54+
55+
WalltimeBenchmark::from_runtime_data(name, uri, iters_per_round, times_per_round_ns, None)
56+
}
57+
}
58+
59+
#[cfg(test)]
60+
mod tests {
61+
use super::*;
62+
63+
#[test]
64+
fn test_raw_result_deserialization() {
65+
let json_data = r#"{
66+
"benchmark_name": "BenchmarkFibonacci20-16",
67+
"pid": 777767,
68+
"codspeed_time_per_round_ns": [1000, 2000, 3000]
69+
}"#;
70+
let result: RawResult = serde_json::from_str(json_data).unwrap();
71+
72+
assert_eq!(result.benchmark_name, "BenchmarkFibonacci20-16");
73+
assert_eq!(result.pid, 777767);
74+
assert_eq!(result.codspeed_time_per_round_ns.len(), 3);
75+
assert_eq!(result.codspeed_iters_per_round.len(), 0); // Default: 1 per round
76+
}
77+
78+
#[test]
79+
fn test_into_walltime_benchmark_with_file_path() {
80+
let raw_result = RawResult {
81+
benchmark_name: "BenchmarkFibonacci20-16".to_string(),
82+
pid: 777767,
83+
codspeed_time_per_round_ns: vec![1000, 2000, 3000],
84+
codspeed_iters_per_round: vec![],
85+
};
86+
87+
// Test with file path - should not panic and create successfully
88+
let _walltime_bench = raw_result
89+
.clone()
90+
.into_walltime_benchmark(Some("pkg/foo/fib_test.go".to_string()));
91+
92+
// Test without file path (should default to TODO) - should not panic and create successfully
93+
let _walltime_bench_no_path = raw_result.into_walltime_benchmark(None);
94+
}
95+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// NOTE: This file was taken from `codspeed-rust` and modified a bit to fit this project.
2+
3+
use anyhow::Result;
4+
5+
use serde::{Deserialize, Serialize};
6+
use statrs::statistics::{Data, Distribution, Max, Min, OrderStatistics};
7+
8+
const IQR_OUTLIER_FACTOR: f64 = 1.5;
9+
const STDEV_OUTLIER_FACTOR: f64 = 3.0;
10+
11+
#[derive(Debug, Serialize, Deserialize)]
12+
pub struct BenchmarkMetadata {
13+
pub name: String,
14+
pub uri: String,
15+
}
16+
17+
#[derive(Debug, Serialize, Deserialize)]
18+
struct BenchmarkStats {
19+
min_ns: f64,
20+
max_ns: f64,
21+
mean_ns: f64,
22+
stdev_ns: f64,
23+
24+
q1_ns: f64,
25+
median_ns: f64,
26+
q3_ns: f64,
27+
28+
rounds: u64,
29+
total_time: f64,
30+
iqr_outlier_rounds: u64,
31+
stdev_outlier_rounds: u64,
32+
iter_per_round: u64,
33+
warmup_iters: u64,
34+
}
35+
36+
#[derive(Debug, Serialize, Deserialize, Default)]
37+
struct BenchmarkConfig {
38+
warmup_time_ns: Option<f64>,
39+
min_round_time_ns: Option<f64>,
40+
max_time_ns: Option<f64>,
41+
max_rounds: Option<u64>,
42+
}
43+
44+
#[derive(Debug, Serialize, Deserialize)]
45+
pub struct WalltimeBenchmark {
46+
#[serde(flatten)]
47+
pub metadata: BenchmarkMetadata,
48+
49+
config: BenchmarkConfig,
50+
stats: BenchmarkStats,
51+
}
52+
53+
impl WalltimeBenchmark {
54+
pub fn from_runtime_data(
55+
name: String,
56+
uri: String,
57+
iters_per_round: Vec<u128>,
58+
times_per_round_ns: Vec<u128>,
59+
max_time_ns: Option<u128>,
60+
) -> Self {
61+
let total_time = times_per_round_ns.iter().sum::<u128>() as f64 / 1_000_000_000.0;
62+
let time_per_iteration_per_round_ns: Vec<_> = times_per_round_ns
63+
.into_iter()
64+
.zip(&iters_per_round)
65+
.map(|(time_per_round, iter_per_round)| time_per_round / iter_per_round)
66+
.map(|t| t as f64)
67+
.collect::<Vec<f64>>();
68+
69+
let mut data = Data::new(time_per_iteration_per_round_ns);
70+
let rounds = data.len() as u64;
71+
72+
let mean_ns = data.mean().unwrap();
73+
74+
let stdev_ns = if data.len() < 2 {
75+
// std_dev() returns f64::NAN if data has less than two entries, so we have to
76+
// manually handle this case.
77+
0.0
78+
} else {
79+
data.std_dev().unwrap()
80+
};
81+
82+
let q1_ns = data.quantile(0.25);
83+
let median_ns = data.median();
84+
let q3_ns = data.quantile(0.75);
85+
86+
let iqr_ns = q3_ns - q1_ns;
87+
let iqr_outlier_rounds = data
88+
.iter()
89+
.filter(|&&t| {
90+
t < q1_ns - IQR_OUTLIER_FACTOR * iqr_ns || t > q3_ns + IQR_OUTLIER_FACTOR * iqr_ns
91+
})
92+
.count() as u64;
93+
94+
let stdev_outlier_rounds = data
95+
.iter()
96+
.filter(|&&t| {
97+
t < mean_ns - STDEV_OUTLIER_FACTOR * stdev_ns
98+
|| t > mean_ns + STDEV_OUTLIER_FACTOR * stdev_ns
99+
})
100+
.count() as u64;
101+
102+
let min_ns = data.min();
103+
let max_ns = data.max();
104+
105+
// TODO(COD-1056): We currently only support single iteration count per round
106+
let iter_per_round =
107+
(iters_per_round.iter().sum::<u128>() / iters_per_round.len() as u128) as u64;
108+
let warmup_iters = 0; // FIXME: add warmup detection
109+
110+
let stats = BenchmarkStats {
111+
min_ns,
112+
max_ns,
113+
mean_ns,
114+
stdev_ns,
115+
q1_ns,
116+
median_ns,
117+
q3_ns,
118+
rounds,
119+
total_time,
120+
iqr_outlier_rounds,
121+
stdev_outlier_rounds,
122+
iter_per_round,
123+
warmup_iters,
124+
};
125+
126+
WalltimeBenchmark {
127+
metadata: BenchmarkMetadata { name, uri },
128+
config: BenchmarkConfig {
129+
max_time_ns: max_time_ns.map(|t| t as f64),
130+
..Default::default()
131+
},
132+
stats,
133+
}
134+
}
135+
}
136+
137+
#[derive(Debug, Serialize, Deserialize)]
138+
struct Instrument {
139+
#[serde(rename = "type")]
140+
type_: String,
141+
}
142+
143+
#[derive(Debug, Serialize, Deserialize)]
144+
pub struct Creator {
145+
pub name: String,
146+
pub version: String,
147+
pub pid: u32,
148+
}
149+
150+
#[derive(Debug, Serialize, Deserialize)]
151+
pub struct WalltimeResults {
152+
creator: Creator,
153+
instrument: Instrument,
154+
pub benchmarks: Vec<WalltimeBenchmark>,
155+
}
156+
157+
impl WalltimeResults {
158+
pub fn new(benchmarks: Vec<WalltimeBenchmark>, creator: Creator) -> Result<Self> {
159+
Ok(WalltimeResults {
160+
instrument: Instrument {
161+
type_: "walltime".to_string(),
162+
},
163+
creator,
164+
benchmarks,
165+
})
166+
}
167+
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use super::*;
172+
173+
const NAME: &str = "benchmark";
174+
const URI: &str = "test::benchmark";
175+
176+
#[test]
177+
fn test_parse_single_benchmark() {
178+
let benchmark = WalltimeBenchmark::from_runtime_data(
179+
NAME.to_string(),
180+
URI.to_string(),
181+
vec![1],
182+
vec![42],
183+
None,
184+
);
185+
assert_eq!(benchmark.stats.stdev_ns, 0.);
186+
assert_eq!(benchmark.stats.min_ns, 42.);
187+
assert_eq!(benchmark.stats.max_ns, 42.);
188+
assert_eq!(benchmark.stats.mean_ns, 42.);
189+
}
190+
191+
#[test]
192+
fn test_parse_bench_with_variable_iterations() {
193+
let iters_per_round = vec![1, 2, 3, 4, 5, 6];
194+
let total_rounds = iters_per_round.iter().sum::<u128>() as f64;
195+
196+
let benchmark = WalltimeBenchmark::from_runtime_data(
197+
NAME.to_string(),
198+
URI.to_string(),
199+
iters_per_round,
200+
vec![42, 42 * 2, 42 * 3, 42 * 4, 42 * 5, 42 * 6],
201+
None,
202+
);
203+
204+
assert_eq!(benchmark.stats.stdev_ns, 0.);
205+
assert_eq!(benchmark.stats.min_ns, 42.);
206+
assert_eq!(benchmark.stats.max_ns, 42.);
207+
assert_eq!(benchmark.stats.mean_ns, 42.);
208+
assert_eq!(
209+
benchmark.stats.total_time,
210+
42. * total_rounds / 1_000_000_000.0
211+
);
212+
}
213+
}

0 commit comments

Comments
 (0)