use crate::stats::GLOBAL_STATS;
use ahash::AHashMap;
use lazy_static::lazy_static;
use regex_automata::meta::{Cache, Regex as MetaRegex};
use slotmap::{SlotMap, new_key_type};
use std::ops::Deref;
use std::sync::Weak;
use std::sync::{Arc, Mutex};
struct WeakSharedRegex {
regex: Weak<MetaRegex>,
cache_size: usize,
}
#[derive(Debug, Clone)]
pub struct SharedRegex {
pub regex: Arc<MetaRegex>,
pub cache_key: RegexCacheKey,
}
impl Deref for SharedRegex {
type Target = MetaRegex;
fn deref(&self) -> &Self::Target {
self.regex.deref()
}
}
pub fn get_memoized_regex<T>(
pattern: &str,
regex_factory: impl FnOnce(&str) -> Result<regex_automata::meta::Regex, T>,
) -> Result<SharedRegex, T> {
get_memoized_regex_with_custom_store(pattern, regex_factory, ®EX_STORE)
}
fn get_memoized_regex_with_custom_store<T>(
pattern: &str,
regex_factory: impl FnOnce(&str) -> Result<regex_automata::meta::Regex, T>,
store: &Mutex<RegexStore>,
) -> Result<SharedRegex, T> {
{
let regex_store = store.lock().unwrap();
if let Some(exiting_regex) = regex_store.get(pattern) {
return Ok(exiting_regex);
}
}
let regex = regex_factory(pattern)?;
let mut regex_store = store.lock().unwrap();
Ok(regex_store.insert(pattern, regex))
}
const GC_FREQUENCY: u64 = 1_000;
lazy_static! {
static ref REGEX_STORE: Arc<Mutex<RegexStore>> = Arc::new(Mutex::new(RegexStore::new()));
}
new_key_type! { pub struct RegexCacheKey; }
struct RegexStore {
pattern_index: AHashMap<String, RegexCacheKey>,
key_map: SlotMap<RegexCacheKey, WeakSharedRegex>,
gc_counter: u64,
}
impl RegexStore {
pub fn new() -> Self {
Self {
pattern_index: AHashMap::new(),
key_map: SlotMap::with_key(),
gc_counter: 0,
}
}
fn gc(&mut self) {
self.gc_counter = 0;
self.pattern_index.retain(|_, cache_key| {
if self.key_map.get(*cache_key).unwrap().regex.strong_count() == 0 {
if let Some(old_regex) = self.key_map.remove(*cache_key) {
GLOBAL_STATS.add_total_regex_cache(-(old_regex.cache_size as i64));
}
false
} else {
true
}
});
GLOBAL_STATS.set_total_regexes(self.key_map.len());
}
pub fn get(&self, pattern: &str) -> Option<SharedRegex> {
self.pattern_index.get(pattern).and_then(|cache_key| {
self.key_map
.get(*cache_key)
.and_then(|x| x.regex.upgrade())
.map(|regex| SharedRegex {
regex,
cache_key: *cache_key,
})
})
}
#[cfg(test)]
fn len(&self) -> usize {
debug_assert_eq!(self.pattern_index.len(), self.key_map.len());
self.key_map.len()
}
pub fn insert(&mut self, pattern: &str, regex: MetaRegex) -> SharedRegex {
self.gc_counter += 1;
if self.gc_counter >= GC_FREQUENCY {
self.gc();
}
match self.get(pattern) {
Some(existing_regex) => existing_regex,
_ => {
let shared_regex = Arc::new(regex);
let regex_cache = shared_regex.create_cache();
let cache_key = self.key_map.insert(WeakSharedRegex {
regex: Arc::downgrade(&shared_regex),
cache_size: regex_cache.memory_usage() + std::mem::size_of::<Cache>(),
});
if let Some(old_cache_key) =
self.pattern_index.insert(pattern.to_owned(), cache_key)
{
if let Some(weak_ref) = self.key_map.remove(old_cache_key) {
GLOBAL_STATS.add_total_regex_cache(-(weak_ref.cache_size as i64));
debug_assert!(weak_ref.regex.strong_count() == 0)
}
}
GLOBAL_STATS.set_total_regexes(self.key_map.len());
SharedRegex {
regex: shared_regex,
cache_key,
}
}
}
}
}
#[cfg(test)]
mod test {
use crate::scanner::regex_rule::regex_store::{
GC_FREQUENCY, RegexStore, get_memoized_regex_with_custom_store,
};
use regex_automata::meta::Regex;
use std::sync::Mutex;
#[test]
fn dropped_regexes_should_be_removed_from_global_store() {
let store = Mutex::new(RegexStore::new());
let regex = get_memoized_regex_with_custom_store("test", Regex::new, &store).unwrap();
assert_eq!(store.lock().unwrap().len(), 1);
drop(regex);
store.lock().unwrap().gc();
assert_eq!(store.lock().unwrap().len(), 0);
}
#[test]
fn test_automatic_gc() {
let store = Mutex::new(RegexStore::new());
let regex = get_memoized_regex_with_custom_store("test", Regex::new, &store).unwrap();
drop(regex);
for i in 0..(GC_FREQUENCY - 1) {
let regex =
get_memoized_regex_with_custom_store(&format!("test-{i}"), Regex::new, &store)
.unwrap();
drop(regex)
}
assert_eq!(store.lock().unwrap().len(), 1);
}
}