use crate::ancestry_index::{CommitGraph, SourceState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompatClass(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CandidateSnapshot {
pub snapshot_id: u64,
pub state: SourceState,
pub class: CompatClass,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CostHistory {
pub full_build_ms: u64,
pub materialize_ms: u64,
pub per_generation_ms: u64,
}
impl CostHistory {
#[must_use]
pub const fn warm_ms(&self, distance: u32) -> u64 {
self.materialize_ms + (distance as u64) * self.per_generation_ms
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColdCause {
NoCompatibleAncestor,
WarmNotWorthIt {
warm_ms: u64,
full_ms: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Selection {
WarmStart {
snapshot_id: u64,
distance: u32,
estimated_saving_ms: u64,
},
ColdBuild(ColdCause),
}
fn nearest_compatible(
candidates: &[CandidateSnapshot],
target: SourceState,
target_class: CompatClass,
graph: &CommitGraph,
) -> Option<(CandidateSnapshot, u32)> {
let compatible: Vec<&CandidateSnapshot> = candidates
.iter()
.filter(|c| c.class == target_class)
.collect();
if let Some(exact) = compatible.iter().find(|c| c.state == target).or_else(|| {
compatible
.iter()
.find(|c| c.state.commit == target.commit && c.state.dirty_digest.is_none())
}) {
return Some((**exact, 0));
}
let mut frontier = vec![target.commit];
let mut seen = Vec::new();
let mut distance = 0_u32;
while !frontier.is_empty() {
if distance > 0 {
for &commit in &frontier {
if let Some(hit) = compatible
.iter()
.find(|c| c.state.commit == commit && c.state.dirty_digest.is_none())
{
return Some((**hit, distance));
}
}
}
let mut next = Vec::new();
for &commit in &frontier {
if seen.contains(&commit) {
continue;
}
seen.push(commit);
next.extend(graph.parents_of(commit));
}
frontier = next;
distance += 1;
}
None
}
#[must_use]
pub fn select(
candidates: &[CandidateSnapshot],
target: SourceState,
target_class: CompatClass,
graph: &CommitGraph,
history: &CostHistory,
) -> Selection {
let Some((candidate, distance)) = nearest_compatible(candidates, target, target_class, graph)
else {
return Selection::ColdBuild(ColdCause::NoCompatibleAncestor);
};
let warm_ms = history.warm_ms(distance);
if warm_ms >= history.full_build_ms {
return Selection::ColdBuild(ColdCause::WarmNotWorthIt {
warm_ms,
full_ms: history.full_build_ms,
});
}
Selection::WarmStart {
snapshot_id: candidate.snapshot_id,
distance,
estimated_saving_ms: history.full_build_ms - warm_ms,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ancestry_index::CommitId;
fn clean(n: u64) -> SourceState {
SourceState {
commit: CommitId(n),
dirty_digest: None,
}
}
fn graph() -> CommitGraph {
let mut g = CommitGraph::default();
g.insert(CommitId(1), &[]);
g.insert(CommitId(10), &[CommitId(1)]);
g.insert(CommitId(11), &[CommitId(10)]);
g.insert(CommitId(20), &[CommitId(1)]);
g
}
const NIGHTLY_A: CompatClass = CompatClass(0xA);
const NIGHTLY_B: CompatClass = CompatClass(0xB);
fn history() -> CostHistory {
CostHistory {
full_build_ms: 60_000,
materialize_ms: 2_000,
per_generation_ms: 8_000,
}
}
fn candidate(snapshot_id: u64, commit: u64, class: CompatClass) -> CandidateSnapshot {
CandidateSnapshot {
snapshot_id,
state: clean(commit),
class,
}
}
#[test]
fn exact_state_and_class_wins_at_distance_zero() {
let candidates = [candidate(100, 1, NIGHTLY_A), candidate(111, 11, NIGHTLY_A)];
assert_eq!(
select(&candidates, clean(11), NIGHTLY_A, &graph(), &history()),
Selection::WarmStart {
snapshot_id: 111,
distance: 0,
estimated_saving_ms: 58_000, }
);
}
#[test]
fn a_nearer_incompatible_snapshot_loses_to_a_farther_compatible_one() {
let candidates = [
candidate(911, 11, NIGHTLY_B), candidate(100, 1, NIGHTLY_A), ];
let selection = select(&candidates, clean(11), NIGHTLY_A, &graph(), &history());
assert_eq!(
selection,
Selection::WarmStart {
snapshot_id: 100,
distance: 2,
estimated_saving_ms: 60_000 - (2_000 + 2 * 8_000),
}
);
}
#[test]
fn no_compatible_ancestor_is_a_cold_build_never_cross_class() {
let candidates = [candidate(911, 11, NIGHTLY_B), candidate(900, 1, NIGHTLY_B)];
assert_eq!(
select(&candidates, clean(11), NIGHTLY_A, &graph(), &history()),
Selection::ColdBuild(ColdCause::NoCompatibleAncestor)
);
}
#[test]
fn the_cost_benefit_estimate_can_reject_a_far_warm_start() {
let mut g = CommitGraph::default();
g.insert(CommitId(0), &[]);
for n in 1..=8_u64 {
g.insert(CommitId(n), &[CommitId(n - 1)]);
}
let candidates = [candidate(50, 0, NIGHTLY_A)];
assert_eq!(
select(&candidates, clean(8), NIGHTLY_A, &g, &history()),
Selection::ColdBuild(ColdCause::WarmNotWorthIt {
warm_ms: 66_000,
full_ms: 60_000,
})
);
assert_eq!(
select(&candidates, clean(3), NIGHTLY_A, &g, &history()),
Selection::WarmStart {
snapshot_id: 50,
distance: 3,
estimated_saving_ms: 34_000,
}
);
}
#[test]
fn cross_branch_selection_uses_the_shared_ancestor() {
let candidates = [
candidate(111, 11, NIGHTLY_A), candidate(100, 1, NIGHTLY_A), ];
assert_eq!(
select(&candidates, clean(20), NIGHTLY_A, &graph(), &history()),
Selection::WarmStart {
snapshot_id: 100,
distance: 1,
estimated_saving_ms: 60_000 - 10_000,
}
);
}
}