use std::collections::HashMap;
use sva_formula::Hash;
use super::PayloadKind;
use super::memory::{Counters, Kept, Memory};
use crate::recent::Recent;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
Hit,
ComputedStored,
ComputedNotStored,
ComputedReplaced,
Extended,
Prefix,
Reused,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Lookup {
pub node: String,
pub key: Hash,
pub kind: PayloadKind,
pub outcome: Outcome,
pub from_store: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Tally {
pub hit: usize,
pub from_store: usize,
pub miss: usize,
pub prefix: usize,
pub new: usize,
pub reused: usize,
}
impl Tally {
pub fn of<'l>(lookups: impl IntoIterator<Item = &'l Lookup>) -> Tally {
let mut tally = Tally::default();
for lookup in lookups {
tally = tally.plus(Tally::one(lookup));
}
tally
}
fn one(lookup: &Lookup) -> Tally {
let outcome = lookup.outcome;
let is = |held: bool| usize::from(held);
Tally {
hit: is(outcome == Outcome::Hit),
from_store: is(outcome == Outcome::Hit && lookup.from_store),
prefix: is(outcome == Outcome::Prefix),
reused: is(outcome == Outcome::Reused),
miss: is(!matches!(
outcome,
Outcome::Hit | Outcome::Prefix | Outcome::Reused
)),
new: is(matches!(
outcome,
Outcome::ComputedStored | Outcome::ComputedReplaced | Outcome::Extended
)),
}
}
pub fn looked(&self) -> usize {
self.hit + self.miss + self.prefix
}
pub fn plus(self, more: Tally) -> Tally {
self.each(more, usize::wrapping_add)
}
pub fn minus(self, less: Tally) -> Tally {
self.each(less, usize::wrapping_sub)
}
fn each(self, other: Tally, by: fn(usize, usize) -> usize) -> Tally {
Tally {
hit: by(self.hit, other.hit),
from_store: by(self.from_store, other.from_store),
miss: by(self.miss, other.miss),
prefix: by(self.prefix, other.prefix),
new: by(self.new, other.new),
reused: by(self.reused, other.reused),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CacheStats {
pub lookups: Vec<Lookup>,
pub shed: usize,
pub bytes: u64,
pub max_bytes: u64,
pub entries: usize,
pub evictions: u64,
pub tier: Counters,
pub reached: Vec<(i64, usize)>,
pub typed: Vec<String>,
pub planned: Vec<String>,
pub analysed: usize,
pub unstaged: Option<String>,
pub unslotted: Option<String>,
}
impl CacheStats {
pub fn nodes(&self) -> usize {
let mut names: Vec<&str> = self.lookups.iter().map(|l| l.node.as_str()).collect();
names.sort_unstable();
names.dedup();
names.len()
}
pub fn hits(&self) -> usize {
self.count(|o| o == Outcome::Hit)
}
pub fn computed(&self) -> usize {
self.count(|o| !matches!(o, Outcome::Hit | Outcome::Reused))
}
pub fn stored(&self) -> usize {
self.count(|o| o == Outcome::ComputedStored)
}
pub fn replaced(&self) -> usize {
self.count(|o| o == Outcome::ComputedReplaced)
}
pub fn extended(&self) -> usize {
self.count(|o| o == Outcome::Extended)
}
pub fn reused(&self) -> usize {
self.count(|o| o == Outcome::Reused)
}
pub fn tally(&self) -> Tally {
Tally::of(&self.lookups)
}
fn count(&self, of: impl Fn(Outcome) -> bool) -> usize {
self.lookups.iter().filter(|l| of(l.outcome)).count()
}
}
pub(crate) struct Recording {
since: Counters,
began: u64,
failures: u64,
lookups: Recent<Lookup>,
shed: Tally,
reached: Option<Vec<(i64, usize)>>,
looked: HashMap<Hash, (u64, usize)>,
change: u64,
}
impl Recording {
pub(crate) fn over(memory: &Memory) -> Recording {
Recording {
since: memory.counters(),
began: memory.clock(),
failures: memory.failures().0,
lookups: Recent::keeping(usize::MAX),
shed: Tally::default(),
reached: Some(Vec::new()),
looked: HashMap::new(),
change: 0,
}
}
pub(crate) fn latest(self, kept: usize) -> Recording {
Recording {
lookups: Recent::keeping(kept),
reached: None,
..self
}
}
pub(crate) fn begin(&mut self) {
self.change += 1;
let shed = self.lookups.shed();
self.looked.retain(|_, (_, at)| *at >= shed);
}
pub(crate) fn reach(&mut self, at: i64) {
let made = self.lookups.made();
if let Some(reached) = &mut self.reached {
reached.push((at, made));
}
}
pub(crate) fn stats(&self, memory: &Memory) -> CacheStats {
let tier = self.since(memory);
let (failures, why) = memory.failures();
let failed = failures > self.failures;
CacheStats {
lookups: self.lookups.iter().cloned().collect(),
shed: self.lookups.shed(),
bytes: memory.bytes(),
max_bytes: memory.max_bytes(),
entries: memory.entries(),
evictions: tier.evictions(),
tier,
reached: self.reached.clone().unwrap_or_default(),
typed: Vec::new(),
planned: Vec::new(),
analysed: 0,
unstaged: why.filter(|_| failed || memory.blocked()),
unslotted: None,
}
}
pub(crate) fn began(&self) -> u64 {
self.began
}
pub(crate) fn stored(&self, key: Hash) -> bool {
let lookup = self.looked(key).and_then(|(_, at)| self.lookups.get(at));
lookup.is_some_and(|lookup| lookup.from_store)
}
pub(crate) fn since(&self, memory: &Memory) -> Counters {
memory.counters().since(self.since)
}
pub(crate) fn tally(&self) -> Tally {
self.shed.plus(Tally::of(self.lookups.iter()))
}
fn made(&mut self, lookup: Lookup) -> usize {
let (at, shed) = self.lookups.pushed(lookup);
if let Some(shed) = shed {
self.shed = self.shed.plus(Tally::one(&shed));
}
at
}
pub(super) fn answered(
&mut self,
(key, node): (Hash, &str),
kind: PayloadKind,
(outcome, from_store): (Outcome, bool),
) {
let now = self.change;
let held = self.looked(key).filter(|(change, _)| *change == now);
if let Some(lookup) = held.and_then(|(_, at)| self.lookups.get_mut(at)) {
let found = |o: Outcome| match o {
Outcome::Hit => 2,
Outcome::Prefix => 1,
_ => 0,
};
lookup.kind = kind;
if found(outcome) > found(lookup.outcome) {
(lookup.outcome, lookup.from_store) = (outcome, from_store);
}
return;
}
let lookup = Lookup {
node: node.to_string(),
key,
kind,
outcome,
from_store,
};
let at = self.made(lookup);
self.looked.insert(key, (self.change, at));
}
pub(super) fn kept(&mut self, key: Hash, kept: Kept) {
let Some(lookup) = self
.looked(key)
.and_then(|(_, at)| self.lookups.get_mut(at))
else {
return;
};
lookup.outcome = match (lookup.outcome, kept) {
(Outcome::ComputedNotStored, Kept::Held) => Outcome::ComputedStored,
(Outcome::ComputedNotStored, Kept::Replaced) => Outcome::ComputedReplaced,
(Outcome::Hit, Kept::Held | Kept::Replaced) => Outcome::Extended,
(outcome, _) => outcome,
};
}
pub(crate) fn reused(&mut self, key: Hash, node: &str, kind: PayloadKind) {
if self.looked(key).is_none() {
return;
}
self.made(Lookup {
node: node.to_string(),
key,
kind,
outcome: Outcome::Reused,
from_store: false,
});
}
fn looked(&self, key: Hash) -> Option<(u64, usize)> {
self.looked.get(&key).copied()
}
}