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