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    failures: u64,
168    lookups: Recent<Lookup>,
169    shed: Tally,
170    reached: Option<Vec<(i64, usize)>>,
171    /// Each key's latest lookup still held, and the change that made it.
172    looked: HashMap<Hash, (u64, usize)>,
173    change: u64,
174}
175
176impl Recording {
177    pub(crate) fn over(memory: &Memory) -> Recording {
178        Recording {
179            since: memory.counters(),
180            failures: memory.failures().0,
181            lookups: Recent::keeping(usize::MAX),
182            shed: Tally::default(),
183            reached: Some(Vec::new()),
184            looked: HashMap::new(),
185            change: 0,
186        }
187    }
188
189    pub(crate) fn latest(self, kept: usize) -> Recording {
190        Recording {
191            lookups: Recent::keeping(kept),
192            reached: None,
193            ..self
194        }
195    }
196
197    /// A stream's change: each key it asks is a lookup anew.
198    pub(crate) fn begin(&mut self) {
199        self.change += 1;
200        let shed = self.lookups.shed();
201        self.looked.retain(|_, (_, at)| *at >= shed);
202    }
203
204    pub(crate) fn reach(&mut self, at: i64) {
205        let made = self.lookups.made();
206        if let Some(reached) = &mut self.reached {
207            reached.push((at, made));
208        }
209    }
210
211    pub(crate) fn stats(&self, memory: &Memory) -> CacheStats {
212        let tier = self.since(memory);
213        let (failures, why) = memory.failures();
214        let failed = failures > self.failures;
215        CacheStats {
216            lookups: self.lookups.iter().cloned().collect(),
217            shed: self.lookups.shed(),
218            bytes: memory.bytes(),
219            max_bytes: memory.max_bytes(),
220            entries: memory.entries(),
221            evictions: tier.evictions(),
222            tier,
223            reached: self.reached.clone().unwrap_or_default(),
224            typed: Vec::new(),
225            planned: Vec::new(),
226            analysed: 0,
227            unstaged: why.filter(|_| failed || memory.blocked()),
228            unslotted: None,
229        }
230    }
231
232    pub(crate) fn since(&self, memory: &Memory) -> Counters {
233        memory.counters().since(self.since)
234    }
235
236    pub(crate) fn tally(&self) -> Tally {
237        self.shed.plus(Tally::of(self.lookups.iter()))
238    }
239
240    fn made(&mut self, lookup: Lookup) -> usize {
241        let (at, shed) = self.lookups.pushed(lookup);
242        if let Some(shed) = shed {
243            self.shed = self.shed.plus(Tally::one(&shed));
244        }
245        at
246    }
247
248    /// Asked again in one change, a key's lookup is settled by what any answer found.
249    pub(super) fn answered(
250        &mut self,
251        (key, node): (Hash, &str),
252        kind: PayloadKind,
253        (outcome, from_store): (Outcome, bool),
254    ) {
255        let now = self.change;
256        let held = self.looked(key).filter(|(change, _)| *change == now);
257        if let Some(lookup) = held.and_then(|(_, at)| self.lookups.get_mut(at)) {
258            let found = |o: Outcome| match o {
259                Outcome::Hit => 2,
260                Outcome::Prefix => 1,
261                _ => 0,
262            };
263            lookup.kind = kind;
264            if found(outcome) > found(lookup.outcome) {
265                (lookup.outcome, lookup.from_store) = (outcome, from_store);
266            }
267            return;
268        }
269        let lookup = Lookup {
270            node: node.to_string(),
271            key,
272            kind,
273            outcome,
274            from_store,
275        };
276        let at = self.made(lookup);
277        self.looked.insert(key, (self.change, at));
278    }
279
280    pub(super) fn kept(&mut self, key: Hash, kept: Kept) {
281        let Some(lookup) = self
282            .looked(key)
283            .and_then(|(_, at)| self.lookups.get_mut(at))
284        else {
285            return;
286        };
287        lookup.outcome = match (lookup.outcome, kept) {
288            (Outcome::ComputedNotStored, Kept::Held) => Outcome::ComputedStored,
289            (Outcome::ComputedNotStored, Kept::Replaced) => Outcome::ComputedReplaced,
290            (Outcome::Hit, Kept::Held | Kept::Replaced) => Outcome::Extended,
291            (outcome, _) => outcome,
292        };
293    }
294
295    pub(crate) fn reused(&mut self, key: Hash, node: &str, kind: PayloadKind) {
296        if self.looked(key).is_none() {
297            return;
298        }
299        self.made(Lookup {
300            node: node.to_string(),
301            key,
302            kind,
303            outcome: Outcome::Reused,
304            from_store: false,
305        });
306    }
307
308    fn looked(&self, key: Hash) -> Option<(u64, usize)> {
309        self.looked.get(&key).copied()
310    }
311}