Skip to main content

sva_engine/cache/
slots.rs

1// Concern: keeps one last value per volatile node, in memory, under a byte cap | Non-concern: which nodes are volatile (render/volatile.rs), every other store | IO: (slot, key) -> a payload + traces
2
3use std::collections::HashMap;
4use std::sync::{Mutex, MutexGuard};
5
6use sva_formula::Hash;
7use sva_samples::{FilterTrace, Label};
8
9use super::{Entry, Expected, Payload, Tier};
10
11pub const DEFAULT_SLOT_BYTES: u64 = 64 << 20;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Put {
15    Slotted,
16    Replaced,
17    /// Larger than the whole cap: the slot's old value stands.
18    Refused,
19}
20
21struct Slotted {
22    key: Hash,
23    payload: Payload,
24    traces: Vec<FilterTrace>,
25    label: Option<Label>,
26    read: u64,
27}
28
29impl Slotted {
30    fn bytes(&self) -> u64 {
31        self.payload.bytes() as u64
32    }
33}
34
35struct State {
36    held: HashMap<Hash, Slotted>,
37    bytes: u64,
38    max_bytes: u64,
39    clock: u64,
40}
41
42impl State {
43    fn evict(&mut self, keep: Option<Hash>) {
44        while self.bytes > self.max_bytes {
45            let oldest = self
46                .held
47                .iter()
48                .filter(|(slot, _)| Some(**slot) != keep)
49                .min_by_key(|(_, held)| held.read)
50                .map(|(slot, _)| *slot);
51            let Some(slot) = oldest else { return };
52            if let Some(gone) = self.held.remove(&slot) {
53                self.bytes -= gone.bytes();
54            }
55        }
56    }
57}
58
59/// A slot is keyed coarsely, by what a node is with its volatile values left out; a hit also
60/// needs the value's full key, so a coarse slot costs a miss and never a wrong buffer.
61pub struct Slots {
62    state: Mutex<State>,
63}
64
65impl Default for Slots {
66    fn default() -> Slots {
67        Slots::holding(DEFAULT_SLOT_BYTES)
68    }
69}
70
71impl Slots {
72    pub fn holding(max_bytes: u64) -> Slots {
73        Slots {
74            state: Mutex::new(State {
75                held: HashMap::new(),
76                bytes: 0,
77                max_bytes,
78                clock: 0,
79            }),
80        }
81    }
82
83    fn locked(&self) -> MutexGuard<'_, State> {
84        self.state.lock().unwrap_or_else(|poisoned| {
85            let mut state = poisoned.into_inner();
86            state.held.clear();
87            state.bytes = 0;
88            self.state.clear_poison();
89            state
90        })
91    }
92
93    pub fn get(&self, slot: Hash, key: Hash, node: &str, expected: Expected) -> Option<Entry> {
94        let mut state = self.locked();
95        let tick = state.clock;
96        state.clock += 1;
97        let held = state.held.get_mut(&slot)?;
98        if held.key != key || !held.payload.answers(expected) {
99            return None;
100        }
101        held.read = tick;
102        Some(Entry {
103            payload: held.payload.clone(),
104            traces: super::renamed(&held.traces, node),
105            label: held.label.clone(),
106            tier: Tier::Volatile,
107        })
108    }
109
110    pub fn put(
111        &self,
112        slot: Hash,
113        key: Hash,
114        payload: &Payload,
115        traces: &[FilterTrace],
116        label: Option<&Label>,
117    ) -> Put {
118        let mut state = self.locked();
119        let bytes = payload.bytes() as u64;
120        if bytes > state.max_bytes {
121            return Put::Refused;
122        }
123        let read = state.clock;
124        state.clock += 1;
125        let before = state.held.insert(
126            slot,
127            Slotted {
128                key,
129                payload: payload.clone(),
130                traces: traces.to_vec(),
131                label: label.cloned(),
132                read,
133            },
134        );
135        state.bytes += bytes;
136        if let Some(old) = &before {
137            state.bytes -= old.bytes();
138        }
139        state.evict(Some(slot));
140        match before {
141            Some(_) => Put::Replaced,
142            None => Put::Slotted,
143        }
144    }
145
146    pub fn held_bytes(&self) -> u64 {
147        self.locked().bytes
148    }
149
150    pub fn max_bytes(&self) -> u64 {
151        self.locked().max_bytes
152    }
153
154    pub fn slots(&self) -> usize {
155        self.locked().held.len()
156    }
157
158    /// A lower cap evicts at once, least recently read first.
159    pub fn bound(&self, max_bytes: u64) {
160        let mut state = self.locked();
161        state.max_bytes = max_bytes;
162        state.evict(None);
163    }
164
165    pub fn clear(&self) {
166        let mut state = self.locked();
167        state.held.clear();
168        state.bytes = 0;
169    }
170}