OXYGENHUB

Coverage / crates/hub/src/render/cache.rs 89.2%

83 of 93 lines run, 38 of 79 functions · run #542 on main · run missed

1
//! Caches of rendered output keyed by what the output depends on (hub spec
2
//! §16.3): the BLAKE3 of the content and of anything else that changes the
3
//! result, such as a Markdown file's link bases or a file's name, which
4
//! picks its syntax. Bounded by the bytes they hold; the least recently
5
//! used entries go first.
6
7
use std::collections::HashMap;
8
use std::sync::{Arc, Mutex};
9
10
pub struct Lru<V> {
11
    inner: Mutex<Inner<V>>,
12
}
13
14
struct Inner<V> {
15
    budget: usize,
16
    used: usize,
17
    tick: u64,
18
    hits: u64,
19
    misses: u64,
20
    map: HashMap<[u8; 32], (u64, Arc<V>, usize)>,
21
}
22
23
/// Something whose size counts against a cache's budget.
24
pub trait Weigh {
25
    fn weight(&self) -> usize;
26
}
27
28
impl Weigh for String {
2968×
    fn weight(&self) -> usize {
3068×
        self.len() + 32
3168×
    }
32
}
33
34
impl Weigh for Vec<String> {
3516×
    fn weight(&self) -> usize {
3649×
        self.iter().map(|s| s.len() + 24).sum::<usize>() + 32
3716×
    }
38
}
39
40
/// The key for a set of parts: BLAKE3 of each part's length and bytes.
41347×
pub fn key(parts: &[&[u8]]) -> [u8; 32] {
42347×
    let mut h = blake3::Hasher::new();
431276×
    for p in parts {
441276×
        h.update(&(p.len() as u64).to_le_bytes());
451276×
        h.update(p);
461276×
    }
47347×
    *h.finalize().as_bytes()
48347×
}
49
50
impl<V: Weigh> Lru<V> {
5153×
    pub fn new(budget: usize) -> Lru<V> {
5253×
        Lru { inner: Mutex::new(Inner { budget, used: 0, tick: 0, hits: 0, misses: 0, map: HashMap::new() }) }
5353×
    }
54
55
    /// The cached value for `key`, or `make()` stored and returned. `make`
56
    /// runs without the lock held, so a slow render blocks nobody.
57345×
    pub fn get_or(&self, key: [u8; 32], make: impl FnOnce() -> V) -> Arc<V> {
58
        {
59345×
            let mut g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
60345×
            g.tick += 1;
61345×
            let tick = g.tick;
62345×
            if let Some(entry) = g.map.get_mut(&key) {
63243×
                entry.0 = tick;
64243×
                let v = entry.1.clone();
65243×
                g.hits += 1;
66243×
                return v;
67102×
            }
68102×
            g.misses += 1;
69
        }
70102×
        let value = Arc::new(make());
71102×
        let weight = value.weight();
72102×
        let mut g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
73102×
        if weight > g.budget / 4 {
744×
            return value; // too big to be worth keeping
7598×
        }
7698×
        g.tick += 1;
7798×
        let tick = g.tick;
7898×
        if let Some(old) = g.map.insert(key, (tick, value.clone(), weight)) {
790×
            g.used -= old.2;
8098×
        }
8198×
        g.used += weight;
8298×
        while g.used > g.budget {
830×
            let Some((&oldest, _)) = g.map.iter().min_by_key(|(_, e)| e.0) else { break };
840×
            if let Some(e) = g.map.remove(&oldest) {
850×
                g.used -= e.2;
860×
            }
87
        }
8898×
        value
89345×
    }
90
91
    /// (hits, misses, bytes held).
924×
    pub fn stats(&self) -> (u64, u64, usize) {
934×
        let g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
944×
        (g.hits, g.misses, g.used)
954×
    }
96
}
97
98
#[cfg(test)]
99
mod tests {
100
    use super::*;
101
102
    #[test]
1031×
    fn hits_misses_and_eviction() {
1041×
        let c: Lru<String> = Lru::new(400);
1051×
        let mut runs = 0;
1061×
        for _ in 0..3 {
1073×
            c.get_or(key(&[b"a"]), || {
1081×
                runs += 1;
1091×
                "x".repeat(50)
1101×
            });
111
        }
1121×
        assert_eq!(runs, 1, "rendered once");
1131×
        assert_eq!(c.stats().0, 2);
114
        // Filling past the budget drops the least recently used.
1151×
        c.get_or(key(&[b"b"]), || "y".repeat(90));
1161×
        c.get_or(key(&[b"a"]), || unreachable!());
1171×
        c.get_or(key(&[b"c"]), || "z".repeat(90));
1181×
        c.get_or(key(&[b"d"]), || "w".repeat(90));
1191×
        assert!(c.stats().2 <= 400);
1201×
        let mut again = false;
1211×
        c.get_or(key(&[b"b"]), || {
1221×
            again = true;
1231×
            String::new()
1241×
        });
1251×
        assert!(again, "b was the least recently used and went first");
126
        // Something larger than a quarter of the budget is not kept.
1271×
        c.get_or(key(&[b"huge"]), || "h".repeat(200));
1281×
        let mut rerun = false;
1291×
        c.get_or(key(&[b"huge"]), || {
1301×
            rerun = true;
1311×
            String::new()
1321×
        });
1331×
        assert!(rerun);
134
        // Parts are length-framed: ("ab","c") and ("a","bc") differ.
1351×
        assert_ne!(key(&[b"ab", b"c"]), key(&[b"a", b"bc"]));
1361×
    }
137
}