OXYGENHUB

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

115 of 118 lines run, 34 of 36 functions · run #379 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).
2749×
    pub(crate) fn of(&mut self, repo: &Repo, h: &ChangeHash) -> Option<&[NodeId]> {
2849×
        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);
3249×
        }
3349×
        self.nodes
3449×
            .get(h)
3549×
            .or_else(|| self.transient.get(h))
3649×
            .map(|n| &n[..])
3749×
    }
38
3929×
    fn keep(&mut self, h: ChangeHash, c: &Change) {
4029×
        if c.kind == ChangeKind::WorkingCopy {
418×
            self.transient.insert(h, nodes_of(c));
4221×
        } else {
4321×
            self.nodes.insert(h, nodes_of(c));
4421×
            self.added += 1;
4521×
        }
4629×
    }
47
48
    /// Read every one of `hashes` the cache lacks.
4912×
    pub(crate) fn fill(&mut self, repo: &Repo, hashes: &[ChangeHash]) {
50
        use rayon::prelude::*;
5112×
        let missing: Vec<ChangeHash> = hashes
5212×
            .iter()
5347×
            .filter(|h| !self.nodes.contains_key(*h) && !self.transient.contains_key(*h))
5412×
            .copied()
5512×
            .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.
5812×
        for batch in missing.chunks(8192) {
5910×
            let read: Vec<(ChangeHash, Change)> = repo
6010×
                .store
6110×
                .get_many(batch)
6210×
                .into_par_iter()
6310×
                .zip(batch.par_iter())
6429×
                .filter_map(|(got, h)| match got {
6529×
                    Ok((ObjectType::Change, bytes)) => {
6629×
                        Some((*h, Change::decode_skeleton(&bytes, *h).ok()?.0))
67
                    }
680×
                    _ => None,
6929×
                })
7010×
                .collect();
7129×
            for (h, c) in &read {
7229×
                self.keep(*h, c);
7329×
            }
74
        }
7512×
    }
76
77
    /// Whether `h` touches any of `nodes` (false when it is not stored).
7849×
    pub(crate) fn touches(&mut self, repo: &Repo, h: &ChangeHash, nodes: &HashSet<NodeId>) -> bool {
7949×
        self.of(repo, h)
8061×
            .is_some_and(|n| n.iter().any(|n| nodes.contains(n)))
8149×
    }
82
83
    /// Write the cache back if it grew; quietly nothing when it cannot be.
8413×
    pub(crate) fn save(self) {
8513×
        if self.added == 0 {
866×
            return;
877×
        }
887×
        let _t = crate::timing::phase("write: touched nodes");
897×
        let mut w = crate::flat::Writer::default();
907×
        w.count(self.nodes.len());
9121×
        for (h, nodes) in &self.nodes {
9221×
            w.object(h);
9321×
            w.count(nodes.len());
9434×
            for n in nodes.iter() {
9534×
                w.fixed(&n.0);
9634×
            }
97
        }
98
        // Checked whole on reading: a damaged entry would hide a change.
997×
        let sum = *blake3::hash(&w.0).as_bytes();
1007×
        w.fixed(&sum);
1017×
        let _ = fs::create_dir_all(self.path.parent().unwrap_or(&self.path));
1027×
        let _ = write_cache(&self.path, &w.0);
10313×
    }
104
}
105
106
/// The nodes a change's tree operations and edits name, each once.
10729×
fn nodes_of(c: &Change) -> Box<[NodeId]> {
10829×
    let mut nodes: Vec<NodeId> = c
10929×
        .tree_ops
11029×
        .iter()
11129×
        .map(|o| o.node())
11236×
        .chain(c.edits.iter().map(|e| e.node()))
11329×
        .collect();
11429×
    nodes.sort();
11529×
    nodes.dedup();
11629×
    nodes.into_boxed_slice()
11729×
}
118
119
impl Repo {
120
    /// The touched-nodes cache: empty when there is none yet or it does
121
    /// not read back whole.
12213×
    pub(crate) fn touched(&self) -> Touched {
12313×
        let _t = crate::timing::phase("load: touched nodes");
12413×
        let path = self.oxy.join("index").join("touched");
12513×
        let nodes = fs::read(&path)
12613×
            .ok()
12713×
            .and_then(|b| read_touched(&b))
12813×
            .unwrap_or_default();
12913×
        Touched {
13013×
            nodes,
13113×
            transient: FastMap::default(),
13213×
            added: 0,
13313×
            path,
13413×
        }
13513×
    }
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×
}