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