1use anyhow::Context;
8use perfgate_types::{RunReceipt, Sample, Stats};
9use serde::Deserialize;
10
11use crate::{compute_u64_summary, make_receipt};
12
13#[derive(Debug, Deserialize)]
15struct HyperfineResult {
16 command: String,
17 times: Vec<f64>,
19 mean: f64,
20 stddev: f64,
21 median: f64,
22 min: f64,
23 max: f64,
24}
25
26#[derive(Debug, Deserialize)]
28struct HyperfineOutput {
29 results: Vec<HyperfineResult>,
30}
31
32pub fn parse_hyperfine(input: &str, name: Option<&str>) -> anyhow::Result<RunReceipt> {
38 let output: HyperfineOutput =
39 serde_json::from_str(input).context("failed to parse hyperfine JSON")?;
40
41 let result = output
42 .results
43 .first()
44 .context("hyperfine JSON contains no results")?;
45
46 let bench_name = name
47 .map(|n| n.to_string())
48 .unwrap_or_else(|| result.command.clone());
49
50 let mut wall_values = Vec::new();
52 let mut samples = Vec::new();
53
54 for &t in &result.times {
55 let ms = seconds_to_ms(t);
56 wall_values.push(ms);
57 samples.push(Sample {
58 wall_ms: ms,
59 exit_code: 0,
60 warmup: false,
61 timed_out: false,
62 cpu_ms: None,
63 page_faults: None,
64 ctx_switches: None,
65 max_rss_kb: None,
66 io_read_bytes: None,
67 io_write_bytes: None,
68 network_packets: None,
69 energy_uj: None,
70 binary_bytes: None,
71 stdout: None,
72 stderr: None,
73 });
74 }
75
76 let mut stats = compute_u64_summary(&wall_values);
77 stats.median = seconds_to_ms(result.median);
80 stats.min = seconds_to_ms(result.min);
81 stats.max = seconds_to_ms(result.max);
82 stats.mean = Some(result.mean * 1000.0);
86 stats.stddev = Some(result.stddev * 1000.0);
87
88 let full_stats = Stats {
89 wall_ms: stats,
90 cpu_ms: None,
91 page_faults: None,
92 ctx_switches: None,
93 max_rss_kb: None,
94 io_read_bytes: None,
95 io_write_bytes: None,
96 network_packets: None,
97 energy_uj: None,
98 binary_bytes: None,
99 throughput_per_s: None,
100 };
101
102 Ok(make_receipt(&bench_name, samples, full_stats))
103}
104
105fn seconds_to_ms(s: f64) -> u64 {
114 let ms = s * 1000.0;
115 if ms < 1.0 && ms > 0.0 {
116 1
117 } else {
118 ms.round() as u64
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use perfgate_types::RUN_SCHEMA_V1;
126
127 const HYPERFINE_JSON: &str = r#"{
128 "results": [
129 {
130 "command": "sleep 0.1",
131 "mean": 0.1023,
132 "stddev": 0.0015,
133 "median": 0.1020,
134 "user": 0.001,
135 "system": 0.002,
136 "min": 0.1001,
137 "max": 0.1056,
138 "times": [0.1001, 0.1015, 0.1020, 0.1030, 0.1056],
139 "exit_codes": [0, 0, 0, 0, 0]
140 }
141 ]
142 }"#;
143
144 #[test]
145 fn parse_hyperfine_basic() {
146 let receipt = parse_hyperfine(HYPERFINE_JSON, Some("sleep-bench")).unwrap();
147 assert_eq!(receipt.schema, RUN_SCHEMA_V1);
148 assert_eq!(receipt.bench.name, "sleep-bench");
149 assert_eq!(receipt.samples.len(), 5);
150 assert_eq!(receipt.stats.wall_ms.median, 102);
152 assert_eq!(receipt.stats.wall_ms.min, 100);
153 assert_eq!(receipt.stats.wall_ms.max, 106);
154 }
155
156 #[test]
157 fn parse_hyperfine_default_name() {
158 let receipt = parse_hyperfine(HYPERFINE_JSON, None).unwrap();
159 assert_eq!(receipt.bench.name, "sleep 0.1");
160 }
161
162 #[test]
163 fn parse_hyperfine_sample_wall_ms() {
164 let receipt = parse_hyperfine(HYPERFINE_JSON, None).unwrap();
165 let wall_values: Vec<u64> = receipt.samples.iter().map(|s| s.wall_ms).collect();
167 assert_eq!(wall_values, vec![100, 102, 102, 103, 106]);
168 }
169
170 #[test]
171 fn parse_hyperfine_multiple_results() {
172 let input = r#"{
174 "results": [
175 {
176 "command": "echo first",
177 "mean": 0.005,
178 "stddev": 0.001,
179 "median": 0.005,
180 "user": 0.001,
181 "system": 0.001,
182 "min": 0.004,
183 "max": 0.006,
184 "times": [0.004, 0.005, 0.006],
185 "exit_codes": [0, 0, 0]
186 },
187 {
188 "command": "echo second",
189 "mean": 0.010,
190 "stddev": 0.002,
191 "median": 0.010,
192 "user": 0.001,
193 "system": 0.001,
194 "min": 0.008,
195 "max": 0.012,
196 "times": [0.008, 0.010, 0.012],
197 "exit_codes": [0, 0, 0]
198 }
199 ]
200 }"#;
201 let receipt = parse_hyperfine(input, None).unwrap();
202 assert_eq!(receipt.bench.name, "echo first");
203 }
204
205 #[test]
206 fn parse_hyperfine_empty_results() {
207 let input = r#"{"results": []}"#;
208 let result = parse_hyperfine(input, None);
209 assert!(result.is_err());
210 }
211
212 #[test]
213 fn parse_hyperfine_invalid_json() {
214 let result = parse_hyperfine("{bad json", None);
215 assert!(result.is_err());
216 }
217}