Skip to main content

sva_engine/cache/
stats.rs

1// Concern: what one render asked its store, and what each lookup came to | Non-concern: deciding what to store (the store's own worth_storing) | IO: (loads, stores) -> CacheStats
2
3use std::path::Path;
4use std::sync::{Mutex, MutexGuard, PoisonError};
5use std::time::Duration;
6
7use sva_formula::Hash;
8use sva_samples::{FilterTrace, Label};
9
10use super::{Cache, Entry, Expected, Payload, PayloadKind, Put, Slots, Tier};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Outcome {
14    Hit(Tier),
15    ComputedStored,
16    ComputedNotStored,
17    /// A volatile node's value, kept in a slot that held nothing.
18    ComputedSlotted,
19    /// A volatile node's value, kept in place of the slot's last one.
20    ComputedReplaced,
21}
22
23#[derive(Clone, Debug, PartialEq)]
24pub struct Lookup {
25    pub node: String,
26    pub key: Hash,
27    pub kind: PayloadKind,
28    pub outcome: Outcome,
29}
30
31/// Every lookup in the order the render made it.
32#[derive(Clone, Debug, Default, PartialEq)]
33pub struct CacheStats {
34    pub lookups: Vec<Lookup>,
35}
36
37impl CacheStats {
38    /// Distinct nodes looked up, however many kinds each asked for.
39    pub fn nodes(&self) -> usize {
40        let mut names: Vec<&str> = self.lookups.iter().map(|l| l.node.as_str()).collect();
41        names.sort_unstable();
42        names.dedup();
43        names.len()
44    }
45
46    pub fn hits(&self) -> usize {
47        self.count(|o| matches!(o, Outcome::Hit(_)))
48    }
49
50    pub fn hits_in(&self, tier: Tier) -> usize {
51        self.count(|o| o == Outcome::Hit(tier))
52    }
53
54    pub fn computed(&self) -> usize {
55        self.count(|o| !matches!(o, Outcome::Hit(_)))
56    }
57
58    pub fn stored(&self) -> usize {
59        self.count(|o| o == Outcome::ComputedStored)
60    }
61
62    pub fn slotted(&self) -> usize {
63        self.count(|o| o == Outcome::ComputedSlotted)
64    }
65
66    pub fn replaced(&self) -> usize {
67        self.count(|o| o == Outcome::ComputedReplaced)
68    }
69
70    fn count(&self, of: impl Fn(Outcome) -> bool) -> usize {
71        self.lookups.iter().filter(|l| of(l.outcome)).count()
72    }
73}
74
75/// Wraps the stores a render was handed, so the render itself never says what it looked up.
76pub(crate) struct Recording<'a> {
77    inner: &'a dyn Cache,
78    slots: Option<&'a Slots>,
79    lookups: Mutex<Vec<Lookup>>,
80}
81
82impl<'a> Recording<'a> {
83    pub(crate) fn over(inner: &'a dyn Cache, slots: Option<&'a Slots>) -> Recording<'a> {
84        Recording {
85            inner,
86            slots,
87            lookups: Mutex::new(Vec::new()),
88        }
89    }
90
91    pub(crate) fn finish(self) -> CacheStats {
92        CacheStats {
93            lookups: self
94                .lookups
95                .into_inner()
96                .unwrap_or_else(PoisonError::into_inner),
97        }
98    }
99
100    /// One node's view of the stores: a volatile node reads through its slot and writes
101    /// nowhere else, every other node reads and writes the stores themselves.
102    pub(crate) fn at(&'a self, slot: Option<Hash>) -> Lens<'a> {
103        Lens {
104            recording: self,
105            slot,
106        }
107    }
108
109    fn held(&self) -> MutexGuard<'_, Vec<Lookup>> {
110        self.lookups.lock().unwrap_or_else(PoisonError::into_inner)
111    }
112
113    fn record(&self, key: Hash, node: &str, expected: Expected, found: Option<&Entry>) {
114        self.held().push(Lookup {
115            node: node.to_string(),
116            key,
117            kind: expected.kind(),
118            outcome: found.map_or(Outcome::ComputedNotStored, |e| Outcome::Hit(e.tier)),
119        });
120    }
121
122    fn settle(&self, key: Hash, outcome: Outcome) {
123        if let Some(missed) = self
124            .held()
125            .iter_mut()
126            .rev()
127            .find(|l| l.key == key && l.outcome == Outcome::ComputedNotStored)
128        {
129            missed.outcome = outcome;
130        }
131    }
132}
133
134pub(crate) struct Lens<'a> {
135    recording: &'a Recording<'a>,
136    slot: Option<Hash>,
137}
138
139impl Lens<'_> {
140    fn slotted(&self, kind: PayloadKind) -> Option<(&Slots, Hash)> {
141        let slot = super::mixed(self.slot?, &[kind as u64, 0x73_6c_6f_74]);
142        Some((self.recording.slots?, slot))
143    }
144}
145
146impl Cache for Lens<'_> {
147    fn load(&self, key: Hash, node: &str, expected: Expected) -> Option<Entry> {
148        let inner = self.recording.inner;
149        let found = match self.slot {
150            None => inner.load(key, node, expected),
151            Some(_) => inner.peek(key, node, expected).or_else(|| {
152                let (slots, slot) = self.slotted(expected.kind())?;
153                slots.get(slot, key, node, expected)
154            }),
155        };
156        self.recording.record(key, node, expected, found.as_ref());
157        found
158    }
159
160    fn peek(&self, key: Hash, node: &str, expected: Expected) -> Option<Entry> {
161        self.recording.inner.peek(key, node, expected)
162    }
163
164    fn store(&self, key: Hash, payload: &Payload, traces: &[FilterTrace], label: Option<&Label>) {
165        if self.slot.is_none() {
166            self.recording.settle(key, Outcome::ComputedStored);
167            return self.recording.inner.store(key, payload, traces, label);
168        }
169        let Some((slots, slot)) = self.slotted(payload.kind()) else {
170            return;
171        };
172        match slots.put(slot, key, payload, traces, label) {
173            Put::Slotted => self.recording.settle(key, Outcome::ComputedSlotted),
174            Put::Replaced => self.recording.settle(key, Outcome::ComputedReplaced),
175            Put::Refused => {}
176        }
177    }
178
179    fn holds(&self, key: Hash) -> bool {
180        self.recording.inner.holds(key)
181    }
182
183    /// A slot keeps a volatile node's buffer or frames; its spectral sum is kept nowhere.
184    fn worth_storing(&self, cost: Duration, bytes: usize, kind: PayloadKind) -> bool {
185        match self.slot {
186            None => self.recording.inner.worth_storing(cost, bytes, kind),
187            Some(_) => kind != PayloadKind::Symbolic && self.recording.slots.is_some(),
188        }
189    }
190
191    fn sweep(&self) {
192        if self.slot.is_none() {
193            self.recording.inner.sweep();
194        }
195    }
196
197    fn held_bytes(&self) -> u64 {
198        self.recording.inner.held_bytes()
199    }
200
201    fn evicted_bytes(&self) -> u64 {
202        self.recording.inner.evicted_bytes()
203    }
204
205    fn faults(&self) -> u64 {
206        self.recording.inner.faults()
207    }
208
209    fn max_bytes(&self) -> u64 {
210        self.recording.inner.max_bytes()
211    }
212
213    fn dir(&self) -> Option<&Path> {
214        self.recording.inner.dir()
215    }
216}