OXYGENHUB
◆ v0.5.0 oxygen / crates / oxy-core / src / repo / touched.rs
5.6 KiB · Rust · last changed log --path and touches() without decoding all of history 26 days ago source blame history raw
170bb436 Andy McCall ↶ 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).
27
    pub(crate) fn of(&mut self, repo: &Repo, h: &ChangeHash) -> Option<&[NodeId]> {
28
        if !self.nodes.contains_key(h) && !self.transient.contains_key(h) {
29
            // The skeleton names every node; no text is read.
30
            let (c, _) = repo.store.get_change_lazy(h).ok()?;
31
            self.keep(*h, &c);
32
        }
33
        self.nodes
34
            .get(h)
35
            .or_else(|| self.transient.get(h))
36
            .map(|n| &n[..])
37
    }
38
39
    fn keep(&mut self, h: ChangeHash, c: &Change) {
40
        if c.kind == ChangeKind::WorkingCopy {
41
            self.transient.insert(h, nodes_of(c));
42
        } else {
43
            self.nodes.insert(h, nodes_of(c));
44
            self.added += 1;
45
        }
46
    }
47
48
    /// Read every one of `hashes` the cache lacks.
49
    pub(crate) fn fill(&mut self, repo: &Repo, hashes: &[ChangeHash]) {
50
        use rayon::prelude::*;
51
        let missing: Vec<ChangeHash> = hashes
52
            .iter()
53
            .filter(|h| !self.nodes.contains_key(*h) && !self.transient.contains_key(*h))
54
            .copied()
55
            .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.
58
        for batch in missing.chunks(8192) {
59
            let read: Vec<(ChangeHash, Change)> = repo
60
                .store
61
                .get_many(batch)
62
                .into_par_iter()
63
                .zip(batch.par_iter())
64
                .filter_map(|(got, h)| match got {
65
                    Ok((ObjectType::Change, bytes)) => {
66
                        Some((*h, Change::decode_skeleton(&bytes, *h).ok()?.0))
67
                    }
68
                    _ => None,
69
                })
70
                .collect();
71
            for (h, c) in &read {
72
                self.keep(*h, c);
73
            }
74
        }
75
    }
76
77
    /// Whether `h` touches any of `nodes` (false when it is not stored).
78
    pub(crate) fn touches(&mut self, repo: &Repo, h: &ChangeHash, nodes: &HashSet<NodeId>) -> bool {
79
        self.of(repo, h)
80
            .is_some_and(|n| n.iter().any(|n| nodes.contains(n)))
81
    }
82
83
    /// Write the cache back if it grew; quietly nothing when it cannot be.
84
    pub(crate) fn save(self) {
85
        if self.added == 0 {
86
            return;
87
        }
88
        let _t = crate::timing::phase("write: touched nodes");
89
        let mut w = crate::flat::Writer::default();
90
        w.count(self.nodes.len());
91
        for (h, nodes) in &self.nodes {
92
            w.object(h);
93
            w.count(nodes.len());
94
            for n in nodes.iter() {
95
                w.fixed(&n.0);
96
            }
97
        }
98
        // Checked whole on reading: a damaged entry would hide a change.
99
        let sum = *blake3::hash(&w.0).as_bytes();
100
        w.fixed(&sum);
101
        let _ = fs::create_dir_all(self.path.parent().unwrap_or(&self.path));
102
        let _ = write_cache(&self.path, &w.0);
103
    }
104
}
105
106
/// The nodes a change's tree operations and edits name, each once.
107
fn nodes_of(c: &Change) -> Box<[NodeId]> {
108
    let mut nodes: Vec<NodeId> = c
109
        .tree_ops
110
        .iter()
111
        .map(|o| o.node())
112
        .chain(c.edits.iter().map(|e| e.node()))
113
        .collect();
114
    nodes.sort();
115
    nodes.dedup();
116
    nodes.into_boxed_slice()
117
}
118
119
impl Repo {
120
    /// The touched-nodes cache: empty when there is none yet or it does
121
    /// not read back whole.
122
    pub(crate) fn touched(&self) -> Touched {
123
        let _t = crate::timing::phase("load: touched nodes");
124
        let path = self.oxy.join("index").join("touched");
125
        let nodes = fs::read(&path)
126
            .ok()
127
            .and_then(|b| read_touched(&b))
128
            .unwrap_or_default();
129
        Touched {
130
            nodes,
131
            transient: FastMap::default(),
132
            added: 0,
133
            path,
134
        }
135
    }
136
}
137
138
fn read_touched(bytes: &[u8]) -> Option<FastMap<ChangeHash, Box<[NodeId]>>> {
139
    let (body, sum) = bytes.split_at_checked(bytes.len().checked_sub(32)?)?;
140
    if blake3::hash(body).as_bytes() != sum {
141
        return None;
142
    }
143
    let mut r = crate::flat::Reader::new(body);
144
    let n = r.count().ok()?;
145
    let mut out = FastMap::default();
146
    out.reserve(n);
147
    for _ in 0..n {
148
        let h = r.object().ok()?;
149
        let k = r.count().ok()?;
150
        let mut nodes = Vec::with_capacity(k);
151
        for _ in 0..k {
152
            nodes.push(NodeId(r.fixed::<16>().ok()?));
153
        }
154
        out.insert(h, nodes.into_boxed_slice());
155
    }
156
    r.at_end().then_some(out)
157
}
158
159
/// The changes a cache file holds, for tests.
160
#[cfg(test)]
161
pub(crate) fn read_touched_for_test(bytes: &[u8]) -> Option<HashSet<ChangeHash>> {
162
    read_touched(bytes).map(|m| m.into_keys().collect())
163
}