1use crate::{RunProfile, Stage};
2use serde::{Deserialize, Serialize};
3use std::fmt::Debug;
4
5pub const PROFILE_COMPARISON_VERSION: u16 = 1;
6pub const COMPARISON_DIFFERENCE_VERSION: u16 = 1;
7pub const STAGE_COMPARISON_VERSION: u16 = 1;
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
11pub struct ComparisonDifference {
12 pub version: u16,
13 pub field: String,
14 pub baseline: String,
15 pub candidate: String,
16}
17
18#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
20pub struct StageComparison {
21 pub version: u16,
22 pub stage: Stage,
23 pub baseline_elapsed_ns: u64,
24 pub candidate_elapsed_ns: u64,
25 pub elapsed_delta_ns: i128,
26 pub elapsed_change_percent: Option<f64>,
27 pub baseline_calls: u64,
28 pub candidate_calls: u64,
29 pub calls_delta: i128,
30}
31
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
34pub struct ProfileComparison {
35 pub version: u16,
36 pub baseline_run_id: String,
37 pub candidate_run_id: String,
38 pub comparable: bool,
39 pub incompatibilities: Vec<ComparisonDifference>,
40 pub baseline_wall_time_ns: u64,
41 pub candidate_wall_time_ns: u64,
42 pub wall_time_delta_ns: i128,
43 pub wall_time_change_percent: Option<f64>,
44 pub stages: Vec<StageComparison>,
45}
46
47fn signed_delta(candidate: u64, baseline: u64) -> i128 {
48 i128::from(candidate) - i128::from(baseline)
49}
50
51fn percent_change(candidate: u64, baseline: u64) -> Option<f64> {
52 if baseline == 0 {
53 return (candidate == 0).then_some(0.0);
54 }
55 Some((candidate as f64 - baseline as f64) * 100.0 / baseline as f64)
56}
57
58fn compare_field<T: Debug + PartialEq>(
59 differences: &mut Vec<ComparisonDifference>,
60 field: &str,
61 baseline: &T,
62 candidate: &T,
63) {
64 if baseline != candidate {
65 differences.push(ComparisonDifference {
66 version: COMPARISON_DIFFERENCE_VERSION,
67 field: field.to_owned(),
68 baseline: format!("{baseline:?}"),
69 candidate: format!("{candidate:?}"),
70 });
71 }
72}
73
74pub fn compare_profiles(baseline: &RunProfile, candidate: &RunProfile) -> ProfileComparison {
76 let mut incompatibilities = Vec::new();
77 compare_field(
78 &mut incompatibilities,
79 "profile.schema",
80 &baseline.schema,
81 &candidate.schema,
82 );
83 compare_field(
84 &mut incompatibilities,
85 "profile.version",
86 &baseline.version,
87 &candidate.version,
88 );
89 compare_field(
90 &mut incompatibilities,
91 "identity.version",
92 &baseline.identity.version,
93 &candidate.identity.version,
94 );
95 compare_field(
96 &mut incompatibilities,
97 "identity.binary_version",
98 &baseline.identity.binary_version,
99 &candidate.identity.binary_version,
100 );
101 compare_field(
102 &mut incompatibilities,
103 "identity.detector_digest",
104 &baseline.identity.detector_digest,
105 &candidate.identity.detector_digest,
106 );
107 compare_field(
108 &mut incompatibilities,
109 "identity.config_digest",
110 &baseline.identity.config_digest,
111 &candidate.identity.config_digest,
112 );
113 compare_field(
114 &mut incompatibilities,
115 "identity.source_kind",
116 &baseline.identity.source_kind,
117 &candidate.identity.source_kind,
118 );
119 compare_field(
120 &mut incompatibilities,
121 "identity.workload_class",
122 &baseline.identity.workload_class,
123 &candidate.identity.workload_class,
124 );
125 compare_field(
126 &mut incompatibilities,
127 "identity.backend_requested",
128 &baseline.identity.backend_requested,
129 &candidate.identity.backend_requested,
130 );
131 compare_field(
132 &mut incompatibilities,
133 "identity.backend_selected",
134 &baseline.identity.backend_selected,
135 &candidate.identity.backend_selected,
136 );
137 compare_field(
138 &mut incompatibilities,
139 "identity.cache_state",
140 &baseline.identity.cache_state,
141 &candidate.identity.cache_state,
142 );
143 compare_field(
144 &mut incompatibilities,
145 "identity.daemon_state",
146 &baseline.identity.daemon_state,
147 &candidate.identity.daemon_state,
148 );
149 compare_field(
150 &mut incompatibilities,
151 "identity.scanner_threads",
152 &baseline.identity.scanner_threads,
153 &candidate.identity.scanner_threads,
154 );
155 compare_field(
156 &mut incompatibilities,
157 "identity.reader_threads",
158 &baseline.identity.reader_threads,
159 &candidate.identity.reader_threads,
160 );
161 compare_field(
162 &mut incompatibilities,
163 "identity.logical_cpus",
164 &baseline.identity.logical_cpus,
165 &candidate.identity.logical_cpus,
166 );
167 compare_field(
168 &mut incompatibilities,
169 "input_bytes",
170 &baseline.input_bytes,
171 &candidate.input_bytes,
172 );
173 compare_field(
174 &mut incompatibilities,
175 "input_units",
176 &baseline.input_units,
177 &candidate.input_units,
178 );
179 compare_field(
180 &mut incompatibilities,
181 "collectors",
182 &baseline.collectors,
183 &candidate.collectors,
184 );
185
186 let stages = Stage::ALL
187 .into_iter()
188 .filter_map(|stage| {
189 let baseline_stage = baseline.stages.iter().find(|item| item.stage == stage);
190 let candidate_stage = candidate.stages.iter().find(|item| item.stage == stage);
191 if baseline_stage.is_none() && candidate_stage.is_none() {
192 return None;
193 }
194 let baseline_elapsed_ns = baseline_stage.map_or(0, |item| item.elapsed_ns);
195 let candidate_elapsed_ns = candidate_stage.map_or(0, |item| item.elapsed_ns);
196 let baseline_calls = baseline_stage.map_or(0, |item| item.calls);
197 let candidate_calls = candidate_stage.map_or(0, |item| item.calls);
198 Some(StageComparison {
199 version: STAGE_COMPARISON_VERSION,
200 stage,
201 baseline_elapsed_ns,
202 candidate_elapsed_ns,
203 elapsed_delta_ns: signed_delta(candidate_elapsed_ns, baseline_elapsed_ns),
204 elapsed_change_percent: percent_change(candidate_elapsed_ns, baseline_elapsed_ns),
205 baseline_calls,
206 candidate_calls,
207 calls_delta: signed_delta(candidate_calls, baseline_calls),
208 })
209 })
210 .collect();
211
212 ProfileComparison {
213 version: PROFILE_COMPARISON_VERSION,
214 baseline_run_id: baseline.identity.run_id.clone(),
215 candidate_run_id: candidate.identity.run_id.clone(),
216 comparable: incompatibilities.is_empty(),
217 incompatibilities,
218 baseline_wall_time_ns: baseline.wall_time_ns,
219 candidate_wall_time_ns: candidate.wall_time_ns,
220 wall_time_delta_ns: signed_delta(candidate.wall_time_ns, baseline.wall_time_ns),
221 wall_time_change_percent: percent_change(candidate.wall_time_ns, baseline.wall_time_ns),
222 stages,
223 }
224}
225
226impl ProfileComparison {
227 pub fn render_text(&self) -> String {
229 let mut output = format!(
230 "KeyHog profile comparison comparable={} baseline_run={:?} candidate_run={:?}\n",
231 self.comparable, self.baseline_run_id, self.candidate_run_id,
232 );
233 for difference in &self.incompatibilities {
234 output.push_str(&format!(
235 "incompatible field={} baseline={} candidate={}\n",
236 difference.field, difference.baseline, difference.candidate,
237 ));
238 }
239 output.push_str(&format!(
240 "wall baseline_ns={} candidate_ns={} delta_ns={}",
241 self.baseline_wall_time_ns, self.candidate_wall_time_ns, self.wall_time_delta_ns,
242 ));
243 if let Some(percent) = self.wall_time_change_percent {
244 output.push_str(&format!(" change_percent={percent:.3}"));
245 } else {
246 output.push_str(" change_percent=undefined");
247 }
248 output.push('\n');
249 for stage in &self.stages {
250 output.push_str(&format!(
251 "stage {} baseline_ns={} candidate_ns={} delta_ns={} calls={}->{}",
252 stage.stage.as_str(),
253 stage.baseline_elapsed_ns,
254 stage.candidate_elapsed_ns,
255 stage.elapsed_delta_ns,
256 stage.baseline_calls,
257 stage.candidate_calls,
258 ));
259 if let Some(percent) = stage.elapsed_change_percent {
260 output.push_str(&format!(" change_percent={percent:.3}"));
261 } else {
262 output.push_str(" change_percent=undefined");
263 }
264 output.push('\n');
265 }
266 output
267 }
268}