Skip to main content

sva_engine/cache/
stats.rs

1// Concern: what one render asked of its values and the store, how far its output got, and what each lookup came to | Non-concern: what the store evicts (store.rs) | IO: (loads, stores) -> CacheStats
2
3use sva_formula::Hash;
4use sva_samples::Label;
5
6use super::store::{Kept, Stamp};
7use super::{Cache, CachePolicy, Entry, Expected, Payload, PayloadKind};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum Outcome {
11    Hit,
12    ComputedStored,
13    ComputedNotStored,
14    /// A volatile node's value, stored in place of its last one.
15    ComputedReplaced,
16    Extended,
17    /// Found only up to a switch.
18    Prefix,
19}
20
21#[derive(Clone, Debug, PartialEq)]
22pub struct Lookup {
23    pub node: String,
24    pub key: Hash,
25    pub kind: PayloadKind,
26    pub outcome: Outcome,
27    /// What a persistent store answered.
28    pub store: Option<bool>,
29}
30
31/// Every lookup in order, and the store as the render left it.
32#[derive(Clone, Debug, Default, PartialEq)]
33pub struct CacheStats {
34    pub lookups: Vec<Lookup>,
35    pub bytes: u64,
36    pub max_bytes: u64,
37    pub entries: usize,
38    /// Entries this render's stores evicted.
39    pub evictions: u64,
40    /// Each output sample a pull reached, and how many lookups had been made by then.
41    pub reached: Vec<(i64, usize)>,
42    pub typed: Vec<String>,
43    pub planned: Vec<String>,
44    /// Why a render stopped staging beside its store.
45    pub unstaged: Option<String>,
46}
47
48impl CacheStats {
49    pub fn nodes(&self) -> usize {
50        let mut names: Vec<&str> = self.lookups.iter().map(|l| l.node.as_str()).collect();
51        names.sort_unstable();
52        names.dedup();
53        names.len()
54    }
55
56    pub fn hits(&self) -> usize {
57        self.count(|o| o == Outcome::Hit)
58    }
59
60    pub fn computed(&self) -> usize {
61        self.count(|o| o != Outcome::Hit)
62    }
63
64    pub fn stored(&self) -> usize {
65        self.count(|o| o == Outcome::ComputedStored)
66    }
67
68    pub fn replaced(&self) -> usize {
69        self.count(|o| o == Outcome::ComputedReplaced)
70    }
71
72    pub fn extended(&self) -> usize {
73        self.count(|o| o == Outcome::Extended)
74    }
75
76    fn count(&self, of: impl Fn(Outcome) -> bool) -> usize {
77        self.lookups.iter().filter(|l| of(l.outcome)).count()
78    }
79}
80
81pub(crate) struct Recording {
82    cache: Option<Cache>,
83    policy: CachePolicy,
84    tree: u64,
85    evictions: u64,
86    lookups: Vec<Lookup>,
87    reached: Vec<(i64, usize)>,
88}
89
90impl Recording {
91    /// `policy` where the render names one, else the store's own.
92    pub(crate) fn over(cache: Option<&Cache>, policy: Option<CachePolicy>) -> Recording {
93        Recording {
94            cache: cache.cloned(),
95            policy: policy.unwrap_or_else(|| cache.map_or(CachePolicy::None, Cache::policy)),
96            tree: cache.map_or(0, Cache::begin_tree),
97            evictions: cache.map_or(0, Cache::evictions),
98            lookups: Vec::new(),
99            reached: Vec::new(),
100        }
101    }
102
103    pub(crate) fn found(&mut self, lookups: Vec<Lookup>) {
104        self.lookups.extend(lookups);
105    }
106
107    pub(crate) fn reach(&mut self, at: i64) {
108        self.reached.push((at, self.lookups.len()));
109    }
110
111    pub(crate) fn stats(&self) -> CacheStats {
112        let cache = self.cache.as_ref();
113        CacheStats {
114            lookups: self.lookups.clone(),
115            bytes: cache.map_or(0, Cache::bytes),
116            max_bytes: cache.map_or(0, Cache::max_bytes),
117            entries: cache.map_or(0, Cache::entries),
118            evictions: cache.map_or(0, |c| c.evictions() - self.evictions),
119            reached: self.reached.clone(),
120            typed: Vec::new(),
121            planned: Vec::new(),
122            unstaged: None,
123        }
124    }
125
126    pub(crate) fn stores(&self, fork: bool, target: bool) -> bool {
127        self.cache.is_some() && self.policy.stores(fork, target)
128    }
129
130    pub(crate) fn mark_every(&self) -> usize {
131        self.cache.as_ref().map_or(usize::MAX, Cache::mark_every)
132    }
133
134    /// `slot` where a volatile parameter reaches it, `fork` where two values read it.
135    pub(crate) fn stamp(&self, slot: Option<Hash>, fork: bool, kind: PayloadKind) -> Stamp {
136        Stamp {
137            tree: self.tree,
138            fork,
139            slot: slot.map(|slot| super::mixed(slot, &[kind as u64, 0x73_6c_6f_74])),
140        }
141    }
142
143    pub(crate) fn note(
144        &mut self,
145        node: &str,
146        key: Hash,
147        kind: PayloadKind,
148        outcome: Outcome,
149    ) -> usize {
150        self.lookups.push(Lookup {
151            node: node.to_string(),
152            key,
153            kind,
154            outcome,
155            store: None,
156        });
157        self.lookups.len() - 1
158    }
159
160    /// Read unnoted; its value notes one lookup.
161    pub(crate) fn load(&self, key: Hash, expected: Expected, stamp: Stamp) -> Option<Entry> {
162        self.cache.as_ref()?.load(key, expected, stamp)
163    }
164
165    /// What a value computed, merged into what the store holds of it.
166    pub(crate) fn store(
167        &mut self,
168        (key, noted): (Hash, Option<usize>),
169        payload: Payload,
170        label: Option<&Label>,
171        stamp: Stamp,
172    ) {
173        let Some(cache) = &self.cache else {
174            return;
175        };
176        let outcome = match cache.merge(key, payload, label, stamp) {
177            Kept::Held => Outcome::ComputedStored,
178            Kept::Replaced => Outcome::ComputedReplaced,
179            Kept::Refused => return,
180        };
181        let Some(lookup) = noted.and_then(|at| self.lookups.get_mut(at)) else {
182            return;
183        };
184        match lookup.outcome {
185            Outcome::ComputedNotStored => lookup.outcome = outcome,
186            Outcome::Hit if lookup.kind == PayloadKind::Run => lookup.outcome = Outcome::Extended,
187            _ => {}
188        }
189    }
190}