1mod criterion;
10mod gobench;
11mod hyperfine;
12mod pytest;
13
14use perfgate_types::{
15 BenchMeta, HostInfo, RUN_SCHEMA_V1, RunMeta, RunReceipt, Sample, Stats, ToolInfo, U64Summary,
16};
17use time::OffsetDateTime;
18use uuid::Uuid;
19
20pub use criterion::parse_criterion;
21pub use gobench::parse_gobench;
22pub use hyperfine::parse_hyperfine;
23pub use pytest::parse_pytest_benchmark;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum IngestFormat {
28 Criterion,
29 Hyperfine,
30 GoBench,
31 PytestBenchmark,
32}
33
34impl IngestFormat {
35 pub fn parse(s: &str) -> Option<Self> {
37 match s.to_lowercase().as_str() {
38 "criterion" => Some(Self::Criterion),
39 "hyperfine" => Some(Self::Hyperfine),
40 "gobench" | "go" => Some(Self::GoBench),
41 "pytest" | "pytest-benchmark" | "pytest_benchmark" => Some(Self::PytestBenchmark),
42 _ => None,
43 }
44 }
45}
46
47pub struct IngestRequest {
49 pub format: IngestFormat,
51 pub input: String,
53 pub name: Option<String>,
55}
56
57pub fn ingest(request: &IngestRequest) -> anyhow::Result<RunReceipt> {
59 match request.format {
60 IngestFormat::Criterion => parse_criterion(&request.input, request.name.as_deref()),
61 IngestFormat::Hyperfine => parse_hyperfine(&request.input, request.name.as_deref()),
62 IngestFormat::GoBench => parse_gobench(&request.input, request.name.as_deref()),
63 IngestFormat::PytestBenchmark => {
64 parse_pytest_benchmark(&request.input, request.name.as_deref())
65 }
66 }
67}
68
69fn make_receipt(name: &str, samples: Vec<Sample>, stats: Stats) -> RunReceipt {
71 let now = OffsetDateTime::now_utc();
72 let timestamp = now
73 .format(&time::format_description::well_known::Rfc3339)
74 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());
75
76 RunReceipt {
77 schema: RUN_SCHEMA_V1.to_string(),
78 tool: ToolInfo {
79 name: "perfgate-ingest".to_string(),
80 version: env!("CARGO_PKG_VERSION").to_string(),
81 },
82 run: RunMeta {
83 id: Uuid::new_v4().to_string(),
84 started_at: timestamp.clone(),
85 ended_at: timestamp,
86 host: HostInfo {
87 os: std::env::consts::OS.to_string(),
88 arch: std::env::consts::ARCH.to_string(),
89 cpu_count: None,
90 memory_bytes: None,
91 hostname_hash: None,
92 },
93 },
94 bench: BenchMeta {
95 name: name.to_string(),
96 cwd: None,
97 command: vec!["(ingested)".to_string()],
98 repeat: samples.len() as u32,
99 warmup: 0,
100 work_units: None,
101 timeout_ms: None,
102 },
103 samples,
104 stats,
105 }
106}
107
108fn compute_u64_summary(values: &[u64]) -> U64Summary {
110 if values.is_empty() {
111 return U64Summary {
112 median: 0,
113 min: 0,
114 max: 0,
115 mean: None,
116 stddev: None,
117 };
118 }
119
120 let mut sorted = values.to_vec();
121 sorted.sort_unstable();
122
123 let min = sorted[0];
124 let max = sorted[sorted.len() - 1];
125 let median = if sorted.len().is_multiple_of(2) {
126 (sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2
127 } else {
128 sorted[sorted.len() / 2]
129 };
130
131 let sum: f64 = values.iter().map(|&v| v as f64).sum();
132 let mean = sum / values.len() as f64;
133
134 let variance = values
135 .iter()
136 .map(|&v| (v as f64 - mean).powi(2))
137 .sum::<f64>()
138 / values.len() as f64;
139 let stddev = variance.sqrt();
140
141 U64Summary {
142 median,
143 min,
144 max,
145 mean: Some(mean),
146 stddev: Some(stddev),
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_ingest_format_parse() {
156 assert_eq!(
157 IngestFormat::parse("criterion"),
158 Some(IngestFormat::Criterion)
159 );
160 assert_eq!(
161 IngestFormat::parse("Criterion"),
162 Some(IngestFormat::Criterion)
163 );
164 assert_eq!(
165 IngestFormat::parse("hyperfine"),
166 Some(IngestFormat::Hyperfine)
167 );
168 assert_eq!(IngestFormat::parse("gobench"), Some(IngestFormat::GoBench));
169 assert_eq!(IngestFormat::parse("go"), Some(IngestFormat::GoBench));
170 assert_eq!(
171 IngestFormat::parse("pytest"),
172 Some(IngestFormat::PytestBenchmark)
173 );
174 assert_eq!(
175 IngestFormat::parse("pytest-benchmark"),
176 Some(IngestFormat::PytestBenchmark)
177 );
178 assert_eq!(
179 IngestFormat::parse("pytest_benchmark"),
180 Some(IngestFormat::PytestBenchmark)
181 );
182 assert_eq!(IngestFormat::parse("unknown"), None);
183 }
184
185 #[test]
186 fn test_compute_u64_summary_basic() {
187 let values = vec![100, 200, 300, 400, 500];
188 let summary = compute_u64_summary(&values);
189 assert_eq!(summary.median, 300);
190 assert_eq!(summary.min, 100);
191 assert_eq!(summary.max, 500);
192 assert!(summary.mean.is_some());
193 assert!((summary.mean.unwrap() - 300.0).abs() < 0.001);
194 }
195
196 #[test]
197 fn test_compute_u64_summary_even_count() {
198 let values = vec![100, 200, 300, 400];
199 let summary = compute_u64_summary(&values);
200 assert_eq!(summary.median, 250);
201 assert_eq!(summary.min, 100);
202 assert_eq!(summary.max, 400);
203 }
204
205 #[test]
206 fn test_compute_u64_summary_empty() {
207 let summary = compute_u64_summary(&[]);
208 assert_eq!(summary.median, 0);
209 assert_eq!(summary.min, 0);
210 assert_eq!(summary.max, 0);
211 assert!(summary.mean.is_none());
212 }
213
214 #[test]
215 fn test_compute_u64_summary_single() {
216 let values = vec![42];
217 let summary = compute_u64_summary(&values);
218 assert_eq!(summary.median, 42);
219 assert_eq!(summary.min, 42);
220 assert_eq!(summary.max, 42);
221 assert!((summary.mean.unwrap() - 42.0).abs() < 0.001);
222 assert!((summary.stddev.unwrap()).abs() < 0.001);
223 }
224
225 #[test]
226 fn test_make_receipt_structure() {
227 let samples = vec![Sample {
228 wall_ms: 100,
229 exit_code: 0,
230 warmup: false,
231 timed_out: false,
232 cpu_ms: None,
233 page_faults: None,
234 ctx_switches: None,
235 max_rss_kb: None,
236 io_read_bytes: None,
237 io_write_bytes: None,
238 network_packets: None,
239 energy_uj: None,
240 binary_bytes: None,
241 stdout: None,
242 stderr: None,
243 }];
244 let stats = Stats {
245 wall_ms: U64Summary::new(100, 100, 100),
246 cpu_ms: None,
247 page_faults: None,
248 ctx_switches: None,
249 max_rss_kb: None,
250 io_read_bytes: None,
251 io_write_bytes: None,
252 network_packets: None,
253 energy_uj: None,
254 binary_bytes: None,
255 throughput_per_s: None,
256 };
257 let receipt = make_receipt("test-bench", samples, stats);
258 assert_eq!(receipt.schema, RUN_SCHEMA_V1);
259 assert_eq!(receipt.bench.name, "test-bench");
260 assert_eq!(receipt.bench.repeat, 1);
261 assert_eq!(receipt.tool.name, "perfgate-ingest");
262 }
263}