use crate::{
data::{Commit, CommitSummary, MeasurementData, MeasurementSummary},
git::git_interop::{self},
stats::{NumericReductionFunc, ReductionFunc},
};
use anyhow::Result;
pub trait MeasurementReducer<'a>: Iterator<Item = &'a MeasurementData> {
fn reduce_by(self, fun: ReductionFunc) -> Option<MeasurementSummary>;
}
pub fn summarize_measurements<'a, F>(
commits: impl Iterator<Item = Result<Commit>> + 'a,
summarize_by: &'a ReductionFunc,
filter_by: &'a F,
) -> impl Iterator<Item = Result<CommitSummary>> + 'a
where
F: Fn(&MeasurementData) -> bool,
{
commits.map(move |c| {
c.map(|c| {
let measurement = c
.measurements
.iter()
.filter(|m| filter_by(m))
.reduce_by(*summarize_by);
CommitSummary {
commit: c.commit,
measurement,
}
})
})
}
pub fn take_while_same_epoch<I>(iter: I) -> impl Iterator<Item = Result<CommitSummary>>
where
I: Iterator<Item = Result<CommitSummary>>,
{
let mut first_epoch: Option<u32> = None;
iter.take_while(move |m| match m {
Ok(CommitSummary {
measurement: Some(m),
..
}) => {
let prev_epoch = first_epoch;
first_epoch = Some(m.epoch);
prev_epoch.unwrap_or(m.epoch) == m.epoch
}
_ => true,
})
}
pub fn collect_epoch_measurements<F>(
commits: impl Iterator<Item = Result<Commit>>,
filter_by: F,
summarize_by: &ReductionFunc,
) -> (Vec<f64>, Vec<String>)
where
F: Fn(&MeasurementData) -> bool,
{
let epoch_data: Vec<(String, f64)> =
take_while_same_epoch(summarize_measurements(commits, summarize_by, &filter_by))
.filter_map(|r| r.ok())
.filter_map(|cs| cs.measurement.map(|m| (cs.commit, m.val)))
.collect();
let values = epoch_data.iter().map(|(_, v)| *v).collect();
let shas = epoch_data.iter().map(|(c, _)| c.clone()).collect();
(values, shas)
}
impl<'a, T> MeasurementReducer<'a> for T
where
T: Iterator<Item = &'a MeasurementData>,
{
fn reduce_by(self, fun: ReductionFunc) -> Option<MeasurementSummary> {
let mut peekable = self.peekable();
let expected_epoch = peekable.peek().map(|m| m.epoch);
let mut vals = peekable.map(|m| {
debug_assert_eq!(Some(m.epoch), expected_epoch);
m.val
});
let aggregate_val = vals.aggregate_by(fun);
Some(MeasurementSummary {
epoch: expected_epoch?,
val: aggregate_val?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::{Commit, MeasurementData};
use crate::stats::ReductionFunc;
use anyhow::Result;
fn make_commit_with_measurement(sha: &str, name: &str, val: f64) -> Commit {
Commit {
commit: sha.to_string(),
title: String::new(),
author: String::new(),
measurements: vec![MeasurementData {
epoch: 1,
name: name.to_string(),
timestamp: 0.0,
val,
key_values: std::collections::HashMap::new(),
}],
}
}
fn make_commits_iter(commits: &[Result<Commit>]) -> impl Iterator<Item = Result<Commit>> + '_ {
commits.iter().map(|r| match r {
Ok(c) => Ok(c.clone()),
Err(e) => Err(anyhow::anyhow!("{}", e)),
})
}
#[test]
fn test_collect_epoch_measurements_basic() {
let commits: Vec<Result<Commit>> = (0..5)
.map(|i| {
Ok(make_commit_with_measurement(
&format!("sha{:040}", i),
"bench",
(i as f64 + 1.0) * 10.0,
))
})
.collect();
let (values, shas) = collect_epoch_measurements(
make_commits_iter(&commits),
|m: &MeasurementData| m.name == "bench",
&ReductionFunc::Min,
);
assert_eq!(values.len(), 5);
assert_eq!(shas.len(), 5);
assert!((values[0] - 10.0).abs() < f64::EPSILON); }
#[test]
fn test_collect_epoch_measurements_filters_by_name() {
let commits: Vec<Result<Commit>> =
vec![Ok(make_commit_with_measurement("sha1", "other", 99.0))];
let (values, shas) = collect_epoch_measurements(
make_commits_iter(&commits),
|m: &MeasurementData| m.name == "bench",
&ReductionFunc::Min,
);
assert!(values.is_empty());
assert!(shas.is_empty());
}
#[test]
fn test_collect_epoch_measurements_stops_at_epoch_change() {
let mut commits: Vec<Result<Commit>> = Vec::new();
for i in 0..3 {
commits.push(Ok(Commit {
commit: format!("sha{}", i),
title: String::new(),
author: String::new(),
measurements: vec![MeasurementData {
epoch: 1,
name: "bench".to_string(),
timestamp: 0.0,
val: 10.0,
key_values: std::collections::HashMap::new(),
}],
}));
}
for i in 3..5 {
commits.push(Ok(Commit {
commit: format!("sha{}", i),
title: String::new(),
author: String::new(),
measurements: vec![MeasurementData {
epoch: 2,
name: "bench".to_string(),
timestamp: 0.0,
val: 20.0,
key_values: std::collections::HashMap::new(),
}],
}));
}
let (values, _) = collect_epoch_measurements(
make_commits_iter(&commits),
|m: &MeasurementData| m.name == "bench",
&ReductionFunc::Min,
);
assert_eq!(values.len(), 3);
for v in &values {
assert!(
(v - 10.0).abs() < f64::EPSILON,
"Should only return epoch 1 values"
);
}
}
}
pub fn walk_commits_from(
start_commit: &str,
num_commits: usize,
since: Option<&str>,
until: Option<&str>,
) -> Result<impl Iterator<Item = Result<Commit>>> {
let vec = git_interop::walk_commits_from(start_commit, num_commits, since, until)?;
Ok(vec
.into_iter()
.take(num_commits)
.map(|commit_data| -> Result<Commit> {
let measurements =
crate::serialization::deserialize(&commit_data.note_lines.join("\n"));
Ok(Commit {
commit: commit_data.sha,
title: commit_data.title,
author: commit_data.author,
measurements,
})
}))
}