OXYGENHUB

Coverage / crates/oxy-core/src/repo/touched.rs 97.5%

115 of 118 lines run, 34 of 36 functions · run #305 on main · run missed

1
//! The nodes each change touches, kept beside the store (oxygen#91).
2
//!
3
//! Finding the changes that touched a file (`oxy log --path`, the
4
//! `touches()` revset) otherwise reads and decodes every change of the
5
//! history, its lines included. A change is immutable, so its entry never
6
//! goes stale: one missing is read from the store once and added. Like
7
//! Git's changed-path filters it is derived data, and deleting
8
//! `.oxy/index/touched` loses nothing but time.
9
10
use super::*;
11
use crate::fasthash::FastMap;
12
use crate::ids::NodeId;
13
14
/// The cache, loaded; [`Touched::save`] writes it back when it grew.
15
pub(crate) struct Touched {
16
    nodes: FastMap<ChangeHash, Box<[NodeId]>>,
17
    /// Working-copy changes, which a snapshot replaces: kept for this
18
    /// command only, so the cache does not fill with dead entries.
19
    transient: FastMap<ChangeHash, Box<[NodeId]>>,
20
    added: usize,
21
    path: PathBuf,
22
}
23
24
impl Touched {
25
    /// The nodes `h` touches, from the cache or else from the store; `None`
26
    /// when the change is not stored (beyond a shallow boundary).
2745×
    pub(crate) fn of(&mut self, repo: &Repo, h: &ChangeHash) -> Option<&[NodeId]> {
2845×
        if !self.nodes.contains_key(h) && !self.transient.contains_key(h) {
29
            // The skeleton names every node; no text is read.
300×
            let (c, _) = repo.store.get_change_lazy(h).ok()?;
310×
            self.keep(*h, &c);
3245×
        }
3345×
        self.nodes
3445×
            .get(h)
3545×
            .or_else(|| self.transient.get(h))
3645×
            .map(|n| &n[..])
3745×
    }
38
3925×
    fn keep(&mut self, h: ChangeHash, c: &Change) {
4025×
        if c.kind == ChangeKind::WorkingCopy {
417×
            self.transient.insert(h, nodes_of(c));
4218×
        } else {
4318×
            self.nodes.insert(h, nodes_of(c));
4418×
            self.added += 1;
4518×
        }
4625×
    }
47
48
    /// Read every one of `hashes` the cache lacks.
4911×
    pub(crate) fn fill(&mut self, repo: &Repo, hashes: &[ChangeHash]) {
50
        use rayon::prelude::*;
5111×
        let missing: Vec<ChangeHash> = hashes
5211×
            .iter()
5343×
            .filter(|h| !self.nodes.contains_key(*h) && !self.transient.contains_key(*h))
5411×
            .copied()
5511×
            .collect();
56
        // Read a batch at a time, then decode skeletons (no text) on every
57
        // core. A change the store lacks is left out, as `of` leaves it.
5811×
        for batch in missing.chunks(8192) {
599×
            let read: Vec<(ChangeHash, Change)> = repo
609×
                .store
619×
                .get_many(batch)
629×
                .into_par_iter()
639×
                .zip(batch.par_iter())
6425×
                .filter_map(|(got, h)| match got {
6525×
                    Ok((ObjectType::Change, bytes)) => {
6625×
                        Some((*h, Change::decode_skeleton(&bytes, *h).ok()?.0))
67
                    }
680×
                    _ => None,
6925×
                })
709×
                .collect();
7125×
            for (h, c) in &read {
7225×
                self.keep(*h, c);
7325×
            }
74
        }
7511×
    }
76
77
    /// Whether `h` touches any of `nodes` (false when it is not stored).
7845×
    pub(crate) fn touches(&mut self, repo: &Repo, h: &ChangeHash, nodes: &HashSet<NodeId>) -> bool {
7945×
        self.of(repo, h)
8072×
            .is_some_and(|n| n.iter().any(|n| nodes.contains(n)))
8145×
    }
82
83
    /// Write the cache back if it grew; quietly nothing when it cannot be.
8412×
    pub(crate) fn save(self) {
8512×
        if self.added == 0 {
866×
            return;
876×
        }
886×
        let _t = crate::timing::phase("write: touched nodes");
896×
        let mut w = crate::flat::Writer::default();
906×
        w.count(self.nodes.len());
9118×
        for (h, nodes) in &self.nodes {
9218×
            w.object(h);
9318×
            w.count(nodes.len());
9431×
            for n in nodes.iter() {
9531×
                w.fixed(&n.0);
9631×
            }
97
        }
98
        // Checked whole on reading: a damaged entry would hide a change.
996×
        let sum = *blake3::hash(&w.0).as_bytes();
1006×
        w.fixed(&sum);
1016×
        let _ = fs::create_dir_all(self.path.parent().unwrap_or(&self.path));
1026×
        let _ = write_cache(&self.path, &w.0);
10312×
    }
104
}
105
106
/// The nodes a change's tree operations and edits name, each once.
10725×
fn nodes_of(c: &Change) -> Box<[NodeId]> {
10825×
    let mut nodes: Vec<NodeId> = c
10925×
        .tree_ops
11025×
        .iter()
11125×
        .map(|o| o.node())
11233×
        .chain(c.edits.iter().map(|e| e.node()))
11325×
        .collect();
11425×
    nodes.sort();
11525×
    nodes.dedup();
11625×
    nodes.into_boxed_slice()
11725×
}
118
119
impl Repo {
120
    /// The touched-nodes cache: empty when there is none yet or it does
121
    /// not read back whole.
12212×
    pub(crate) fn touched(&self) -> Touched {
12312×
        let _t = crate::timing::phase("load: touched nodes");
12412×
        let path = self.oxy.join("index").join("touched");
12512×
        let nodes = fs::read(&path)
12612×
            .ok()
12712×
            .and_then(|b| read_touched(&b))
12812×
            .unwrap_or_default();
12912×
        Touched {
13012×
            nodes,
13112×
            transient: FastMap::default(),
13212×
            added: 0,
13312×
            path,
13412×
        }
13512×
    }
136
}
137
1388×
fn read_touched(bytes: &[u8]) -> Option<FastMap<ChangeHash, Box<[NodeId]>>> {
1398×
    let (body, sum) = bytes.split_at_checked(bytes.len().checked_sub(32)?)?;
1408×
    if blake3::hash(body).as_bytes() != sum {
1411×
        return None;
1427×
    }
1437×
    let mut r = crate::flat::Reader::new(body);
1447×
    let n = r.count().ok()?;
1457×
    let mut out = FastMap::default();
1467×
    out.reserve(n);
1477×
    for _ in 0..n {
14825×
        let h = r.object().ok()?;
14925×
        let k = r.count().ok()?;
15025×
        let mut nodes = Vec::with_capacity(k);
15125×
        for _ in 0..k {
15246×
            nodes.push(NodeId(r.fixed::<16>().ok()?));
153
        }
15425×
        out.insert(h, nodes.into_boxed_slice());
155
    }
1567×
    r.at_end().then_some(out)
1578×
}
158
159
/// The changes a cache file holds, for tests.
160
#[cfg(test)]
1611×
pub(crate) fn read_touched_for_test(bytes: &[u8]) -> Option<HashSet<ChangeHash>> {
1621×
    read_touched(bytes).map(|m| m.into_keys().collect())
1631×
}