Skip to main content

sva_engine/cache/
stats.rs

1// Concern: what one render asked the store, what each lookup came to, and the store after it | Non-concern: what the store evicts (store.rs) | IO: (loads, stores) -> CacheStats
2
3use std::sync::{Mutex, MutexGuard, PoisonError};
4
5use sva_formula::Hash;
6use sva_samples::Label;
7
8use super::store::{Kept, Stamp};
9use super::{Cache, CachePolicy, Entry, Expected, Payload, PayloadKind};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Outcome {
13    Hit,
14    ComputedStored,
15    ComputedNotStored,
16    /// A volatile node's value, stored in place of its last one.
17    ComputedReplaced,
18}
19
20#[derive(Clone, Debug, PartialEq)]
21pub struct Lookup {
22    pub node: String,
23    pub key: Hash,
24    pub kind: PayloadKind,
25    pub outcome: Outcome,
26}
27
28/// Every lookup in the order the render made it, and the store as the render left it.
29#[derive(Clone, Debug, Default, PartialEq)]
30pub struct CacheStats {
31    pub lookups: Vec<Lookup>,
32    pub bytes: u64,
33    pub max_bytes: u64,
34    pub entries: usize,
35    /// Entries this render's stores evicted.
36    pub evictions: u64,
37}
38
39impl CacheStats {
40    /// Distinct nodes looked up, however many kinds each asked for.
41    pub fn nodes(&self) -> usize {
42        let mut names: Vec<&str> = self.lookups.iter().map(|l| l.node.as_str()).collect();
43        names.sort_unstable();
44        names.dedup();
45        names.len()
46    }
47
48    pub fn hits(&self) -> usize {
49        self.count(|o| o == Outcome::Hit)
50    }
51
52    pub fn computed(&self) -> usize {
53        self.count(|o| o != Outcome::Hit)
54    }
55
56    pub fn stored(&self) -> usize {
57        self.count(|o| o == Outcome::ComputedStored)
58    }
59
60    pub fn replaced(&self) -> usize {
61        self.count(|o| o == Outcome::ComputedReplaced)
62    }
63
64    fn count(&self, of: impl Fn(Outcome) -> bool) -> usize {
65        self.lookups.iter().filter(|l| of(l.outcome)).count()
66    }
67}
68
69/// One render's view of the store, so the render itself never says what it looked up.
70pub(crate) struct Recording<'a> {
71    cache: &'a Cache,
72    policy: CachePolicy,
73    tree: u64,
74    evictions: u64,
75    lookups: Mutex<Vec<Lookup>>,
76}
77
78impl<'a> Recording<'a> {
79    /// `policy` where the render names one, the store's own where it names none.
80    pub(crate) fn over(cache: &'a Cache, policy: Option<CachePolicy>) -> Recording<'a> {
81        Recording {
82            cache,
83            policy: policy.unwrap_or_else(|| cache.policy()),
84            tree: cache.begin_tree(),
85            evictions: cache.evictions(),
86            lookups: Mutex::new(Vec::new()),
87        }
88    }
89
90    pub(crate) fn finish(self) -> CacheStats {
91        CacheStats {
92            lookups: self
93                .lookups
94                .into_inner()
95                .unwrap_or_else(PoisonError::into_inner),
96            bytes: self.cache.bytes(),
97            max_bytes: self.cache.max_bytes(),
98            entries: self.cache.entries(),
99            evictions: self.cache.evictions() - self.evictions,
100        }
101    }
102
103    /// One node's view: `slot` where a volatile parameter reaches it, `fork` where two nodes
104    /// read it, `target` where the render is of it.
105    pub(crate) fn at(&self, slot: Option<Hash>, fork: bool, target: bool) -> Lens<'_> {
106        Lens {
107            recording: self,
108            slot,
109            fork,
110            stores: self.stores(fork, target),
111        }
112    }
113
114    pub(crate) fn stores(&self, fork: bool, target: bool) -> bool {
115        self.policy.stores(fork, target)
116    }
117
118    pub(crate) fn holds(&self, key: Hash) -> bool {
119        self.cache.holds(key)
120    }
121
122    fn held(&self) -> MutexGuard<'_, Vec<Lookup>> {
123        self.lookups.lock().unwrap_or_else(PoisonError::into_inner)
124    }
125
126    fn settle(&self, key: Hash, outcome: Outcome) {
127        if let Some(missed) = self
128            .held()
129            .iter_mut()
130            .rev()
131            .find(|l| l.key == key && l.outcome == Outcome::ComputedNotStored)
132        {
133            missed.outcome = outcome;
134        }
135    }
136}
137
138pub(crate) struct Lens<'a> {
139    recording: &'a Recording<'a>,
140    slot: Option<Hash>,
141    fork: bool,
142    stores: bool,
143}
144
145impl Lens<'_> {
146    fn stamp(&self, kind: PayloadKind) -> Stamp {
147        Stamp {
148            tree: self.recording.tree,
149            fork: self.fork,
150            slot: self
151                .slot
152                .map(|slot| super::mixed(slot, &[kind as u64, 0x73_6c_6f_74])),
153        }
154    }
155
156    pub(crate) fn load(&self, key: Hash, node: &str, expected: Expected) -> Option<Entry> {
157        let found = self
158            .recording
159            .cache
160            .load(key, expected, self.stamp(expected.kind()));
161        self.recording.held().push(Lookup {
162            node: node.to_string(),
163            key,
164            kind: expected.kind(),
165            outcome: match found {
166                Some(_) => Outcome::Hit,
167                None => Outcome::ComputedNotStored,
168            },
169        });
170        found
171    }
172
173    pub(crate) fn store(&self, key: Hash, payload: &Payload, label: Option<&Label>) {
174        if !self.stores {
175            return;
176        }
177        let stamp = self.stamp(payload.kind());
178        match self.recording.cache.store(key, payload, label, stamp) {
179            Kept::Held => self.recording.settle(key, Outcome::ComputedStored),
180            Kept::Replaced => self.recording.settle(key, Outcome::ComputedReplaced),
181            Kept::Refused => {}
182        }
183    }
184}