use std::borrow::Cow;
use std::ffi::OsString;
use std::fmt::{Debug, Display};
use std::fs::File;
use std::hash::Hash;
use std::io::stdout;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Context, Result};
use derive_more::AsRef;
use glob::glob;
use indexmap::{indexmap, IndexMap, IndexSet};
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::callgrind::Summaries;
use super::common::ModulePath;
use super::format::{Formatter, OutputFormat, OutputFormatKind, VerticalFormatter};
use super::metrics::Metrics;
use super::tool::ValgrindTool;
use crate::api::{DhatMetricKind, ErrorMetricKind, EventKind};
use crate::error::Error;
use crate::runner::metrics::Summarize;
use crate::util::{factor_diff, make_absolute, percentage_diff, EitherOrBoth};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct Baseline {
pub kind: BaselineKind,
pub path: PathBuf,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct BaselineName(String);
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum BaselineKind {
Old,
Name(BaselineName),
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum BenchmarkKind {
LibraryBenchmark,
BinaryBenchmark,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct BenchmarkSummary {
pub version: String,
pub kind: BenchmarkKind,
pub summary_output: Option<SummaryOutput>,
pub project_root: PathBuf,
pub package_dir: PathBuf,
pub benchmark_file: PathBuf,
pub benchmark_exe: PathBuf,
pub function_name: String,
pub module_path: String,
pub id: Option<String>,
pub details: Option<String>,
pub callgrind_summary: Option<CallgrindSummary>,
pub tool_summaries: Vec<ToolSummary>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct CallgrindRegression {
pub event_kind: EventKind,
pub new: u64,
pub old: u64,
#[serde(with = "crate::serde::float_64")]
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub diff_pct: f64,
#[serde(with = "crate::serde::float_64")]
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub limit: f64,
}
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct CallgrindRun {
pub segments: Vec<CallgrindRunSegment>,
pub total: CallgrindTotal,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct CallgrindRunSegment {
pub command: String,
pub baseline: Option<Baseline>,
pub events: MetricsSummary<EventKind>,
pub regressions: Vec<CallgrindRegression>,
}
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct CallgrindTotal {
pub summary: MetricsSummary,
pub regressions: Vec<CallgrindRegression>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct CallgrindSummary {
pub log_paths: Vec<PathBuf>,
pub out_paths: Vec<PathBuf>,
pub flamegraphs: Vec<FlamegraphSummary>,
pub callgrind_run: CallgrindRun,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct MetricsDiff {
pub metrics: EitherOrBoth<u64>,
pub diffs: Option<Diffs>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum ToolMetrics {
#[default]
None,
DhatMetrics(Metrics<DhatMetricKind>),
ErrorMetrics(Metrics<ErrorMetricKind>),
CallgrindMetrics(Metrics<EventKind>),
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct MetricsSummary<K: Hash + Eq = EventKind>(IndexMap<K, MetricsDiff>);
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum ToolMetricSummary {
#[default]
None,
ErrorSummary(MetricsSummary<ErrorMetricKind>),
DhatSummary(MetricsSummary<DhatMetricKind>),
CallgrindSummary(MetricsSummary<EventKind>),
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct Diffs {
#[serde(with = "crate::serde::float_64")]
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub diff_pct: f64,
#[serde(with = "crate::serde::float_64")]
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub factor: f64,
}
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct FlamegraphSummaries {
pub summaries: Vec<FlamegraphSummary>,
pub totals: Vec<FlamegraphSummary>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct FlamegraphSummary {
pub event_kind: EventKind,
pub regular_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub diff_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum SummaryFormat {
Json,
PrettyJson,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct SummaryOutput {
format: SummaryFormat,
path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, AsRef)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct SegmentDetails {
pub command: String,
pub pid: i32,
pub parent_pid: Option<i32>,
pub details: Option<String>,
pub path: PathBuf,
pub part: Option<u64>,
pub thread: Option<usize>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct ToolRun {
pub segments: Vec<ToolRunSegment>,
pub total: ToolMetricSummary,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct ToolRunSegment {
pub details: EitherOrBoth<SegmentDetails>,
pub metrics_summary: ToolMetricSummary,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct ToolSummary {
pub tool: ValgrindTool,
pub log_paths: Vec<PathBuf>,
pub out_paths: Vec<PathBuf>,
pub summaries: ToolRun,
}
impl FromStr for BaselineName {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
for char in s.chars() {
if !(char.is_ascii_alphanumeric() || char == '_') {
return Err(format!(
"A baseline name can only consist of ascii characters which are alphanumeric \
or '_' but found: '{char}'"
));
}
}
Ok(Self(s.to_owned()))
}
}
impl Display for BaselineName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl BenchmarkSummary {
pub fn new(
kind: BenchmarkKind,
project_root: PathBuf,
package_dir: PathBuf,
benchmark_file: PathBuf,
benchmark_exe: PathBuf,
module_path: &ModulePath,
function_name: &str,
id: Option<String>,
details: Option<String>,
output: Option<SummaryOutput>,
) -> Self {
Self {
version: "3".to_owned(),
kind,
benchmark_file: make_absolute(&project_root, benchmark_file),
benchmark_exe: make_absolute(&project_root, benchmark_exe),
module_path: module_path.to_string(),
function_name: function_name.to_owned(),
id,
details,
callgrind_summary: None,
tool_summaries: vec![],
summary_output: output,
project_root,
package_dir,
}
}
pub fn print_and_save(&self, output_format: &OutputFormatKind) -> Result<()> {
let value = match (output_format, &self.summary_output) {
(OutputFormatKind::Default, None) => return Ok(()),
_ => {
serde_json::to_value(self).with_context(|| "Failed to serialize summary to json")?
}
};
let result = match output_format {
OutputFormatKind::Default => Ok(()),
OutputFormatKind::Json => {
let output = stdout();
let writer = output.lock();
let result = serde_json::to_writer(writer, &value);
println!();
result
}
OutputFormatKind::PrettyJson => {
let output = stdout();
let writer = output.lock();
let result = serde_json::to_writer_pretty(writer, &value);
println!();
result
}
};
result.with_context(|| "Failed to print json to stdout")?;
if let Some(output) = &self.summary_output {
let file = output.create()?;
let result = if matches!(output.format, SummaryFormat::PrettyJson) {
serde_json::to_writer_pretty(file, &value)
} else {
serde_json::to_writer(file, &value)
};
result.with_context(|| {
format!("Failed to write summary to file: {}", output.path.display())
})?;
}
Ok(())
}
pub fn check_regression(&self, is_regressed: &mut bool, fail_fast: bool) -> Result<()> {
if let Some(callgrind_summary) = &self.callgrind_summary {
let benchmark_is_regressed = callgrind_summary.is_regressed();
if benchmark_is_regressed && fail_fast {
return Err(Error::RegressionError(true).into());
}
*is_regressed |= benchmark_is_regressed;
}
Ok(())
}
pub fn compare_and_print(
&self,
id: &str,
other: &Self,
output_format: &OutputFormat,
) -> Result<()> {
if let (Some(callgrind_summary), Some(other_callgrind_summary)) =
(&self.callgrind_summary, &other.callgrind_summary)
{
if let (
EitherOrBoth::Left(new) | EitherOrBoth::Both(new, _),
EitherOrBoth::Left(other_new) | EitherOrBoth::Both(other_new, _),
) = (
callgrind_summary
.callgrind_run
.total
.summary
.extract_costs(),
other_callgrind_summary
.callgrind_run
.total
.summary
.extract_costs(),
) {
let new_summary = MetricsSummary::new(EitherOrBoth::Both(new, other_new));
VerticalFormatter::new(*output_format).print_comparison(
&self.function_name,
id,
self.details.as_deref(),
&ToolMetricSummary::CallgrindSummary(new_summary),
)?;
}
}
Ok(())
}
}
impl CallgrindSummary {
pub fn new(log_paths: Vec<PathBuf>, out_paths: Vec<PathBuf>) -> CallgrindSummary {
Self {
log_paths,
out_paths,
flamegraphs: Vec::default(),
callgrind_run: CallgrindRun::default(),
}
}
pub fn is_regressed(&self) -> bool {
self.callgrind_run
.segments
.iter()
.any(|r| !r.regressions.is_empty())
}
pub fn add_summaries(
&mut self,
bench_bin: &Path,
bench_args: &[OsString],
baselines: &(Option<String>, Option<String>),
summaries: Summaries,
regressions: Vec<CallgrindRegression>,
) {
let command = format!(
"{} {}",
bench_bin.display(),
shlex::try_join(
bench_args
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<String>>()
.as_slice()
.iter()
.map(String::as_str)
)
.unwrap()
);
for summary in summaries.summaries {
let old_baseline = match summary.details {
EitherOrBoth::Left(_) => None,
EitherOrBoth::Both(_, old) | EitherOrBoth::Right(old) => Some(Baseline {
kind: baselines.1.as_ref().map_or(BaselineKind::Old, |name| {
BaselineKind::Name(BaselineName(name.to_owned()))
}),
path: old.0,
}),
};
self.callgrind_run.segments.push(CallgrindRunSegment {
command: command.clone(),
baseline: old_baseline,
events: summary.metrics_summary,
regressions: vec![],
});
}
self.callgrind_run.total.summary = summaries.total.clone();
self.callgrind_run.total.regressions = regressions;
}
}
impl MetricsDiff {
pub fn new(metrics: EitherOrBoth<u64>) -> Self {
if let EitherOrBoth::Both(new, old) = metrics {
Self {
metrics,
diffs: Some(Diffs::new(new, old)),
}
} else {
Self {
metrics,
diffs: None,
}
}
}
pub fn add(&self, other: &Self) -> Self {
match (&self.metrics, &other.metrics) {
(EitherOrBoth::Left(new), EitherOrBoth::Left(other_new)) => {
Self::new(EitherOrBoth::Left(new.saturating_add(*other_new)))
}
(EitherOrBoth::Right(old), EitherOrBoth::Left(new))
| (EitherOrBoth::Left(new), EitherOrBoth::Right(old)) => {
Self::new(EitherOrBoth::Both(*new, *old))
}
(EitherOrBoth::Right(old), EitherOrBoth::Right(other_old)) => {
Self::new(EitherOrBoth::Right(old.saturating_add(*other_old)))
}
(EitherOrBoth::Both(new, old), EitherOrBoth::Left(other_new))
| (EitherOrBoth::Left(new), EitherOrBoth::Both(other_new, old)) => {
Self::new(EitherOrBoth::Both(new.saturating_add(*other_new), *old))
}
(EitherOrBoth::Both(new, old), EitherOrBoth::Right(other_old))
| (EitherOrBoth::Right(old), EitherOrBoth::Both(new, other_old)) => {
Self::new(EitherOrBoth::Both(*new, old.saturating_add(*other_old)))
}
(EitherOrBoth::Both(new, old), EitherOrBoth::Both(other_new, other_old)) => {
Self::new(EitherOrBoth::Both(
new.saturating_add(*other_new),
old.saturating_add(*other_old),
))
}
}
}
}
impl Diffs {
pub fn new(new: u64, old: u64) -> Self {
Self {
diff_pct: percentage_diff(new, old),
factor: factor_diff(new, old),
}
}
}
impl<K> MetricsSummary<K>
where
K: Hash + Eq + Summarize + Display + Clone,
{
pub fn new(metrics: EitherOrBoth<Metrics<K>>) -> Self {
match metrics {
EitherOrBoth::Left(new) => {
assert!(!new.is_empty());
let mut new = Cow::Owned(new);
K::summarize(&mut new);
Self(
new.iter()
.map(|(metric_kind, metric)| {
(
metric_kind.clone(),
MetricsDiff::new(EitherOrBoth::Left(*metric)),
)
})
.collect::<IndexMap<_, _>>(),
)
}
EitherOrBoth::Right(old) => {
assert!(!old.is_empty());
let mut old = Cow::Owned(old);
K::summarize(&mut old);
Self(
old.iter()
.map(|(metric_kind, metric)| {
(
metric_kind.clone(),
MetricsDiff::new(EitherOrBoth::Right(*metric)),
)
})
.collect::<IndexMap<_, _>>(),
)
}
EitherOrBoth::Both(new, old) => {
assert!(!new.is_empty());
assert!(!old.is_empty());
let mut new = Cow::Owned(new);
K::summarize(&mut new);
let mut old = Cow::Owned(old);
K::summarize(&mut old);
let mut map = indexmap! {};
for metric_kind in new.metric_kinds_union(&old) {
let diff = match (
new.metric_by_kind(metric_kind),
old.metric_by_kind(metric_kind),
) {
(Some(metric), None) => MetricsDiff::new(EitherOrBoth::Left(metric)),
(None, Some(metric)) => MetricsDiff::new(EitherOrBoth::Right(metric)),
(Some(new), Some(old)) => MetricsDiff::new(EitherOrBoth::Both(new, old)),
(None, None) => {
unreachable!(
"The union contains the event kinds either from new or old or \
from both"
)
}
};
map.insert(metric_kind.clone(), diff);
}
Self(map)
}
}
}
pub fn diff_by_kind(&self, metric_kind: &K) -> Option<&MetricsDiff> {
self.0.get(metric_kind)
}
pub fn all_diffs(&self) -> impl Iterator<Item = (&K, &MetricsDiff)> {
self.0.iter()
}
pub fn extract_costs(&self) -> EitherOrBoth<Metrics<K>> {
let mut new_metrics: Metrics<K> = Metrics::empty();
let mut old_metrics: Metrics<K> = Metrics::empty();
for (metric_kind, diff) in self.all_diffs() {
match diff.metrics {
EitherOrBoth::Left(new) => {
new_metrics.insert(metric_kind.clone(), new);
}
EitherOrBoth::Right(old) => {
old_metrics.insert(metric_kind.clone(), old);
}
EitherOrBoth::Both(new, old) => {
new_metrics.insert(metric_kind.clone(), new);
old_metrics.insert(metric_kind.clone(), old);
}
}
}
match (new_metrics.is_empty(), old_metrics.is_empty()) {
(false, false) => EitherOrBoth::Both(new_metrics, old_metrics),
(false, true) => EitherOrBoth::Left(new_metrics),
(true, false) => EitherOrBoth::Right(old_metrics),
(true, true) => unreachable!("A costs diff contains new or old values or both."),
}
}
pub fn add(&mut self, other: &Self) {
let other_keys = other.0.keys().cloned().collect::<IndexSet<_>>();
let keys = self.0.keys().cloned().collect::<IndexSet<_>>();
let union = keys.union(&other_keys);
for key in union {
match (self.diff_by_kind(key), other.diff_by_kind(key)) {
(None, None) => unreachable!("One key of the union set must be present"),
(None, Some(other_diff)) => {
self.0.insert(key.clone(), other_diff.clone());
}
(Some(_), None) => {
}
(Some(this_diff), Some(other_diff)) => {
let new_diff = this_diff.add(other_diff);
self.0.insert(key.clone(), new_diff);
}
}
}
}
}
impl<K> Default for MetricsSummary<K>
where
K: Hash + Eq,
{
fn default() -> Self {
Self(IndexMap::default())
}
}
impl FlamegraphSummary {
pub fn new(event_kind: EventKind) -> Self {
Self {
event_kind,
regular_path: Option::default(),
base_path: Option::default(),
diff_path: Option::default(),
}
}
}
impl SummaryOutput {
pub fn new(format: SummaryFormat, dir: &Path) -> Self {
Self {
format,
path: dir.join("summary.json"),
}
}
pub fn init(&self) -> Result<()> {
for entry in glob(self.path.with_extension("*").to_string_lossy().as_ref())
.expect("Glob pattern should be valid")
{
let entry = entry?;
std::fs::remove_file(entry.as_path()).with_context(|| {
format!(
"Failed removing summary file '{}'",
entry.as_path().display()
)
})?;
}
Ok(())
}
pub fn create(&self) -> Result<File> {
File::create(&self.path).with_context(|| "Failed to create json summary file")
}
}
impl ToolMetricSummary {
pub fn add_mut(&mut self, other: &Self) {
match (self, other) {
(ToolMetricSummary::ErrorSummary(this), ToolMetricSummary::ErrorSummary(other)) => {
this.add(other);
}
(ToolMetricSummary::DhatSummary(this), ToolMetricSummary::DhatSummary(other)) => {
this.add(other);
}
(
ToolMetricSummary::CallgrindSummary(this),
ToolMetricSummary::CallgrindSummary(other),
) => {
this.add(other);
}
_ => {}
}
}
pub fn from_new_metrics(metrics: &ToolMetrics) -> Self {
match metrics {
ToolMetrics::None => ToolMetricSummary::None,
ToolMetrics::DhatMetrics(metrics) => ToolMetricSummary::DhatSummary(
MetricsSummary::new(EitherOrBoth::Left(metrics.clone())),
),
ToolMetrics::ErrorMetrics(metrics) => ToolMetricSummary::ErrorSummary(
MetricsSummary::new(EitherOrBoth::Left(metrics.clone())),
),
ToolMetrics::CallgrindMetrics(metrics) => ToolMetricSummary::CallgrindSummary(
MetricsSummary::new(EitherOrBoth::Left(metrics.clone())),
),
}
}
pub fn from_old_metrics(metrics: &ToolMetrics) -> Self {
match metrics {
ToolMetrics::None => ToolMetricSummary::None,
ToolMetrics::DhatMetrics(metrics) => ToolMetricSummary::DhatSummary(
MetricsSummary::new(EitherOrBoth::Right(metrics.clone())),
),
ToolMetrics::ErrorMetrics(metrics) => ToolMetricSummary::ErrorSummary(
MetricsSummary::new(EitherOrBoth::Right(metrics.clone())),
),
ToolMetrics::CallgrindMetrics(metrics) => ToolMetricSummary::CallgrindSummary(
MetricsSummary::new(EitherOrBoth::Right(metrics.clone())),
),
}
}
pub fn try_from_new_and_old_metrics(
new_metrics: &ToolMetrics,
old_metrics: &ToolMetrics,
) -> Result<Self> {
match (new_metrics, old_metrics) {
(ToolMetrics::None, ToolMetrics::None) => Ok(ToolMetricSummary::None),
(ToolMetrics::DhatMetrics(new_metrics), ToolMetrics::DhatMetrics(old_metrics)) => {
Ok(ToolMetricSummary::DhatSummary(MetricsSummary::new(
EitherOrBoth::Both(new_metrics.clone(), old_metrics.clone()),
)))
}
(ToolMetrics::ErrorMetrics(new_metrics), ToolMetrics::ErrorMetrics(old_metrics)) => {
Ok(ToolMetricSummary::ErrorSummary(MetricsSummary::new(
EitherOrBoth::Both(new_metrics.clone(), old_metrics.clone()),
)))
}
(
ToolMetrics::CallgrindMetrics(new_metrics),
ToolMetrics::CallgrindMetrics(old_metrics),
) => Ok(ToolMetricSummary::CallgrindSummary(MetricsSummary::new(
EitherOrBoth::Both(new_metrics.clone(), old_metrics.clone()),
))),
_ => Err(anyhow!("Cannot create summary from incompatible costs")),
}
}
pub fn is_some(&self) -> bool {
!self.is_none()
}
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
impl ToolRun {
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
pub fn has_multiple(&self) -> bool {
self.segments.len() > 1
}
}
impl ToolRunSegment {
pub fn new_has_errors(&self) -> bool {
match &self.metrics_summary {
ToolMetricSummary::None
| ToolMetricSummary::DhatSummary(_)
| ToolMetricSummary::CallgrindSummary(_) => false,
ToolMetricSummary::ErrorSummary(metrics) => metrics
.diff_by_kind(&ErrorMetricKind::Errors)
.map_or(false, |e| match e.metrics {
EitherOrBoth::Left(new) | EitherOrBoth::Both(new, _) => new > 0,
EitherOrBoth::Right(_) => false,
}),
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use EventKind::*;
use super::*;
fn expected_metrics_diff<D>(metrics: EitherOrBoth<u64>, diffs: D) -> MetricsDiff
where
D: Into<Option<(f64, f64)>>,
{
MetricsDiff {
metrics,
diffs: diffs
.into()
.map(|(diff_pct, factor)| Diffs { diff_pct, factor }),
}
}
fn metrics_fixture(metrics: &[u64]) -> Metrics<EventKind> {
let event_kinds = [
Ir,
Dr,
Dw,
I1mr,
D1mr,
D1mw,
ILmr,
DLmr,
DLmw,
L1hits,
LLhits,
RamHits,
TotalRW,
EstimatedCycles,
];
Metrics::with_metric_kinds(
event_kinds
.iter()
.zip(metrics.iter())
.map(|(e, v)| (*e, *v)),
)
}
fn metrics_summary_fixture<T>(kinds: &[(EitherOrBoth<u64>, T)]) -> MetricsSummary<EventKind>
where
T: Into<Option<(f64, f64)>> + Clone,
{
let event_kinds = [
Ir,
Dr,
Dw,
I1mr,
D1mr,
D1mw,
ILmr,
DLmr,
DLmw,
L1hits,
LLhits,
RamHits,
TotalRW,
EstimatedCycles,
];
let map: IndexMap<EventKind, MetricsDiff> = event_kinds
.iter()
.zip(kinds.iter())
.map(|(e, (m, d))| (*e, expected_metrics_diff(m.clone(), d.clone())))
.collect();
MetricsSummary(map)
}
#[rstest]
#[case::new_zero(EitherOrBoth::Left(0), None)]
#[case::new_one(EitherOrBoth::Left(1), None)]
#[case::new_u64_max(EitherOrBoth::Left(u64::MAX), None)]
#[case::old_zero(EitherOrBoth::Right(0), None)]
#[case::old_one(EitherOrBoth::Right(1), None)]
#[case::old_u64_max(EitherOrBoth::Right(u64::MAX), None)]
#[case::both_zero(
EitherOrBoth::Both(0, 0),
(0f64, 1f64)
)]
#[case::both_one(
EitherOrBoth::Both(1, 1),
(0f64, 1f64)
)]
#[case::both_u64_max(
EitherOrBoth::Both(u64::MAX, u64::MAX),
(0f64, 1f64)
)]
#[case::new_one_old_zero(
EitherOrBoth::Both(1, 0),
(f64::INFINITY, f64::INFINITY)
)]
#[case::new_one_old_two(
EitherOrBoth::Both(1, 2),
(-50f64, -2f64)
)]
#[case::new_zero_old_one(
EitherOrBoth::Both(0, 1),
(-100f64, f64::NEG_INFINITY)
)]
#[case::new_two_old_one(
EitherOrBoth::Both(2, 1),
(100f64, 2f64)
)]
fn test_metrics_diff_new<T>(#[case] metrics: EitherOrBoth<u64>, #[case] expected_diffs: T)
where
T: Into<Option<(f64, f64)>>,
{
let expected = expected_metrics_diff(metrics.clone(), expected_diffs);
let actual = MetricsDiff::new(metrics);
assert_eq!(actual, expected);
}
#[rstest]
#[case::new_new(EitherOrBoth::Left(1), EitherOrBoth::Left(2), EitherOrBoth::Left(3))]
#[case::new_old(
EitherOrBoth::Left(1),
EitherOrBoth::Right(2),
EitherOrBoth::Both(1, 2)
)]
#[case::new_both(
EitherOrBoth::Left(1),
EitherOrBoth::Both(2, 5),
EitherOrBoth::Both(3, 5)
)]
#[case::old_old(EitherOrBoth::Right(1), EitherOrBoth::Right(2), EitherOrBoth::Right(3))]
#[case::old_new(
EitherOrBoth::Right(1),
EitherOrBoth::Left(2),
EitherOrBoth::Both(2, 1)
)]
#[case::old_both(
EitherOrBoth::Right(1),
EitherOrBoth::Both(2, 5),
EitherOrBoth::Both(2, 6)
)]
#[case::both_new(
EitherOrBoth::Both(2, 5),
EitherOrBoth::Left(1),
EitherOrBoth::Both(3, 5)
)]
#[case::both_old(
EitherOrBoth::Both(2, 5),
EitherOrBoth::Right(1),
EitherOrBoth::Both(2, 6)
)]
#[case::both_both(
EitherOrBoth::Both(2, 5),
EitherOrBoth::Both(1, 3),
EitherOrBoth::Both(3, 8)
)]
#[case::saturating_new(
EitherOrBoth::Left(u64::MAX),
EitherOrBoth::Left(1),
EitherOrBoth::Left(u64::MAX)
)]
#[case::saturating_new_other(
EitherOrBoth::Left(1),
EitherOrBoth::Left(u64::MAX),
EitherOrBoth::Left(u64::MAX)
)]
#[case::saturating_old(
EitherOrBoth::Right(u64::MAX),
EitherOrBoth::Right(1),
EitherOrBoth::Right(u64::MAX)
)]
#[case::saturating_old_other(
EitherOrBoth::Right(1),
EitherOrBoth::Right(u64::MAX),
EitherOrBoth::Right(u64::MAX)
)]
#[case::saturating_both(
EitherOrBoth::Both(u64::MAX, u64::MAX),
EitherOrBoth::Both(1, 1),
EitherOrBoth::Both(u64::MAX, u64::MAX)
)]
#[case::saturating_both_other(
EitherOrBoth::Both(1, 1),
EitherOrBoth::Both(u64::MAX, u64::MAX),
EitherOrBoth::Both(u64::MAX, u64::MAX)
)]
fn test_metrics_diff_add(
#[case] metric: EitherOrBoth<u64>,
#[case] other_metric: EitherOrBoth<u64>,
#[case] expected: EitherOrBoth<u64>,
) {
let new_diff = MetricsDiff::new(metric);
let old_diff = MetricsDiff::new(other_metric);
let expected = MetricsDiff::new(expected);
assert_eq!(new_diff.add(&old_diff), expected);
assert_eq!(old_diff.add(&new_diff), expected);
}
#[rstest]
#[case::new_ir(&[0], &[], &[(EitherOrBoth::Left(0), None)])]
#[case::new_is_summarized(&[10, 20, 30, 1, 2, 3, 4, 2, 0], &[],
&[
(EitherOrBoth::Left(10), None),
(EitherOrBoth::Left(20), None),
(EitherOrBoth::Left(30), None),
(EitherOrBoth::Left(1), None),
(EitherOrBoth::Left(2), None),
(EitherOrBoth::Left(3), None),
(EitherOrBoth::Left(4), None),
(EitherOrBoth::Left(2), None),
(EitherOrBoth::Left(0), None),
(EitherOrBoth::Left(54), None),
(EitherOrBoth::Left(0), None),
(EitherOrBoth::Left(6), None),
(EitherOrBoth::Left(60), None),
(EitherOrBoth::Left(264), None),
]
)]
#[case::old_ir(&[], &[0], &[(EitherOrBoth::Right(0), None)])]
#[case::old_is_summarized(&[], &[5, 10, 15, 1, 2, 3, 4, 1, 0],
&[
(EitherOrBoth::Right(5), None),
(EitherOrBoth::Right(10), None),
(EitherOrBoth::Right(15), None),
(EitherOrBoth::Right(1), None),
(EitherOrBoth::Right(2), None),
(EitherOrBoth::Right(3), None),
(EitherOrBoth::Right(4), None),
(EitherOrBoth::Right(1), None),
(EitherOrBoth::Right(0), None),
(EitherOrBoth::Right(24), None),
(EitherOrBoth::Right(1), None),
(EitherOrBoth::Right(5), None),
(EitherOrBoth::Right(30), None),
(EitherOrBoth::Right(204), None),
]
)]
#[case::new_and_old_ir_zero(&[0], &[0], &[(EitherOrBoth::Both(0, 0), (0f64, 1f64))])]
#[case::new_and_old_summarized_when_equal(
&[10, 20, 30, 1, 2, 3, 4, 2, 0],
&[10, 20, 30, 1, 2, 3, 4, 2, 0],
&[
(EitherOrBoth::Both(10, 10), (0f64, 1f64)),
(EitherOrBoth::Both(20, 20), (0f64, 1f64)),
(EitherOrBoth::Both(30, 30), (0f64, 1f64)),
(EitherOrBoth::Both(1, 1), (0f64, 1f64)),
(EitherOrBoth::Both(2, 2), (0f64, 1f64)),
(EitherOrBoth::Both(3, 3), (0f64, 1f64)),
(EitherOrBoth::Both(4, 4), (0f64, 1f64)),
(EitherOrBoth::Both(2, 2), (0f64, 1f64)),
(EitherOrBoth::Both(0, 0), (0f64, 1f64)),
(EitherOrBoth::Both(54, 54), (0f64, 1f64)),
(EitherOrBoth::Both(0, 0), (0f64, 1f64)),
(EitherOrBoth::Both(6, 6), (0f64, 1f64)),
(EitherOrBoth::Both(60, 60), (0f64, 1f64)),
(EitherOrBoth::Both(264, 264), (0f64, 1f64)),
]
)]
#[case::new_and_old_summarized_when_not_equal(
&[10, 20, 30, 1, 2, 3, 4, 2, 0],
&[5, 10, 15, 1, 2, 3, 4, 1, 0],
&[
(EitherOrBoth::Both(10, 5), (100f64, 2f64)),
(EitherOrBoth::Both(20, 10), (100f64, 2f64)),
(EitherOrBoth::Both(30, 15), (100f64, 2f64)),
(EitherOrBoth::Both(1, 1), (0f64, 1f64)),
(EitherOrBoth::Both(2, 2), (0f64, 1f64)),
(EitherOrBoth::Both(3, 3), (0f64, 1f64)),
(EitherOrBoth::Both(4, 4), (0f64, 1f64)),
(EitherOrBoth::Both(2, 1), (100f64, 2f64)),
(EitherOrBoth::Both(0, 0), (0f64, 1f64)),
(EitherOrBoth::Both(54, 24), (125f64, 2.25f64)),
(EitherOrBoth::Both(0, 1), (-100f64, f64::NEG_INFINITY)),
(EitherOrBoth::Both(6, 5), (20f64, 1.2f64)),
(EitherOrBoth::Both(60, 30), (100f64, 2f64)),
(EitherOrBoth::Both(264, 204),
(29.411_764_705_882_355_f64, 1.294_117_647_058_823_6_f64)
),
]
)]
fn test_metrics_summary_new<V>(
#[case] new_metrics: &[u64],
#[case] old_metrics: &[u64],
#[case] expected: &[(EitherOrBoth<u64>, V)],
) where
V: Into<Option<(f64, f64)>> + Clone,
{
let expected_metrics_summary = metrics_summary_fixture(expected);
let actual = match (
(!new_metrics.is_empty()).then_some(new_metrics),
(!old_metrics.is_empty()).then_some(old_metrics),
) {
(None, None) => unreachable!(),
(Some(new), None) => MetricsSummary::new(EitherOrBoth::Left(metrics_fixture(new))),
(None, Some(old)) => MetricsSummary::new(EitherOrBoth::Right(metrics_fixture(old))),
(Some(new), Some(old)) => MetricsSummary::new(EitherOrBoth::Both(
metrics_fixture(new),
metrics_fixture(old),
)),
};
assert_eq!(actual, expected_metrics_summary);
}
}