OXYGENHUB
# 2e640ffa6c93 oxygenhub / crates / hub / src / repos / cache.rs
6.1 KiB · Rust · last changed Bookmark summaries: stop walking every state through the cache 1 month ago source blame history raw
1
//! Loaded states cached by their heads (hub spec §16.3). A state is a pure
2
//! function of its heads, so entries never go stale; the cache is only
3
//! bounded. Shared across threads because `oxy_core::state::State` is
4
//! `Send + Sync`.
5
6
use oxy_core::hash::ChangeHash;
7
use oxy_core::repo::Repo;
8
use oxy_core::state::{Snapshot, State};
9
use std::collections::HashMap;
10
use std::sync::{Arc, Mutex};
11
12
/// A state and its materialised snapshot.
13
pub struct Loaded {
14
    pub heads: Vec<ChangeHash>,
15
    pub state: State,
16
    pub snapshot: Snapshot,
17
}
18
19
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
20
struct Key {
21
    repo: [u8; 16],
22
    heads: Vec<ChangeHash>,
23
}
24
25
struct Entry {
26
    loaded: Arc<Loaded>,
27
    last_used: u64,
28
}
29
30
/// What the bookmark lists and a change's "in bookmarks" need of a state:
31
/// small, so many are kept, and computed without filling the state cache.
32
/// A repository with more bookmarks than the state cache holds otherwise
33
/// cycled through it on every such page, so nothing stayed cached
34
/// (metalgear's 128 bookmarks: 7.7 s per change page, and every page of
35
/// every repository slower after it).
36
pub struct Summary {
37
    pub changes: std::collections::HashSet<ChangeHash>,
38
    pub latest_ms: u64,
39
    pub conflicted: bool,
40
}
41
42
/// Summaries kept; each is the state's change set, a few hundred kilobytes
43
/// for a long history.
44
const SUMMARIES: usize = 2048;
45
46
/// Summaries by heads, each with when it was last used, and the clock.
47
type Summaries = (HashMap<Key, (Arc<Summary>, u64)>, u64);
48
49
/// A bounded least-recently-used cache of loaded states.
50
pub struct StateCache {
51
    capacity: usize,
52
    inner: Mutex<(HashMap<Key, Entry>, u64)>,
53
    summaries: Mutex<Summaries>,
54
}
55
56
impl StateCache {
57
    pub fn new(capacity: usize) -> StateCache {
58
        StateCache { capacity: capacity.max(1), inner: Mutex::new((HashMap::new(), 0)), summaries: Mutex::new((HashMap::new(), 0)) }
59
    }
60
61
    /// The summary of the state with these heads: from the state cache when
62
    /// it holds the state, else loaded and dropped, the state cache left as
63
    /// it was.
64
    pub fn summary(&self, repo: &Repo, heads: &[ChangeHash]) -> Result<Arc<Summary>, oxy_core::repo::RepoError> {
65
        let mut sorted = heads.to_vec();
66
        sorted.sort();
67
        sorted.dedup();
68
        let key = Key { repo: repo.id()?, heads: sorted.clone() };
69
        {
70
            let mut guard = self.summaries.lock().unwrap();
71
            let (map, tick) = &mut *guard;
72
            *tick += 1;
73
            if let Some(e) = map.get_mut(&key) {
74
                e.1 = *tick;
75
                return Ok(e.0.clone());
76
            }
77
        }
78
        let held = self.inner.lock().unwrap().0.get(&key).map(|e| e.loaded.clone());
79
        let summary = Arc::new(match held {
80
            Some(l) => summarise(&l.state, &l.snapshot),
81
            None => {
82
                let state = repo.load_state(&sorted)?;
83
                let snapshot = state.materialize();
84
                summarise(&state, &snapshot)
85
            }
86
        });
87
        let mut guard = self.summaries.lock().unwrap();
88
        let (map, tick) = &mut *guard;
89
        if map.len() >= SUMMARIES {
90
            if let Some(oldest) = map.iter().min_by_key(|(_, e)| e.1).map(|(k, _)| k.clone()) {
91
                map.remove(&oldest);
92
            }
93
        }
94
        map.insert(key, (summary.clone(), *tick));
95
        Ok(summary)
96
    }
97
98
    /// The state with these heads, loaded once per hub process.
99
    pub fn load(&self, repo: &Repo, heads: &[ChangeHash]) -> Result<Arc<Loaded>, oxy_core::repo::RepoError> {
100
        let mut sorted = heads.to_vec();
101
        sorted.sort();
102
        sorted.dedup();
103
        let key = Key { repo: repo.id()?, heads: sorted.clone() };
104
        {
105
            let mut guard = self.inner.lock().unwrap();
106
            let (map, tick) = &mut *guard;
107
            *tick += 1;
108
            if let Some(e) = map.get_mut(&key) {
109
                e.last_used = *tick;
110
                return Ok(e.loaded.clone());
111
            }
112
        }
113
        let state = repo.load_state(&sorted)?;
114
        let snapshot = state.materialize();
115
        let loaded = Arc::new(Loaded { heads: sorted, state, snapshot });
116
        let mut guard = self.inner.lock().unwrap();
117
        let (map, tick) = &mut *guard;
118
        if map.len() >= self.capacity {
119
            if let Some(oldest) = map.iter().min_by_key(|(_, e)| e.last_used).map(|(k, _)| k.clone()) {
120
                map.remove(&oldest);
121
            }
122
        }
123
        map.insert(key, Entry { loaded: loaded.clone(), last_used: *tick });
124
        Ok(loaded)
125
    }
126
127
    pub fn len(&self) -> usize {
128
        self.inner.lock().unwrap().0.len()
129
    }
130
131
    pub fn is_empty(&self) -> bool {
132
        self.len() == 0
133
    }
134
}
135
136
fn summarise(state: &State, snapshot: &Snapshot) -> Summary {
137
    Summary {
138
        changes: state.changes().map(|(h, _)| *h).collect(),
139
        latest_ms: state.changes().map(|(_, m)| m.hlc.wall_ms).max().unwrap_or(0),
140
        conflicted: snapshot.is_conflicted(),
141
    }
142
}
143
144
#[cfg(test)]
145
mod tests {
146
    use super::*;
147
148
    #[test]
149
    fn caches_by_heads_and_evicts_the_oldest() {
150
        let dir = tempfile::tempdir().unwrap();
151
        let repo = Repo::init_bare(dir.path()).unwrap();
152
        let cache = StateCache::new(2);
153
        let a = cache.load(&repo, &[]).unwrap();
154
        let b = cache.load(&repo, &[]).unwrap();
155
        assert!(Arc::ptr_eq(&a, &b), "same heads, same entry");
156
        assert_eq!(cache.len(), 1);
157
        let h1 = oxy_core::hash::ObjectRef([1; 32]);
158
        let h2 = oxy_core::hash::ObjectRef([2; 32]);
159
        // Unknown heads fail to load and are not cached.
160
        assert!(cache.load(&repo, &[h1]).is_err());
161
        assert_eq!(cache.len(), 1);
162
        let _ = (h2, dir);
163
    }
164
165
    #[test]
166
    fn summaries_leave_the_state_cache_alone() {
167
        let dir = tempfile::tempdir().unwrap();
168
        let repo = Repo::init_bare(dir.path()).unwrap();
169
        let cache = StateCache::new(2);
170
        let a = cache.summary(&repo, &[]).unwrap();
171
        assert_eq!(cache.len(), 0, "a summary does not fill the state cache");
172
        assert!(Arc::ptr_eq(&a, &cache.summary(&repo, &[]).unwrap()), "kept");
173
        assert!(a.changes.is_empty() && !a.conflicted);
174
        assert!(cache.summary(&repo, &[oxy_core::hash::ObjectRef([3; 32])]).is_err());
175
    }
176
}