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    /// Supports found by analysis: none a stored node told.
49    pub analysed: usize,
50    /// Why memory stopped writing to the disk meanwhile.
51    pub unstaged: Option<String>,
52    pub unslotted: Option<String>,
53}
54
55impl CacheStats {
56    pub fn nodes(&self) -> usize {
57        let mut names: Vec<&str> = self.lookups.iter().map(|l| l.node.as_str()).collect();
58        names.sort_unstable();
59        names.dedup();
60        names.len()
61    }
62
63    pub fn hits(&self) -> usize {
64        self.count(|o| o == Outcome::Hit)
65    }
66
67    pub fn computed(&self) -> usize {
68        self.count(|o| !matches!(o, Outcome::Hit | Outcome::Reused))
69    }
70
71    pub fn stored(&self) -> usize {
72        self.count(|o| o == Outcome::ComputedStored)
73    }
74
75    pub fn replaced(&self) -> usize {
76        self.count(|o| o == Outcome::ComputedReplaced)
77    }
78
79    pub fn extended(&self) -> usize {
80        self.count(|o| o == Outcome::Extended)
81    }
82
83    pub fn reused(&self) -> usize {
84        self.count(|o| o == Outcome::Reused)
85    }
86
87    fn count(&self, of: impl Fn(Outcome) -> bool) -> usize {
88        self.lookups.iter().filter(|l| of(l.outcome)).count()
89    }
90}
91
92/// What the memo told one render or stream: one lookup per key a change asks.
93pub(crate) struct Recording {
94    since: Counters,
95    failures: u64,
96    lookups: Recent<Lookup>,
97    reached: Option<Vec<(i64, usize)>>,
98    /// Each key's latest lookup still held, and the change that made it.
99    looked: HashMap<Hash, (u64, usize)>,
100    change: u64,
101}
102
103impl Recording {
104    pub(crate) fn over(memory: &Memory) -> Recording {
105        Recording {
106            since: memory.counters(),
107            failures: memory.failures().0,
108            lookups: Recent::keeping(usize::MAX),
109            reached: Some(Vec::new()),
110            looked: HashMap::new(),
111            change: 0,
112        }
113    }
114
115    pub(crate) fn latest(self, kept: usize) -> Recording {
116        Recording {
117            lookups: Recent::keeping(kept),
118            reached: None,
119            ..self
120        }
121    }
122
123    /// A stream's change: each key it asks is a lookup anew.
124    pub(crate) fn begin(&mut self) {
125        self.change += 1;
126        let shed = self.lookups.shed();
127        self.looked.retain(|_, (_, at)| *at >= shed);
128    }
129
130    pub(crate) fn reach(&mut self, at: i64) {
131        let made = self.lookups.made();
132        if let Some(reached) = &mut self.reached {
133            reached.push((at, made));
134        }
135    }
136
137    pub(crate) fn stats(&self, memory: &Memory) -> CacheStats {
138        let tier = self.since(memory);
139        let (failures, why) = memory.failures();
140        let failed = failures > self.failures;
141        CacheStats {
142            lookups: self.lookups.iter().cloned().collect(),
143            shed: self.lookups.shed(),
144            bytes: memory.bytes(),
145            max_bytes: memory.max_bytes(),
146            entries: memory.entries(),
147            evictions: tier.evictions(),
148            tier,
149            reached: self.reached.clone().unwrap_or_default(),
150            typed: Vec::new(),
151            planned: Vec::new(),
152            analysed: 0,
153            unstaged: why.filter(|_| failed || memory.blocked()),
154            unslotted: None,
155        }
156    }
157
158    pub(crate) fn since(&self, memory: &Memory) -> Counters {
159        memory.counters().since(self.since)
160    }
161
162    /// Asked again in one change, a key's lookup is settled by what any answer found.
163    pub(super) fn answered(&mut self, key: Hash, node: &str, kind: PayloadKind, outcome: Outcome) {
164        let now = self.change;
165        let held = self.looked(key).filter(|(change, _)| *change == now);
166        if let Some(lookup) = held.and_then(|(_, at)| self.lookups.get_mut(at)) {
167            let found = |o: Outcome| match o {
168                Outcome::Hit => 2,
169                Outcome::Prefix => 1,
170                _ => 0,
171            };
172            lookup.kind = kind;
173            if found(outcome) > found(lookup.outcome) {
174                lookup.outcome = outcome;
175            }
176            return;
177        }
178        let lookup = Lookup {
179            node: node.to_string(),
180            key,
181            kind,
182            outcome,
183        };
184        let at = self.lookups.push(lookup);
185        self.looked.insert(key, (self.change, at));
186    }
187
188    pub(super) fn kept(&mut self, key: Hash, kept: Kept) {
189        let Some(lookup) = self
190            .looked(key)
191            .and_then(|(_, at)| self.lookups.get_mut(at))
192        else {
193            return;
194        };
195        lookup.outcome = match (lookup.outcome, kept) {
196            (Outcome::ComputedNotStored, Kept::Held) => Outcome::ComputedStored,
197            (Outcome::ComputedNotStored, Kept::Replaced) => Outcome::ComputedReplaced,
198            (Outcome::Hit, Kept::Held | Kept::Replaced) => Outcome::Extended,
199            (outcome, _) => outcome,
200        };
201    }
202
203    pub(crate) fn reused(&mut self, key: Hash, node: &str, kind: PayloadKind) {
204        if self.looked(key).is_none() {
205            return;
206        }
207        self.lookups.push(Lookup {
208            node: node.to_string(),
209            key,
210            kind,
211            outcome: Outcome::Reused,
212        });
213    }
214
215    fn looked(&self, key: Hash) -> Option<(u64, usize)> {
216        self.looked.get(&key).copied()
217    }
218}