1use crate::parsers::types::{BenchStatistics, BenchmarkMeasurement, ParsedMeasurement, Parser};
2use anyhow::{Context, Result};
3use serde::Deserialize;
4use std::collections::HashMap;
5
6pub struct CriterionJsonParser;
8
9impl Parser for CriterionJsonParser {
10 fn parse(&self, input: &str) -> Result<Vec<ParsedMeasurement>> {
11 let mut measurements = Vec::new();
12
13 for (line_num, line) in input.lines().enumerate() {
14 let line = line.trim();
15 if line.is_empty() {
16 continue;
17 }
18
19 let message: CriterionMessage = serde_json::from_str(line)
20 .with_context(|| format!("Failed to parse JSON on line {}", line_num + 1))?;
21
22 if message.reason == "benchmark-complete" {
24 if let Some(measurement) = message.into_measurement()? {
25 measurements.push(measurement);
26 }
27 }
28 }
29
30 Ok(measurements)
31 }
32}
33
34#[derive(Debug, Deserialize)]
35struct CriterionMessage {
36 reason: String,
37 #[serde(default)]
38 id: String,
39 group: Option<String>,
40 unit: Option<String>,
41 mean: Option<Estimate>,
42 median: Option<Estimate>,
43 slope: Option<Estimate>,
44 median_abs_dev: Option<Estimate>,
45}
46
47#[derive(Debug, Deserialize)]
48#[allow(dead_code)]
49struct Estimate {
50 estimate: f64,
51 lower_bound: Option<f64>,
52 upper_bound: Option<f64>,
53}
54
55impl CriterionMessage {
56 fn into_measurement(self) -> Result<Option<ParsedMeasurement>> {
57 if self.reason != "benchmark-complete" {
58 return Ok(None);
59 }
60
61 let unit = self.unit.unwrap_or_else(|| "ns".to_string());
62
63 let (parsed_group, bench_name, input) = parse_benchmark_id(&self.id);
67 let group = self.group.or(parsed_group);
68
69 let statistics = BenchStatistics {
70 mean_ns: self.mean.map(|e| convert_to_nanoseconds(e.estimate, &unit)),
71 median_ns: self
72 .median
73 .map(|e| convert_to_nanoseconds(e.estimate, &unit)),
74 slope_ns: self
75 .slope
76 .map(|e| convert_to_nanoseconds(e.estimate, &unit)),
77 mad_ns: self
78 .median_abs_dev
79 .map(|e| convert_to_nanoseconds(e.estimate, &unit)),
80 unit,
81 };
82
83 let mut metadata = HashMap::new();
84 metadata.insert("type".to_string(), "bench".to_string());
85 if let Some(g) = group {
86 metadata.insert("group".to_string(), g);
87 }
88 if let Some(n) = bench_name {
89 metadata.insert("bench_name".to_string(), n);
90 }
91 if let Some(i) = input {
92 metadata.insert("input".to_string(), i);
93 }
94
95 Ok(Some(ParsedMeasurement::Benchmark(BenchmarkMeasurement {
96 id: self.id,
97 statistics,
98 metadata,
99 })))
100 }
101}
102
103fn parse_benchmark_id(id: &str) -> (Option<String>, Option<String>, Option<String>) {
109 let parts: Vec<&str> = id.split('/').collect();
110
111 match parts.len() {
112 0 => (None, None, None),
113 1 => (None, Some(parts[0].to_string()), None),
114 2 => (Some(parts[0].to_string()), Some(parts[1].to_string()), None),
115 _ => (
116 Some(parts[0].to_string()),
117 Some(parts[1].to_string()),
118 Some(parts[2..].join("/")),
119 ),
120 }
121}
122
123fn convert_to_nanoseconds(value: f64, unit: &str) -> f64 {
125 match unit {
126 "ns" => value,
127 "us" => value * 1_000.0,
128 "ms" => value * 1_000_000.0,
129 "s" => value * 1_000_000_000.0,
130 _ => value, }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_parse_benchmark_complete() {
140 let json = r#"{"reason":"benchmark-complete","id":"add_measurements/add_measurement/50","unit":"ns","mean":{"estimate":15456.78,"lower_bound":15234.0,"upper_bound":15678.5},"median":{"estimate":15400.0},"slope":{"estimate":15420.5},"median_abs_dev":{"estimate":123.45}}"#;
141
142 let parser = CriterionJsonParser;
143 let result = parser.parse(json).unwrap();
144
145 assert_eq!(result.len(), 1);
146
147 if let ParsedMeasurement::Benchmark(bench) = &result[0] {
148 assert_eq!(bench.id, "add_measurements/add_measurement/50");
149 assert_eq!(bench.statistics.unit, "ns");
150 assert_eq!(bench.statistics.mean_ns, Some(15456.78));
151 assert_eq!(bench.statistics.median_ns, Some(15400.0));
152 assert_eq!(bench.statistics.slope_ns, Some(15420.5));
153 assert_eq!(bench.statistics.mad_ns, Some(123.45));
154 assert_eq!(bench.metadata.get("group").unwrap(), "add_measurements");
155 assert_eq!(bench.metadata.get("bench_name").unwrap(), "add_measurement");
156 assert_eq!(bench.metadata.get("input").unwrap(), "50");
157 } else {
158 panic!("Expected Benchmark measurement");
159 }
160 }
161
162 #[test]
163 fn test_parse_multiple_lines() {
164 let json = r#"{"reason":"group-start","group":"fibonacci"}
165{"reason":"benchmark-complete","id":"fibonacci_10","unit":"us","mean":{"estimate":1.234}}
166{"reason":"benchmark-complete","id":"fibonacci_20","unit":"us","mean":{"estimate":56.789}}
167{"reason":"group-complete","group":"fibonacci"}"#;
168
169 let parser = CriterionJsonParser;
170 let result = parser.parse(json).unwrap();
171
172 assert_eq!(result.len(), 2);
173
174 if let ParsedMeasurement::Benchmark(bench) = &result[0] {
175 assert_eq!(bench.id, "fibonacci_10");
176 assert_eq!(bench.statistics.mean_ns, Some(1234.0));
178 } else {
179 panic!("Expected Benchmark measurement");
180 }
181 }
182
183 #[test]
184 fn test_parse_benchmark_id_three_parts() {
185 let (group, name, input) = parse_benchmark_id("add_measurements/add_measurement/50");
186 assert_eq!(group, Some("add_measurements".to_string()));
187 assert_eq!(name, Some("add_measurement".to_string()));
188 assert_eq!(input, Some("50".to_string()));
189 }
190
191 #[test]
192 fn test_parse_benchmark_id_two_parts() {
193 let (group, name, input) = parse_benchmark_id("fibonacci/fib_10");
194 assert_eq!(group, Some("fibonacci".to_string()));
195 assert_eq!(name, Some("fib_10".to_string()));
196 assert_eq!(input, None);
197 }
198
199 #[test]
200 fn test_parse_benchmark_id_one_part() {
201 let (group, name, input) = parse_benchmark_id("simple_bench");
202 assert_eq!(group, None);
203 assert_eq!(name, Some("simple_bench".to_string()));
204 assert_eq!(input, None);
205 }
206
207 #[test]
208 fn test_convert_units() {
209 assert_eq!(convert_to_nanoseconds(1.0, "ns"), 1.0);
210 assert_eq!(convert_to_nanoseconds(1.0, "us"), 1000.0);
211 assert_eq!(convert_to_nanoseconds(1.0, "ms"), 1_000_000.0);
212 assert_eq!(convert_to_nanoseconds(1.0, "s"), 1_000_000_000.0);
213 }
214
215 #[test]
216 fn test_parse_empty_lines() {
217 let json = r#"
218{"reason":"benchmark-complete","id":"test","unit":"ns","mean":{"estimate":100.0}}
219
220{"reason":"benchmark-complete","id":"test2","unit":"ns","mean":{"estimate":200.0}}
221"#;
222
223 let parser = CriterionJsonParser;
224 let result = parser.parse(json).unwrap();
225 assert_eq!(result.len(), 2);
226 }
227
228 #[test]
229 fn test_parse_invalid_json() {
230 let json = "not valid json";
231 let parser = CriterionJsonParser;
232 assert!(parser.parse(json).is_err());
233 }
234
235 #[test]
236 fn test_parse_missing_unit_defaults_to_ns() {
237 let json = r#"{"reason":"benchmark-complete","id":"test","mean":{"estimate":15456.78}}"#;
238
239 let parser = CriterionJsonParser;
240 let result = parser.parse(json).unwrap();
241
242 if let ParsedMeasurement::Benchmark(bench) = &result[0] {
243 assert_eq!(bench.statistics.unit, "ns");
244 assert_eq!(bench.statistics.mean_ns, Some(15456.78));
245 } else {
246 panic!("Expected Benchmark measurement");
247 }
248 }
249}