Skip to main content

git_perf/
measurement_retrieval.rs

1use crate::{
2    data::{Commit, CommitSummary, MeasurementData, MeasurementSummary},
3    git::git_interop::{self},
4    stats::{NumericReductionFunc, ReductionFunc},
5};
6
7use anyhow::Result;
8
9pub trait MeasurementReducer<'a>: Iterator<Item = &'a MeasurementData> {
10    fn reduce_by(self, fun: ReductionFunc) -> Option<MeasurementSummary>;
11}
12
13pub fn summarize_measurements<'a, F>(
14    commits: impl Iterator<Item = Result<Commit>> + 'a,
15    summarize_by: &'a ReductionFunc,
16    filter_by: &'a F,
17) -> impl Iterator<Item = Result<CommitSummary>> + 'a
18where
19    F: Fn(&MeasurementData) -> bool,
20{
21    commits.map(move |c| {
22        c.map(|c| {
23            let measurement = c
24                .measurements
25                .iter()
26                .filter(|m| filter_by(m))
27                .reduce_by(*summarize_by);
28
29            CommitSummary {
30                commit: c.commit,
31                measurement,
32            }
33        })
34    })
35}
36
37/// Adapter to take results while the epoch is the same as the first one encountered.
38pub fn take_while_same_epoch<I>(iter: I) -> impl Iterator<Item = Result<CommitSummary>>
39where
40    I: Iterator<Item = Result<CommitSummary>>,
41{
42    let mut first_epoch: Option<u32> = None;
43    iter.take_while(move |m| match m {
44        Ok(CommitSummary {
45            measurement: Some(m),
46            ..
47        }) => {
48            let prev_epoch = first_epoch;
49            first_epoch = Some(m.epoch);
50            prev_epoch.unwrap_or(m.epoch) == m.epoch
51        }
52        _ => true,
53    })
54}
55
56impl<'a, T> MeasurementReducer<'a> for T
57where
58    T: Iterator<Item = &'a MeasurementData>,
59{
60    fn reduce_by(self, fun: ReductionFunc) -> Option<MeasurementSummary> {
61        let mut peekable = self.peekable();
62        let expected_epoch = peekable.peek().map(|m| m.epoch);
63        let mut vals = peekable.map(|m| {
64            debug_assert_eq!(Some(m.epoch), expected_epoch);
65            m.val
66        });
67
68        let aggregate_val = vals.aggregate_by(fun);
69
70        Some(MeasurementSummary {
71            epoch: expected_epoch?,
72            val: aggregate_val?,
73        })
74    }
75}
76
77/// Walks through commit history starting from a specific commit, retrieving performance measurements.
78///
79/// This function traverses the Git commit history beginning at the specified commit
80/// and returns an iterator of commits with their associated performance measurements
81/// deserialized from git notes. The iterator yields up to `num_commits` commits,
82/// following the first-parent ancestry chain.
83///
84/// # Arguments
85///
86/// * `start_commit` - The committish reference to start walking from (e.g., "HEAD", "main", commit hash)
87/// * `num_commits` - Maximum number of commits to retrieve
88///
89/// # Returns
90///
91/// Returns an iterator that yields `Result<Commit>` for each commit in the history.
92/// Each successful `Commit` contains the commit hash and its deserialized performance measurements.
93///
94/// # Errors
95///
96/// Returns an error if:
97/// - The starting commit cannot be resolved
98/// - The repository is a shallow clone (full history required)
99/// - Git operations fail during commit traversal
100///
101/// # Notes
102///
103/// Measurements are copied during deserialization. This is necessary due to the current
104/// storage model but could be optimized with architectural changes.
105///
106/// # Examples
107///
108/// ```no_run
109/// # use git_perf::measurement_retrieval::walk_commits_from;
110/// for commit_result in walk_commits_from("HEAD", 10, None, None).unwrap() {
111///     let commit = commit_result.unwrap();
112///     println!("Commit: {}", commit.commit);
113/// }
114/// ```
115pub fn walk_commits_from(
116    start_commit: &str,
117    num_commits: usize,
118    since: Option<&str>,
119    until: Option<&str>,
120) -> Result<impl Iterator<Item = Result<Commit>>> {
121    let vec = git_interop::walk_commits_from(start_commit, num_commits, since, until)?;
122    Ok(vec
123        .into_iter()
124        .take(num_commits)
125        .map(|commit_data| -> Result<Commit> {
126            let measurements =
127                crate::serialization::deserialize(&commit_data.note_lines.join("\n"));
128            Ok(Commit {
129                commit: commit_data.sha,
130                title: commit_data.title,
131                author: commit_data.author,
132                measurements,
133            })
134        }))
135    // When this fails it is due to a shallow clone.
136}