use std::mem::size_of;
use std::sync::{Arc, Mutex};
use lru::LruCache;
use sha2::{Digest, Sha256};
use crate::{EntityKind, Match, Source};
pub(crate) const MIN_LEN: usize = 256;
pub const DEFAULT_BUDGET: usize = 32 * 1024 * 1024;
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Scanned {
pub matches: Vec<Match>,
pub tokens: Vec<(EntityKind, u64)>,
}
impl Scanned {
fn cost(&self) -> usize {
let names: usize = self
.matches
.iter()
.map(|m| match &m.source {
Source::Regex(name) => name.len(),
_ => 0,
})
.sum();
96 + self.matches.len() * size_of::<Match>() + names + self.tokens.len() * size_of::<(EntityKind, u64)>()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScanCacheStats {
pub entries: usize,
pub bytes: usize,
pub hits: u64,
pub misses: u64,
}
pub(crate) struct ScanCache {
budget: usize,
inner: Mutex<Inner>,
}
struct Inner {
map: LruCache<[u8; 32], (Arc<Scanned>, usize)>,
bytes: usize,
hits: u64,
misses: u64,
}
impl ScanCache {
pub fn new(budget: usize) -> Self {
ScanCache { budget, inner: Mutex::new(Inner { map: LruCache::unbounded(), bytes: 0, hits: 0, misses: 0 }) }
}
pub fn enabled(&self) -> bool {
self.budget > 0
}
pub fn key(rules_id: u64, text: &str) -> [u8; 32] {
let mut h = Sha256::new();
h.update(rules_id.to_le_bytes());
h.update(text.as_bytes());
h.finalize().into()
}
pub fn get(&self, key: &[u8; 32]) -> Option<Arc<Scanned>> {
let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
let found = g.map.get(key).map(|(s, _)| s.clone());
match found {
Some(_) => g.hits += 1,
None => g.misses += 1,
}
found
}
pub fn put(&self, key: [u8; 32], scanned: Arc<Scanned>) {
let cost = scanned.cost();
if cost > self.budget {
return;
}
let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
if let Some((_, old)) = g.map.put(key, (scanned, cost)) {
g.bytes -= old;
}
g.bytes += cost;
while g.bytes > self.budget {
match g.map.pop_lru() {
Some((_, (_, c))) => g.bytes -= c,
None => break,
}
}
}
pub fn clear(&self) {
let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
g.map.clear();
g.bytes = 0;
}
pub fn stats(&self) -> ScanCacheStats {
let g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
ScanCacheStats { entries: g.map.len(), bytes: g.bytes, hits: g.hits, misses: g.misses }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kinds;
fn scanned(n: usize) -> Arc<Scanned> {
let kind = EntityKind::new(kinds::EMAIL).unwrap();
let matches =
(0..n).map(|i| Match { start: i, end: i + 1, kind: kind.clone(), source: Source::BuiltIn }).collect();
Arc::new(Scanned { matches, tokens: Vec::new() })
}
#[test]
fn keys_depend_on_the_rules_and_the_text() {
assert_eq!(ScanCache::key(1, "abc"), ScanCache::key(1, "abc"));
assert_ne!(ScanCache::key(1, "abc"), ScanCache::key(2, "abc"));
assert_ne!(ScanCache::key(1, "abc"), ScanCache::key(1, "abd"));
}
#[test]
fn counts_hits_and_misses() {
let c = ScanCache::new(DEFAULT_BUDGET);
let k = ScanCache::key(1, "x");
assert!(c.get(&k).is_none());
c.put(k, scanned(2));
assert_eq!(c.get(&k).unwrap().matches.len(), 2);
let s = c.stats();
assert_eq!((s.entries, s.hits, s.misses), (1, 1, 1));
assert_eq!(s.bytes, scanned(2).cost());
}
#[test]
fn stays_within_the_budget_and_drops_the_least_recently_used() {
let one = scanned(10).cost();
let c = ScanCache::new(one * 3);
let keys: Vec<_> = (0..3).map(|i| ScanCache::key(1, &i.to_string())).collect();
for k in &keys {
c.put(*k, scanned(10));
}
assert!(c.get(&keys[0]).is_some());
c.put(ScanCache::key(1, "new"), scanned(10));
assert!(c.stats().bytes <= one * 3);
assert!(c.get(&keys[1]).is_none(), "least recently used goes first");
assert!(c.get(&keys[0]).is_some());
assert!(c.get(&keys[2]).is_some());
}
#[test]
fn an_entry_larger_than_the_budget_is_not_kept() {
let c = ScanCache::new(scanned(1).cost());
c.put(ScanCache::key(1, "big"), scanned(1000));
assert_eq!(c.stats().entries, 0);
}
#[test]
fn replacing_an_entry_keeps_the_byte_count_right() {
let c = ScanCache::new(DEFAULT_BUDGET);
let k = ScanCache::key(1, "x");
c.put(k, scanned(5));
c.put(k, scanned(1));
assert_eq!(c.stats().bytes, scanned(1).cost());
c.clear();
assert_eq!(c.stats(), ScanCacheStats { entries: 0, bytes: 0, hits: 0, misses: 0 });
}
}