Skip to main content

perfgate_types/
paired.rs

1//! Paired mode types for perfgate.
2
3use crate::{F64Summary, RunMeta, Significance, 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    #[serde(skip_serializing_if = "Option::is_none")]
62    pub significance: Option<Significance>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
66#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
67pub struct PairedStats {
68    pub baseline_wall_ms: U64Summary,
69    pub current_wall_ms: U64Summary,
70    pub wall_diff_ms: PairedDiffSummary,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub baseline_max_rss_kb: Option<U64Summary>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub current_max_rss_kb: Option<U64Summary>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub rss_diff_kb: Option<PairedDiffSummary>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub baseline_throughput_per_s: Option<F64Summary>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub current_throughput_per_s: Option<F64Summary>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub throughput_diff_per_s: Option<PairedDiffSummary>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
86#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
87pub struct PairedRunReceipt {
88    pub schema: String,
89    pub tool: ToolInfo,
90    pub run: RunMeta,
91    pub bench: PairedBenchMeta,
92    pub samples: Vec<PairedSample>,
93    pub stats: PairedStats,
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::{HostInfo, RunMeta, ToolInfo, U64Summary};
100
101    fn make_receipt() -> PairedRunReceipt {
102        PairedRunReceipt {
103            schema: PAIRED_SCHEMA_V1.to_string(),
104            tool: ToolInfo {
105                name: "perfgate".to_string(),
106                version: "0.1.0".to_string(),
107            },
108            run: RunMeta {
109                id: "run-id".to_string(),
110                started_at: "2024-01-01T00:00:00Z".to_string(),
111                ended_at: "2024-01-01T00:00:01Z".to_string(),
112                host: HostInfo {
113                    os: "linux".to_string(),
114                    arch: "x86_64".to_string(),
115                    cpu_count: None,
116                    memory_bytes: None,
117                    hostname_hash: None,
118                },
119            },
120            bench: PairedBenchMeta {
121                name: "bench".to_string(),
122                cwd: None,
123                baseline_command: vec!["echo".to_string(), "baseline".to_string()],
124                current_command: vec!["echo".to_string(), "current".to_string()],
125                repeat: 2,
126                warmup: 0,
127                work_units: None,
128                timeout_ms: None,
129            },
130            samples: vec![PairedSample {
131                pair_index: 0,
132                warmup: false,
133                baseline: PairedSampleHalf {
134                    wall_ms: 100,
135                    exit_code: 0,
136                    timed_out: false,
137                    max_rss_kb: None,
138                    stdout: None,
139                    stderr: None,
140                },
141                current: PairedSampleHalf {
142                    wall_ms: 110,
143                    exit_code: 0,
144                    timed_out: false,
145                    max_rss_kb: None,
146                    stdout: None,
147                    stderr: None,
148                },
149                wall_diff_ms: 10,
150                rss_diff_kb: None,
151            }],
152            stats: PairedStats {
153                baseline_wall_ms: U64Summary::new(100, 100, 100),
154                current_wall_ms: U64Summary::new(110, 110, 110),
155                wall_diff_ms: PairedDiffSummary {
156                    mean: 10.0,
157                    median: 10.0,
158                    std_dev: 0.0,
159                    min: 10.0,
160                    max: 10.0,
161                    count: 1,
162                    significance: None,
163                },
164                baseline_max_rss_kb: None,
165                current_max_rss_kb: None,
166                rss_diff_kb: None,
167                baseline_throughput_per_s: None,
168                current_throughput_per_s: None,
169                throughput_diff_per_s: None,
170            },
171        }
172    }
173
174    #[test]
175    fn paired_receipt_json_round_trip() {
176        let receipt = make_receipt();
177        let json = serde_json::to_string(&receipt).expect("serialize paired receipt");
178        let decoded: PairedRunReceipt =
179            serde_json::from_str(&json).expect("deserialize paired receipt");
180        assert_eq!(decoded.schema, PAIRED_SCHEMA_V1);
181        assert_eq!(decoded.bench.name, "bench");
182        assert_eq!(decoded.samples.len(), 1);
183        assert_eq!(decoded.stats.wall_diff_ms.count, 1);
184    }
185
186    #[test]
187    fn paired_receipt_omits_optional_fields_when_none() {
188        let receipt = make_receipt();
189        let json: serde_json::Value =
190            serde_json::from_str(&serde_json::to_string(&receipt).unwrap()).unwrap();
191
192        let bench = &json["bench"];
193        assert!(bench.get("cwd").is_none());
194        assert!(bench.get("work_units").is_none());
195        assert!(bench.get("timeout_ms").is_none());
196
197        let sample = &json["samples"][0];
198        assert!(sample["baseline"].get("max_rss_kb").is_none());
199        assert!(sample["baseline"].get("stdout").is_none());
200        assert!(sample["baseline"].get("stderr").is_none());
201    }
202}