use crate::{RunProfile, Stage};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
pub const PROFILE_COMPARISON_VERSION: u16 = 1;
pub const COMPARISON_DIFFERENCE_VERSION: u16 = 1;
pub const STAGE_COMPARISON_VERSION: u16 = 1;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ComparisonDifference {
pub version: u16,
pub field: String,
pub baseline: String,
pub candidate: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StageComparison {
pub version: u16,
pub stage: Stage,
pub baseline_elapsed_ns: u64,
pub candidate_elapsed_ns: u64,
pub elapsed_delta_ns: i128,
pub elapsed_change_percent: Option<f64>,
pub baseline_calls: u64,
pub candidate_calls: u64,
pub calls_delta: i128,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ProfileComparison {
pub version: u16,
pub baseline_run_id: String,
pub candidate_run_id: String,
pub comparable: bool,
pub incompatibilities: Vec<ComparisonDifference>,
pub baseline_wall_time_ns: u64,
pub candidate_wall_time_ns: u64,
pub wall_time_delta_ns: i128,
pub wall_time_change_percent: Option<f64>,
pub stages: Vec<StageComparison>,
}
fn signed_delta(candidate: u64, baseline: u64) -> i128 {
i128::from(candidate) - i128::from(baseline)
}
fn percent_change(candidate: u64, baseline: u64) -> Option<f64> {
if baseline == 0 {
return (candidate == 0).then_some(0.0);
}
Some((candidate as f64 - baseline as f64) * 100.0 / baseline as f64)
}
fn compare_field<T: Debug + PartialEq>(
differences: &mut Vec<ComparisonDifference>,
field: &str,
baseline: &T,
candidate: &T,
) {
if baseline != candidate {
differences.push(ComparisonDifference {
version: COMPARISON_DIFFERENCE_VERSION,
field: field.to_owned(),
baseline: format!("{baseline:?}"),
candidate: format!("{candidate:?}"),
});
}
}
pub fn compare_profiles(baseline: &RunProfile, candidate: &RunProfile) -> ProfileComparison {
let mut incompatibilities = Vec::new();
compare_field(
&mut incompatibilities,
"profile.schema",
&baseline.schema,
&candidate.schema,
);
compare_field(
&mut incompatibilities,
"profile.version",
&baseline.version,
&candidate.version,
);
compare_field(
&mut incompatibilities,
"identity.version",
&baseline.identity.version,
&candidate.identity.version,
);
compare_field(
&mut incompatibilities,
"identity.binary_version",
&baseline.identity.binary_version,
&candidate.identity.binary_version,
);
compare_field(
&mut incompatibilities,
"identity.detector_digest",
&baseline.identity.detector_digest,
&candidate.identity.detector_digest,
);
compare_field(
&mut incompatibilities,
"identity.config_digest",
&baseline.identity.config_digest,
&candidate.identity.config_digest,
);
compare_field(
&mut incompatibilities,
"identity.source_kind",
&baseline.identity.source_kind,
&candidate.identity.source_kind,
);
compare_field(
&mut incompatibilities,
"identity.workload_class",
&baseline.identity.workload_class,
&candidate.identity.workload_class,
);
compare_field(
&mut incompatibilities,
"identity.backend_requested",
&baseline.identity.backend_requested,
&candidate.identity.backend_requested,
);
compare_field(
&mut incompatibilities,
"identity.backend_selected",
&baseline.identity.backend_selected,
&candidate.identity.backend_selected,
);
compare_field(
&mut incompatibilities,
"identity.cache_state",
&baseline.identity.cache_state,
&candidate.identity.cache_state,
);
compare_field(
&mut incompatibilities,
"identity.daemon_state",
&baseline.identity.daemon_state,
&candidate.identity.daemon_state,
);
compare_field(
&mut incompatibilities,
"identity.scanner_threads",
&baseline.identity.scanner_threads,
&candidate.identity.scanner_threads,
);
compare_field(
&mut incompatibilities,
"identity.reader_threads",
&baseline.identity.reader_threads,
&candidate.identity.reader_threads,
);
compare_field(
&mut incompatibilities,
"identity.logical_cpus",
&baseline.identity.logical_cpus,
&candidate.identity.logical_cpus,
);
compare_field(
&mut incompatibilities,
"input_bytes",
&baseline.input_bytes,
&candidate.input_bytes,
);
compare_field(
&mut incompatibilities,
"input_units",
&baseline.input_units,
&candidate.input_units,
);
compare_field(
&mut incompatibilities,
"collectors",
&baseline.collectors,
&candidate.collectors,
);
let stages = Stage::ALL
.into_iter()
.filter_map(|stage| {
let baseline_stage = baseline.stages.iter().find(|item| item.stage == stage);
let candidate_stage = candidate.stages.iter().find(|item| item.stage == stage);
if baseline_stage.is_none() && candidate_stage.is_none() {
return None;
}
let baseline_elapsed_ns = baseline_stage.map_or(0, |item| item.elapsed_ns);
let candidate_elapsed_ns = candidate_stage.map_or(0, |item| item.elapsed_ns);
let baseline_calls = baseline_stage.map_or(0, |item| item.calls);
let candidate_calls = candidate_stage.map_or(0, |item| item.calls);
Some(StageComparison {
version: STAGE_COMPARISON_VERSION,
stage,
baseline_elapsed_ns,
candidate_elapsed_ns,
elapsed_delta_ns: signed_delta(candidate_elapsed_ns, baseline_elapsed_ns),
elapsed_change_percent: percent_change(candidate_elapsed_ns, baseline_elapsed_ns),
baseline_calls,
candidate_calls,
calls_delta: signed_delta(candidate_calls, baseline_calls),
})
})
.collect();
ProfileComparison {
version: PROFILE_COMPARISON_VERSION,
baseline_run_id: baseline.identity.run_id.clone(),
candidate_run_id: candidate.identity.run_id.clone(),
comparable: incompatibilities.is_empty(),
incompatibilities,
baseline_wall_time_ns: baseline.wall_time_ns,
candidate_wall_time_ns: candidate.wall_time_ns,
wall_time_delta_ns: signed_delta(candidate.wall_time_ns, baseline.wall_time_ns),
wall_time_change_percent: percent_change(candidate.wall_time_ns, baseline.wall_time_ns),
stages,
}
}
impl ProfileComparison {
pub fn render_text(&self) -> String {
let mut output = format!(
"KeyHog profile comparison comparable={} baseline_run={:?} candidate_run={:?}\n",
self.comparable, self.baseline_run_id, self.candidate_run_id,
);
for difference in &self.incompatibilities {
output.push_str(&format!(
"incompatible field={} baseline={} candidate={}\n",
difference.field, difference.baseline, difference.candidate,
));
}
output.push_str(&format!(
"wall baseline_ns={} candidate_ns={} delta_ns={}",
self.baseline_wall_time_ns, self.candidate_wall_time_ns, self.wall_time_delta_ns,
));
if let Some(percent) = self.wall_time_change_percent {
output.push_str(&format!(" change_percent={percent:.3}"));
} else {
output.push_str(" change_percent=undefined");
}
output.push('\n');
for stage in &self.stages {
output.push_str(&format!(
"stage {} baseline_ns={} candidate_ns={} delta_ns={} calls={}->{}",
stage.stage.as_str(),
stage.baseline_elapsed_ns,
stage.candidate_elapsed_ns,
stage.elapsed_delta_ns,
stage.baseline_calls,
stage.candidate_calls,
));
if let Some(percent) = stage.elapsed_change_percent {
output.push_str(&format!(" change_percent={percent:.3}"));
} else {
output.push_str(" change_percent=undefined");
}
output.push('\n');
}
output
}
}