use crate::op_log::OpLog;
use crate::operation::OpId;
use std::collections::{BTreeSet, HashMap};
use std::io;
pub struct HistoryIndex {
head: OpId,
ids: Vec<OpId>,
pos: HashMap<OpId, u32>,
parent_start: Vec<u32>,
parents: Vec<u32>,
}
impl HistoryIndex {
pub fn build(log: &OpLog, head: &OpId) -> io::Result<Self> {
let records = log.walk_back(head, None)?;
let ids: Vec<OpId> = records.iter().map(|r| r.op_id.clone()).collect();
let pos: HashMap<OpId, u32> =
ids.iter().enumerate().map(|(i, id)| (id.clone(), i as u32)).collect();
let mut parent_start = Vec::with_capacity(ids.len() + 1);
let mut parents = Vec::new();
for rec in &records {
parent_start.push(parents.len() as u32);
parents.extend(rec.op.parents.iter().filter_map(|p| pos.get(p).copied()));
}
parent_start.push(parents.len() as u32);
Ok(Self { head: head.clone(), ids, pos, parent_start, parents })
}
pub fn head(&self) -> &OpId {
&self.head
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub fn id(&self, i: u32) -> &OpId {
&self.ids[i as usize]
}
pub fn since(&self, log: &OpLog, base: Option<&OpId>) -> io::Result<Vec<u32>> {
let mut excluded = vec![false; self.ids.len()];
match base {
None => {}
Some(b) => match self.pos.get(b) {
Some(&start) => {
let mut stack = vec![start];
excluded[start as usize] = true;
while let Some(i) = stack.pop() {
let i = i as usize;
let (lo, hi) = (self.parent_start[i] as usize, self.parent_start[i + 1] as usize);
for &p in &self.parents[lo..hi] {
if !excluded[p as usize] {
excluded[p as usize] = true;
stack.push(p);
}
}
}
}
None => {
let anc: BTreeSet<OpId> =
log.walk_back(b, None)?.into_iter().map(|r| r.op_id).collect();
for (i, id) in self.ids.iter().enumerate() {
excluded[i] = anc.contains(id);
}
}
},
}
Ok((0..self.ids.len() as u32).rev().filter(|&i| !excluded[i as usize]).collect())
}
pub fn topological(&self, delta: &[u32]) -> Vec<u32> {
use std::collections::BinaryHeap;
let n = self.ids.len();
let mut in_delta = vec![false; n];
for &i in delta {
in_delta[i as usize] = true;
}
let mut indegree = vec![0u32; n];
let mut children: HashMap<u32, Vec<u32>> = HashMap::new();
for &i in delta {
let (lo, hi) = (self.parent_start[i as usize] as usize, self.parent_start[i as usize + 1] as usize);
for &p in &self.parents[lo..hi] {
if in_delta[p as usize] {
indegree[i as usize] += 1;
children.entry(p).or_default().push(i);
}
}
}
let mut ready: BinaryHeap<u32> =
delta.iter().copied().filter(|&i| indegree[i as usize] == 0).collect();
let mut out = Vec::with_capacity(delta.len());
while let Some(i) = ready.pop() {
out.push(i);
for &c in children.get(&i).map(Vec::as_slice).unwrap_or(&[]) {
indegree[c as usize] -= 1;
if indegree[c as usize] == 0 {
ready.push(c);
}
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::operation::{Operation, OperationKind, OperationRecord, StageTransition};
use std::collections::BTreeMap;
fn rec(parents: &[&OpId], tag: usize) -> OperationRecord {
let kind = if parents.len() > 1 {
OperationKind::Merge { resolved: tag }
} else {
OperationKind::AddFunction {
sig_id: format!("s{tag}"),
stage_id: format!("t{tag}"),
effects: BTreeSet::new(),
budget_cost: None,
in_file: None,
}
};
let produces = if parents.len() > 1 {
StageTransition::Merge { entries: BTreeMap::new() }
} else {
StageTransition::Create { sig_id: format!("s{tag}"), stage_id: format!("t{tag}") }
};
OperationRecord::new(Operation::new(kind, parents.iter().map(|p| (*p).clone())), produces)
}
fn random_dag(log: &OpLog, n: usize, seed: u64) -> Vec<OpId> {
let mut state = seed;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let mut ids: Vec<OpId> = Vec::new();
for i in 0..n {
let r = next();
let parents: Vec<&OpId> = if ids.is_empty() || r % 17 == 0 {
vec![]
} else {
let k = match r % 7 { 0 => 2, 1 => 3, _ => 1 };
(0..k).map(|_| &ids[(next() as usize) % ids.len()]).collect()
};
let rec = rec(&parents, i);
log.put(&rec).unwrap();
ids.push(rec.op_id);
}
ids
}
fn oracle(log: &OpLog, head: &OpId, base: Option<&OpId>) -> Vec<OpId> {
let mut v: Vec<OpId> = log.ops_since(head, base).unwrap().into_iter().map(|r| r.op_id).collect();
v.reverse();
v
}
#[test]
fn since_matches_ops_since_on_random_dags() {
for seed in [1u64, 7, 42, 1234, 99991] {
let tmp = tempfile::tempdir().unwrap();
let log = OpLog::open(tmp.path()).unwrap();
let ids = random_dag(&log, 120, seed);
let ghost: OpId = "0".repeat(64);
for head in ids.iter().step_by(13).chain(ids.last()) {
let idx = HistoryIndex::build(&log, head).unwrap();
assert_eq!(idx.len(), log.walk_back(head, None).unwrap().len());
let bases = std::iter::once(None)
.chain(ids.iter().step_by(7).map(Some))
.chain(std::iter::once(Some(&ghost)));
for base in bases {
let got: Vec<OpId> =
idx.since(&log, base).unwrap().into_iter().map(|i| idx.id(i).clone()).collect();
assert_eq!(got, oracle(&log, head, base), "seed={seed} head={head} base={base:?}");
}
}
}
}
#[test]
fn topological_is_a_parent_first_permutation_that_keeps_topological_input() {
for seed in [3u64, 11, 77, 2024] {
let tmp = tempfile::tempdir().unwrap();
let log = OpLog::open(tmp.path()).unwrap();
let ids = random_dag(&log, 150, seed);
let ghost: OpId = "0".repeat(64);
for head in ids.iter().step_by(17).chain(ids.last()) {
let idx = HistoryIndex::build(&log, head).unwrap();
let bases = std::iter::once(None)
.chain(ids.iter().step_by(11).map(Some))
.chain(std::iter::once(Some(&ghost)));
for base in bases {
let delta = idx.since(&log, base).unwrap();
let topo = idx.topological(&delta);
let mut a = delta.clone();
let mut b = topo.clone();
a.sort_unstable();
b.sort_unstable();
assert_eq!(a, b, "a permutation of the delta");
let rank: HashMap<u32, usize> = topo.iter().enumerate().map(|(r, &i)| (i, r)).collect();
for (r, &i) in topo.iter().enumerate() {
let rec = log.get(idx.id(i)).unwrap().unwrap();
for p in &rec.op.parents {
if let Some(&pi) = idx.pos.get(p) {
if let Some(&pr) = rank.get(&pi) {
assert!(pr < r, "parent after child (seed={seed})");
}
}
}
}
if let (Some(&last), false) = (topo.last(), delta.is_empty()) {
if delta.contains(&0) {
assert_eq!(last, 0, "the head is last");
}
}
assert_eq!(idx.topological(&topo), topo);
}
}
}
}
#[test]
fn topological_leaves_a_linear_history_in_since_order() {
let tmp = tempfile::tempdir().unwrap();
let log = OpLog::open(tmp.path()).unwrap();
let mut prev: Vec<OpId> = vec![];
for i in 0..20 {
let r = rec(&prev.iter().collect::<Vec<_>>(), i);
log.put(&r).unwrap();
prev = vec![r.op_id];
}
let idx = HistoryIndex::build(&log, &prev[0]).unwrap();
let delta = idx.since(&log, None).unwrap();
assert_eq!(idx.topological(&delta), delta);
}
#[test]
fn unknown_head_is_an_empty_index() {
let tmp = tempfile::tempdir().unwrap();
let log = OpLog::open(tmp.path()).unwrap();
let idx = HistoryIndex::build(&log, &"f".repeat(64)).unwrap();
assert!(idx.is_empty());
assert!(idx.since(&log, None).unwrap().is_empty());
}
}