Skip to main content

sva_engine/cache/
log.rs

1// Concern: writes a render's lookups as log lines, per second of output and per node | Non-concern: counting them (stats.rs), where the lines go or when | IO: (CacheStats, rate) -> text
2
3use std::collections::BTreeMap;
4use std::fmt::Write;
5
6use super::{CacheStats, Lookup, Outcome};
7
8#[derive(Default)]
9struct Tally {
10    hit: usize,
11    miss: usize,
12    prefix: usize,
13    new: usize,
14    reused: usize,
15}
16
17impl Tally {
18    fn of<'l>(lookups: impl IntoIterator<Item = &'l Lookup>) -> Tally {
19        let mut tally = Tally::default();
20        for lookup in lookups {
21            tally.add(lookup);
22        }
23        tally
24    }
25
26    /// An extended value was found short and written again, so it is both a miss and new.
27    fn add(&mut self, lookup: &Lookup) {
28        let outcome = lookup.outcome;
29        match outcome {
30            Outcome::Hit => self.hit += 1,
31            Outcome::Prefix => self.prefix += 1,
32            Outcome::Reused => self.reused += 1,
33            _ => self.miss += 1,
34        }
35        if matches!(
36            outcome,
37            Outcome::ComputedStored | Outcome::ComputedReplaced | Outcome::Extended
38        ) {
39            self.new += 1;
40        }
41    }
42
43    fn looked(&self) -> usize {
44        self.hit + self.miss + self.prefix
45    }
46}
47
48impl std::fmt::Display for Tally {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(
51            f,
52            "hit={} miss={} prefix={} new={} reused={}",
53            self.hit, self.miss, self.prefix, self.new, self.reused
54        )
55    }
56}
57
58fn percent(part: usize, whole: usize) -> f64 {
59    match whole {
60        0 => 0.0,
61        _ => 100.0 * part as f64 / whole as f64,
62    }
63}
64
65/// Read once the render ends, so each lookup is counted as it finally settled: a run is stored
66/// only when its node ends. `pass` is what the render looked up before its first block.
67pub fn cache_log(stats: &CacheStats, rate: u32) -> String {
68    let (lookups, reached) = (&stats.lookups, &stats.reached);
69    let mut out = String::new();
70    let mut cuts: Vec<(String, usize)> = Vec::new();
71    let mut second = None;
72    for (k, &(at, made)) in reached.iter().enumerate() {
73        let whole = at.div_euclid(i64::from(rate));
74        match second {
75            None => cuts.push(("pass".to_string(), made)),
76            Some(last) if whole > last || k + 1 == reached.len() => {
77                cuts.push((format!("t={:.3}s", at as f64 / f64::from(rate)), made));
78            }
79            Some(_) => continue,
80        }
81        second = Some(whole);
82    }
83    if cuts.last().is_none_or(|(_, made)| *made < lookups.len()) {
84        cuts.push(("end".to_string(), lookups.len()));
85    }
86    let (mut from, mut hits, mut looked) = (0, 0, 0);
87    for (label, to) in cuts {
88        let tally = Tally::of(&lookups[from..to]);
89        (hits, looked) = (hits + tally.hit, looked + tally.looked());
90        let cum = percent(hits, looked);
91        let _ = writeln!(out, "sva-cache {label} {tally} cum-hit={cum:.1}%");
92        from = to;
93    }
94    let mut nodes: BTreeMap<&str, Tally> = BTreeMap::new();
95    for lookup in lookups {
96        nodes.entry(&lookup.node).or_default().add(lookup);
97    }
98    for (node, tally) in &nodes {
99        let _ = writeln!(out, "sva-cache node {tally} {node}");
100    }
101    let total = Tally::of(lookups);
102    let _ = writeln!(
103        out,
104        "sva-cache total {total} hit-rate={:.1}% nodes={} entries={} bytes={}",
105        percent(total.hit, total.looked()),
106        nodes.len(),
107        stats.entries,
108        stats.bytes
109    );
110    out
111}