Skip to main content

sva_engine/cache/
stats.rs

1// Concern: what one render asked of its values and memory, how far its output got, and what each lookup came to | Non-concern: what memory evicts (memory.rs) | IO: (loads, stores) -> CacheStats
2
3use std::collections::HashMap;
4
5use sva_formula::Hash;
6
7use super::PayloadKind;
8use super::memory::{Counters, Kept, Memory};
9use crate::recent::Recent;
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    /// Found short, the rest computed and stored.
19    Extended,
20    /// Found only up to a switch.
21    Prefix,
22    Reused,
23}
24
25#[derive(Clone, Debug, PartialEq)]
26pub struct Lookup {
27    pub node: String,
28    pub key: Hash,
29    pub kind: PayloadKind,
30    pub outcome: Outcome,
31}
32
33/// Every lookup in order, or a stream's latest, and memory as the render left it.
34#[derive(Clone, Debug, Default, PartialEq)]
35pub struct CacheStats {
36    pub lookups: Vec<Lookup>,
37    /// Lookups made before `lookups`.
38    pub shed: usize,
39    pub bytes: u64,
40    pub max_bytes: u64,
41    pub entries: usize,
42    pub evictions: u64,
43    pub tier: Counters,
44    /// Each output sample a pull reached, and how many lookups had been made by then.
45    pub reached: Vec<(i64, usize)>,
46    pub typed: Vec<String>,
47    pub planned: Vec<String>,
48    /// Why memory stopped writing to the disk meanwhile.
49    pub unstaged: Option<String>,
50    pub unslotted: Option<String>,
51}
52
53impl CacheStats {
54    pub fn nodes(&self) -> usize {
55        let mut names: Vec<&str> = self.lookups.iter().map(|l| l.node.as_str()).collect();
56        names.sort_unstable();
57        names.dedup();
58        names.len()
59    }
60
61    pub fn hits(&self) -> usize {
62        self.count(|o| o == Outcome::Hit)
63    }
64
65    pub fn computed(&self) -> usize {
66        self.count(|o| !matches!(o, Outcome::Hit | Outcome::Reused))
67    }
68
69    pub fn stored(&self) -> usize {
70        self.count(|o| o == Outcome::ComputedStored)
71    }
72
73    pub fn replaced(&self) -> usize {
74        self.count(|o| o == Outcome::ComputedReplaced)
75    }
76
77    pub fn extended(&self) -> usize {
78        self.count(|o| o == Outcome::Extended)
79    }
80
81    pub fn reused(&self) -> usize {
82        self.count(|o| o == Outcome::Reused)
83    }
84
85    fn count(&self, of: impl Fn(Outcome) -> bool) -> usize {
86        self.lookups.iter().filter(|l| of(l.outcome)).count()
87    }
88}
89
90/// What the memo told one render or stream: one lookup per key a change asks.
91pub(crate) struct Recording {
92    since: Counters,
93    failures: u64,
94    lookups: Recent<Lookup>,
95    reached: Option<Vec<(i64, usize)>>,
96    /// Each key's latest lookup still held, and the change that made it.
97    looked: HashMap<Hash, (u64, usize)>,
98    change: u64,
99}
100
101impl Recording {
102    pub(crate) fn over(memory: &Memory) -> Recording {
103        Recording {
104            since: memory.counters(),
105            failures: memory.failures().0,
106            lookups: Recent::keeping(usize::MAX),
107            reached: Some(Vec::new()),
108            looked: HashMap::new(),
109            change: 0,
110        }
111    }
112
113    pub(crate) fn latest(self, kept: usize) -> Recording {
114        Recording {
115            lookups: Recent::keeping(kept),
116            reached: None,
117            ..self
118        }
119    }
120
121    /// A stream's change: each key it asks is a lookup anew.
122    pub(crate) fn begin(&mut self) {
123        self.change += 1;
124        let shed = self.lookups.shed();
125        self.looked.retain(|_, (_, at)| *at >= shed);
126    }
127
128    pub(crate) fn reach(&mut self, at: i64) {
129        let made = self.lookups.made();
130        if let Some(reached) = &mut self.reached {
131            reached.push((at, made));
132        }
133    }
134
135    pub(crate) fn stats(&self, memory: &Memory) -> CacheStats {
136        let tier = self.since(memory);
137        let (failures, why) = memory.failures();
138        let failed = failures > self.failures;
139        CacheStats {
140            lookups: self.lookups.iter().cloned().collect(),
141            shed: self.lookups.shed(),
142            bytes: memory.bytes(),
143            max_bytes: memory.max_bytes(),
144            entries: memory.entries(),
145            evictions: tier.evictions(),
146            tier,
147            reached: self.reached.clone().unwrap_or_default(),
148            typed: Vec::new(),
149            planned: Vec::new(),
150            unstaged: why.filter(|_| failed || memory.blocked()),
151            unslotted: None,
152        }
153    }
154
155    pub(crate) fn since(&self, memory: &Memory) -> Counters {
156        memory.counters().since(self.since)
157    }
158
159    /// Asked again in one change, a key's lookup is settled by what any answer found.
160    pub(super) fn answered(&mut self, key: Hash, node: &str, kind: PayloadKind, outcome: Outcome) {
161        let now = self.change;
162        let held = self.looked(key).filter(|(change, _)| *change == now);
163        if let Some(lookup) = held.and_then(|(_, at)| self.lookups.get_mut(at)) {
164            let found = |o: Outcome| match o {
165                Outcome::Hit => 2,
166                Outcome::Prefix => 1,
167                _ => 0,
168            };
169            lookup.kind = kind;
170            if found(outcome) > found(lookup.outcome) {
171                lookup.outcome = outcome;
172            }
173            return;
174        }
175        let lookup = Lookup {
176            node: node.to_string(),
177            key,
178            kind,
179            outcome,
180        };
181        let at = self.lookups.push(lookup);
182        self.looked.insert(key, (self.change, at));
183    }
184
185    pub(super) fn kept(&mut self, key: Hash, kept: Kept) {
186        let Some(lookup) = self
187            .looked(key)
188            .and_then(|(_, at)| self.lookups.get_mut(at))
189        else {
190            return;
191        };
192        lookup.outcome = match (lookup.outcome, kept) {
193            (Outcome::ComputedNotStored, Kept::Held) => Outcome::ComputedStored,
194            (Outcome::ComputedNotStored, Kept::Replaced) => Outcome::ComputedReplaced,
195            (Outcome::Hit, Kept::Held | Kept::Replaced) => Outcome::Extended,
196            (outcome, _) => outcome,
197        };
198    }
199
200    pub(crate) fn reused(&mut self, key: Hash, node: &str, kind: PayloadKind) {
201        if self.looked(key).is_none() {
202            return;
203        }
204        self.lookups.push(Lookup {
205            node: node.to_string(),
206            key,
207            kind,
208            outcome: Outcome::Reused,
209        });
210    }
211
212    fn looked(&self, key: Hash) -> Option<(u64, usize)> {
213        self.looked.get(&key).copied()
214    }
215}