use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use metrale_grammar::compiler::{RuleLevelCache, SnapshotIdentity, mask_snapshot};
use super::engine::GrammarEngine;
const CACHE_DIR_NAME: &str = ".metrale-grammar-cache";
const MAX_PERSISTED_MASKS: usize = 512;
pub(super) struct MaskSnapshot {
path: PathBuf,
identity: SnapshotIdentity,
cache: RuleLevelCache,
saved_entries: Arc<AtomicUsize>,
writing: Arc<AtomicBool>,
}
pub type PrewarmHook = Arc<dyn Fn(usize) + Send + Sync>;
pub(super) fn cache_enabled_from(value: Option<&str>) -> bool {
!matches!(
value
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
.as_str(),
"0" | "false" | "off" | "no"
)
}
pub(super) fn snapshot_path(
model_dir: &Path,
dir_override: Option<&str>,
fingerprint: u64,
) -> PathBuf {
let dir = match dir_override.map(str::trim).filter(|s| !s.is_empty()) {
Some(d) => PathBuf::from(d),
None => model_dir.join(CACHE_DIR_NAME),
};
dir.join(format!("masks-{fingerprint:016x}.bin"))
}
impl GrammarEngine {
pub fn attach_mask_cache(&mut self, model_dir: &Path) {
if !cache_enabled_from(std::env::var("METRALE_GRAMMAR_CACHE").ok().as_deref()) {
tracing::info!("Grammar: on-disk mask cache disabled (METRALE_GRAMMAR_CACHE)");
return;
}
let Some(cache) = self.compiler.rule_cache_handle() else {
return;
};
let identity = self.compiler.snapshot_identity();
let path = snapshot_path(
model_dir,
std::env::var("METRALE_GRAMMAR_CACHE_DIR").ok().as_deref(),
identity.tokenizer_fingerprint,
);
let imported = match self.compiler.load_mask_snapshot(&path) {
Ok(n) => n,
Err(e) => {
tracing::warn!(
"Grammar: mask snapshot unreadable at {}: {e}",
path.display()
);
0
}
};
if imported > 0 {
tracing::info!(
"Grammar: warmed {imported} token masks from {} — the first tool-call \
request skips the cold mask compile (#918)",
path.display(),
);
} else {
tracing::info!(
"Grammar: no usable mask snapshot at {}; the first grammar of this \
process will compute and persist one (#918)",
path.display(),
);
}
self.snapshot = Some(MaskSnapshot {
path,
identity,
cache,
saved_entries: Arc::new(AtomicUsize::new(imported)),
writing: Arc::new(AtomicBool::new(false)),
});
}
pub(crate) fn mask_snapshot_hook(&self) -> Option<PrewarmHook> {
let snap = self.snapshot.as_ref()?;
let (path, identity) = (snap.path.clone(), snap.identity);
let cache = snap.cache.clone();
let saved = Arc::clone(&snap.saved_entries);
let writing = Arc::clone(&snap.writing);
Some(Arc::new(move |_warmed: usize| {
if cache.len() <= saved.load(Ordering::Relaxed) {
return;
}
if writing.swap(true, Ordering::AcqRel) {
return;
}
let (path, cache, saved) = (path.clone(), cache.clone(), Arc::clone(&saved));
let done = Arc::clone(&writing);
let spawned = std::thread::Builder::new()
.name("grammar-mask-save".to_string())
.spawn(move || {
let written =
mask_snapshot::save_to_file(&cache, identity, &path, MAX_PERSISTED_MASKS);
match written {
Ok(n) => {
saved.store(n, Ordering::Relaxed);
tracing::debug!(
"Grammar: persisted {n} token masks to {}",
path.display()
);
}
Err(e) => tracing::debug!(
"Grammar: could not persist masks to {}: {e}",
path.display()
),
}
done.store(false, Ordering::Release);
});
if let Err(e) = spawned {
tracing::debug!("Grammar: mask-snapshot writer not spawned: {e}");
writing.store(false, Ordering::Release);
}
}))
}
}