use std::collections::HashMap;
use std::path::PathBuf;
const BLOCK: usize = 64;
const MIN_BLOCKS: usize = 2;
const MAX: usize = 1 << 16;
#[derive(Default)]
pub struct Warm {
by_prefix: HashMap<u64, (String, u64)>,
clock: u64,
path: Option<PathBuf>,
dirty: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Stored {
prefixes: Vec<(u64, String, u64)>,
}
fn chain<'a>(model: &str, tokens: &'a [u32]) -> impl Iterator<Item = u64> + 'a {
let seed = model.bytes().fold(0xCBF2_9CE4_8422_2325u64, |h, b| (h ^ b as u64).wrapping_mul(0x0000_0100_0000_01B3));
tokens.as_chunks::<BLOCK>().0.iter().scan(seed, |h, block| {
for t in block {
for b in t.to_le_bytes() {
*h = (*h ^ b as u64).wrapping_mul(0x0000_0100_0000_01B3);
}
}
Some(*h)
})
}
impl Warm {
pub fn kept_in(path: PathBuf) -> Warm {
let stored: Option<Stored> = std::fs::read(&path).ok().and_then(|b| serde_json::from_slice(&b).ok());
let mut warm = Warm { path: Some(path), ..Warm::default() };
for (hash, node, at) in stored.map(|s| s.prefixes).unwrap_or_default() {
warm.clock = warm.clock.max(at);
warm.by_prefix.insert(hash, (node, at));
}
warm
}
pub fn warmest(&self, model: &str, tokens: &[u32]) -> Option<(&str, usize)> {
let mut best = None;
for (n, hash) in chain(model, tokens).enumerate() {
match self.by_prefix.get(&hash) {
Some((node, _)) => best = Some((node.as_str(), (n + 1) * BLOCK)),
None => break,
}
}
best.filter(|&(_, len)| len >= MIN_BLOCKS * BLOCK)
}
pub fn ran(&mut self, model: &str, tokens: &[u32], node: &str) {
self.clock += 1;
let at = self.clock;
for hash in chain(model, tokens) {
self.by_prefix.insert(hash, (node.to_string(), at));
}
if self.by_prefix.len() > MAX {
let mut ages: Vec<u64> = self.by_prefix.values().map(|&(_, at)| at).collect();
ages.sort_unstable();
let cut = ages[MAX / 8];
self.by_prefix.retain(|_, &mut (_, at)| at > cut);
}
self.dirty = true;
}
pub fn save(&mut self) {
let Some(path) = &self.path else { return };
if !self.dirty {
return;
}
let stored = Stored { prefixes: self.by_prefix.iter().map(|(&h, (node, at))| (h, node.clone(), *at)).collect() };
let Ok(bytes) = serde_json::to_vec(&stored) else { return };
let tmp = path.with_extension("tmp");
if std::fs::write(&tmp, bytes).and_then(|()| std::fs::rename(&tmp, path)).is_ok() {
self.dirty = false;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_longest_prefix_a_node_ran_wins_and_is_kept_across_a_restart() {
let path = std::env::temp_dir().join(format!("superfluid-warm-{}.json", std::process::id()));
let _ = std::fs::remove_file(&path);
let turn1: Vec<u32> = (0..300).collect();
let mut turn2 = turn1.clone();
turn2.extend(1000..1200);
let mut warm = Warm::kept_in(path.clone());
warm.ran("m", &turn1, "a");
warm.ran("m", &turn2[..400], "b");
assert_eq!(warm.warmest("m", &turn2), Some(("b", 384)), "b ran more of it");
assert_eq!(warm.warmest("m", &turn1), Some(("b", 256)), "b ran the same first blocks last");
assert_eq!(warm.warmest("other", &turn2), None, "another model's prefix is its own");
assert_eq!(warm.warmest("m", &(5000..5300).collect::<Vec<u32>>()), None);
assert_eq!(warm.warmest("m", &turn1[..100]), None, "one block is too short to matter");
warm.save();
let again = Warm::kept_in(path.clone());
assert_eq!(again.warmest("m", &turn2), Some(("b", 384)));
let _ = std::fs::remove_file(&path);
}
}