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    /// The disk answered it for the call that looked it up.
32    pub from_store: bool,
33}
34
35/// Lookups by what each came to. An extended value was found short and written again, so it is
36/// both a miss and new.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct Tally {
39    pub hit: usize,
40    /// The hits the disk answered; the rest memory held.
41    pub from_store: usize,
42    pub miss: usize,
43    pub prefix: usize,
44    pub new: usize,
45    pub reused: usize,
46}
47
48impl Tally {
49    pub fn of<'l>(lookups: impl IntoIterator<Item = &'l Lookup>) -> Tally {
50        let mut tally = Tally::default();
51        for lookup in lookups {
52            tally = tally.plus(Tally::one(lookup));
53        }
54        tally
55    }
56
57    fn one(lookup: &Lookup) -> Tally {
58        let outcome = lookup.outcome;
59        let is = |held: bool| usize::from(held);
60        Tally {
61            hit: is(outcome == Outcome::Hit),
62            from_store: is(outcome == Outcome::Hit && lookup.from_store),
63            prefix: is(outcome == Outcome::Prefix),
64            reused: is(outcome == Outcome::Reused),
65            miss: is(!matches!(
66                outcome,
67                Outcome::Hit | Outcome::Prefix | Outcome::Reused
68            )),
69            new: is(matches!(
70                outcome,
71                Outcome::ComputedStored | Outcome::ComputedReplaced | Outcome::Extended
72            )),
73        }
74    }
75
76    pub fn looked(&self) -> usize {
77        self.hit + self.miss + self.prefix
78    }
79
80    pub fn plus(self, more: Tally) -> Tally {
81        self.each(more, usize::wrapping_add)
82    }
83
84    /// Wrapping, so a tally that fell meanwhile adds back as the fall.
85    pub fn minus(self, less: Tally) -> Tally {
86        self.each(less, usize::wrapping_sub)
87    }
88
89    fn each(self, other: Tally, by: fn(usize, usize) -> usize) -> Tally {
90        Tally {
91            hit: by(self.hit, other.hit),
92            from_store: by(self.from_store, other.from_store),
93            miss: by(self.miss, other.miss),
94            prefix: by(self.prefix, other.prefix),
95            new: by(self.new, other.new),
96            reused: by(self.reused, other.reused),
97        }
98    }
99}
100
101/// Every lookup in order, or a stream's latest, and memory as the render left it.
102#[derive(Clone, Debug, Default, PartialEq)]
103pub struct CacheStats {
104    pub lookups: Vec<Lookup>,
105    /// Lookups made before `lookups`.
106    pub shed: usize,
107    pub bytes: u64,
108    pub max_bytes: u64,
109    pub entries: usize,
110    pub evictions: u64,
111    pub tier: Counters,
112    /// Each output sample a pull reached, and how many lookups had been made by then.
113    pub reached: Vec<(i64, usize)>,
114    pub typed: Vec<String>,
115    pub planned: Vec<String>,
116    /// Supports found by analysis: none a stored node told.
117    pub analysed: usize,
118    /// Why memory stopped writing to the disk meanwhile.
119    pub unstaged: Option<String>,
120    pub unslotted: Option<String>,
121}
122
123impl CacheStats {
124    pub fn nodes(&self) -> usize {
125        let mut names: Vec<&str> = self.lookups.iter().map(|l| l.node.as_str()).collect();
126        names.sort_unstable();
127        names.dedup();
128        names.len()
129    }
130
131    pub fn hits(&self) -> usize {
132        self.count(|o| o == Outcome::Hit)
133    }
134
135    pub fn computed(&self) -> usize {
136        self.count(|o| !matches!(o, Outcome::Hit | Outcome::Reused))
137    }
138
139    pub fn stored(&self) -> usize {
140        self.count(|o| o == Outcome::ComputedStored)
141    }
142
143    pub fn replaced(&self) -> usize {
144        self.count(|o| o == Outcome::ComputedReplaced)
145    }
146
147    pub fn extended(&self) -> usize {
148        self.count(|o| o == Outcome::Extended)
149    }
150
151    pub fn reused(&self) -> usize {
152        self.count(|o| o == Outcome::Reused)
153    }
154
155    pub fn tally(&self) -> Tally {
156        Tally::of(&self.lookups)
157    }
158
159    fn count(&self, of: impl Fn(Outcome) -> bool) -> usize {
160        self.lookups.iter().filter(|l| of(l.outcome)).count()
161    }
162}
163
164/// What the memo told one render or stream: one lookup per key a change asks.
165pub(crate) struct Recording {
166    since: Counters,
167    /// Memory's clock where it began.
168    began: u64,
169    failures: u64,
170    lookups: Recent<Lookup>,
171    shed: Tally,
172    reached: Option<Vec<(i64, usize)>>,
173    /// Each key's latest lookup still held, and the change that made it.
174    looked: HashMap<Hash, (u64, usize)>,
175    change: u64,
176}
177
178impl Recording {
179    pub(crate) fn over(memory: &Memory) -> Recording {
180        Recording {
181            since: memory.counters(),
182            began: memory.clock(),
183            failures: memory.failures().0,
184            lookups: Recent::keeping(usize::MAX),
185            shed: Tally::default(),
186            reached: Some(Vec::new()),
187            looked: HashMap::new(),
188            change: 0,
189        }
190    }
191
192    pub(crate) fn latest(self, kept: usize) -> Recording {
193        Recording {
194            lookups: Recent::keeping(kept),
195            reached: None,
196            ..self
197        }
198    }
199
200    /// A stream's change: each key it asks is a lookup anew.
201    pub(crate) fn begin(&mut self) {
202        self.change += 1;
203        let shed = self.lookups.shed();
204        self.looked.retain(|_, (_, at)| *at >= shed);
205    }
206
207    pub(crate) fn reach(&mut self, at: i64) {
208        let made = self.lookups.made();
209        if let Some(reached) = &mut self.reached {
210            reached.push((at, made));
211        }
212    }
213
214    pub(crate) fn stats(&self, memory: &Memory) -> CacheStats {
215        let tier = self.since(memory);
216        let (failures, why) = memory.failures();
217        let failed = failures > self.failures;
218        CacheStats {
219            lookups: self.lookups.iter().cloned().collect(),
220            shed: self.lookups.shed(),
221            bytes: memory.bytes(),
222            max_bytes: memory.max_bytes(),
223            entries: memory.entries(),
224            evictions: tier.evictions(),
225            tier,
226            reached: self.reached.clone().unwrap_or_default(),
227            typed: Vec::new(),
228            planned: Vec::new(),
229            analysed: 0,
230            unstaged: why.filter(|_| failed || memory.blocked()),
231            unslotted: None,
232        }
233    }
234
235    pub(crate) fn began(&self) -> u64 {
236        self.began
237    }
238
239    /// Whether the disk answered `key`'s lookup.
240    pub(crate) fn stored(&self, key: Hash) -> bool {
241        let lookup = self.looked(key).and_then(|(_, at)| self.lookups.get(at));
242        lookup.is_some_and(|lookup| lookup.from_store)
243    }
244
245    pub(crate) fn since(&self, memory: &Memory) -> Counters {
246        memory.counters().since(self.since)
247    }
248
249    pub(crate) fn tally(&self) -> Tally {
250        self.shed.plus(Tally::of(self.lookups.iter()))
251    }
252
253    fn made(&mut self, lookup: Lookup) -> usize {
254        let (at, shed) = self.lookups.pushed(lookup);
255        if let Some(shed) = shed {
256            self.shed = self.shed.plus(Tally::one(&shed));
257        }
258        at
259    }
260
261    /// Asked again in one change, a key's lookup is settled by what any answer found.
262    pub(super) fn answered(
263        &mut self,
264        (key, node): (Hash, &str),
265        kind: PayloadKind,
266        (outcome, from_store): (Outcome, bool),
267    ) {
268        let now = self.change;
269        let held = self.looked(key).filter(|(change, _)| *change == now);
270        if let Some(lookup) = held.and_then(|(_, at)| self.lookups.get_mut(at)) {
271            let found = |o: Outcome| match o {
272                Outcome::Hit => 2,
273                Outcome::Prefix => 1,
274                _ => 0,
275            };
276            lookup.kind = kind;
277            if found(outcome) > found(lookup.outcome) {
278                (lookup.outcome, lookup.from_store) = (outcome, from_store);
279            }
280            return;
281        }
282        let lookup = Lookup {
283            node: node.to_string(),
284            key,
285            kind,
286            outcome,
287            from_store,
288        };
289        let at = self.made(lookup);
290        self.looked.insert(key, (self.change, at));
291    }
292
293    pub(super) fn kept(&mut self, key: Hash, kept: Kept) {
294        let Some(lookup) = self
295            .looked(key)
296            .and_then(|(_, at)| self.lookups.get_mut(at))
297        else {
298            return;
299        };
300        lookup.outcome = match (lookup.outcome, kept) {
301            (Outcome::ComputedNotStored, Kept::Held) => Outcome::ComputedStored,
302            (Outcome::ComputedNotStored, Kept::Replaced) => Outcome::ComputedReplaced,
303            (Outcome::Hit, Kept::Held | Kept::Replaced) => Outcome::Extended,
304            (outcome, _) => outcome,
305        };
306    }
307
308    pub(crate) fn reused(&mut self, key: Hash, node: &str, kind: PayloadKind) {
309        if self.looked(key).is_none() {
310            return;
311        }
312        self.made(Lookup {
313            node: node.to_string(),
314            key,
315            kind,
316            outcome: Outcome::Reused,
317            from_store: false,
318        });
319    }
320
321    fn looked(&self, key: Hash) -> Option<(u64, usize)> {
322        self.looked.get(&key).copied()
323    }
324}