1use crate::{F64Summary, RunMeta, ToolInfo, U64Summary};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7pub const PAIRED_SCHEMA_V1: &str = "perfgate.paired.v1";
8
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
10#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
11pub struct PairedBenchMeta {
12 pub name: String,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub cwd: Option<String>,
15 pub baseline_command: Vec<String>,
16 pub current_command: Vec<String>,
17 pub repeat: u32,
18 pub warmup: u32,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub work_units: Option<u64>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub timeout_ms: Option<u64>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
26#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
27pub struct PairedSampleHalf {
28 pub wall_ms: u64,
29 pub exit_code: i32,
30 pub timed_out: bool,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub max_rss_kb: Option<u64>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub stdout: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub stderr: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
40#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
41pub struct PairedSample {
42 pub pair_index: u32,
43 #[serde(default)]
44 pub warmup: bool,
45 pub baseline: PairedSampleHalf,
46 pub current: PairedSampleHalf,
47 pub wall_diff_ms: i64,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub rss_diff_kb: Option<i64>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
53#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
54pub struct PairedDiffSummary {
55 pub mean: f64,
56 pub median: f64,
57 pub std_dev: f64,
58 pub min: f64,
59 pub max: f64,
60 pub count: u32,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
64#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
65pub struct PairedStats {
66 pub baseline_wall_ms: U64Summary,
67 pub current_wall_ms: U64Summary,
68 pub wall_diff_ms: PairedDiffSummary,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub baseline_max_rss_kb: Option<U64Summary>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub current_max_rss_kb: Option<U64Summary>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub rss_diff_kb: Option<PairedDiffSummary>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub baseline_throughput_per_s: Option<F64Summary>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub current_throughput_per_s: Option<F64Summary>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub throughput_diff_per_s: Option<PairedDiffSummary>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
84#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
85pub struct PairedRunReceipt {
86 pub schema: String,
87 pub tool: ToolInfo,
88 pub run: RunMeta,
89 pub bench: PairedBenchMeta,
90 pub samples: Vec<PairedSample>,
91 pub stats: PairedStats,
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::{HostInfo, RunMeta, ToolInfo, U64Summary};
98
99 fn make_receipt() -> PairedRunReceipt {
100 PairedRunReceipt {
101 schema: PAIRED_SCHEMA_V1.to_string(),
102 tool: ToolInfo {
103 name: "perfgate".to_string(),
104 version: "0.1.0".to_string(),
105 },
106 run: RunMeta {
107 id: "run-id".to_string(),
108 started_at: "2024-01-01T00:00:00Z".to_string(),
109 ended_at: "2024-01-01T00:00:01Z".to_string(),
110 host: HostInfo {
111 os: "linux".to_string(),
112 arch: "x86_64".to_string(),
113 cpu_count: None,
114 memory_bytes: None,
115 hostname_hash: None,
116 },
117 },
118 bench: PairedBenchMeta {
119 name: "bench".to_string(),
120 cwd: None,
121 baseline_command: vec!["echo".to_string(), "baseline".to_string()],
122 current_command: vec!["echo".to_string(), "current".to_string()],
123 repeat: 2,
124 warmup: 0,
125 work_units: None,
126 timeout_ms: None,
127 },
128 samples: vec![PairedSample {
129 pair_index: 0,
130 warmup: false,
131 baseline: PairedSampleHalf {
132 wall_ms: 100,
133 exit_code: 0,
134 timed_out: false,
135 max_rss_kb: None,
136 stdout: None,
137 stderr: None,
138 },
139 current: PairedSampleHalf {
140 wall_ms: 110,
141 exit_code: 0,
142 timed_out: false,
143 max_rss_kb: None,
144 stdout: None,
145 stderr: None,
146 },
147 wall_diff_ms: 10,
148 rss_diff_kb: None,
149 }],
150 stats: PairedStats {
151 baseline_wall_ms: U64Summary {
152 median: 100,
153 min: 100,
154 max: 100,
155 },
156 current_wall_ms: U64Summary {
157 median: 110,
158 min: 110,
159 max: 110,
160 },
161 wall_diff_ms: PairedDiffSummary {
162 mean: 10.0,
163 median: 10.0,
164 std_dev: 0.0,
165 min: 10.0,
166 max: 10.0,
167 count: 1,
168 },
169 baseline_max_rss_kb: None,
170 current_max_rss_kb: None,
171 rss_diff_kb: None,
172 baseline_throughput_per_s: None,
173 current_throughput_per_s: None,
174 throughput_diff_per_s: None,
175 },
176 }
177 }
178
179 #[test]
180 fn paired_receipt_json_round_trip() {
181 let receipt = make_receipt();
182 let json = serde_json::to_string(&receipt).expect("serialize paired receipt");
183 let decoded: PairedRunReceipt =
184 serde_json::from_str(&json).expect("deserialize paired receipt");
185 assert_eq!(decoded.schema, PAIRED_SCHEMA_V1);
186 assert_eq!(decoded.bench.name, "bench");
187 assert_eq!(decoded.samples.len(), 1);
188 assert_eq!(decoded.stats.wall_diff_ms.count, 1);
189 }
190
191 #[test]
192 fn paired_receipt_omits_optional_fields_when_none() {
193 let receipt = make_receipt();
194 let json: serde_json::Value =
195 serde_json::from_str(&serde_json::to_string(&receipt).unwrap()).unwrap();
196
197 let bench = &json["bench"];
198 assert!(bench.get("cwd").is_none());
199 assert!(bench.get("work_units").is_none());
200 assert!(bench.get("timeout_ms").is_none());
201
202 let sample = &json["samples"][0];
203 assert!(sample["baseline"].get("max_rss_kb").is_none());
204 assert!(sample["baseline"].get("stdout").is_none());
205 assert!(sample["baseline"].get("stderr").is_none());
206 }
207}