use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Mutex;
use super::super::workload::{render_workload_key, WorkloadKey};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum AutorouteCacheMiss {
NoCacheConfigured,
CacheRejected,
WorkloadUnclassified,
BucketAbsent,
RuntimeClassUnproved,
RouteQuarantined,
HealthUnavailable,
PeerIdentityChanged,
}
impl AutorouteCacheMiss {
pub(crate) const ALL: [Self; 8] = [
Self::NoCacheConfigured,
Self::CacheRejected,
Self::WorkloadUnclassified,
Self::BucketAbsent,
Self::RuntimeClassUnproved,
Self::RouteQuarantined,
Self::HealthUnavailable,
Self::PeerIdentityChanged,
];
pub(crate) fn label(self) -> &'static str {
match self {
Self::NoCacheConfigured => "no-cache-configured",
Self::CacheRejected => "cache-rejected",
Self::WorkloadUnclassified => "workload-unclassified",
Self::BucketAbsent => "bucket-absent",
Self::RuntimeClassUnproved => "runtime-class-unproved",
Self::RouteQuarantined => "route-quarantined",
Self::HealthUnavailable => "route-health-unavailable",
Self::PeerIdentityChanged => "gpu-peer-identity-changed",
}
}
pub(crate) fn repair(self) -> &'static str {
match self {
Self::NoCacheConfigured => {
"configure an autoroute cache with --autoroute-cache <path>, then calibrate it"
}
Self::CacheRejected => {
"the cache belongs to a different build, host, detector corpus or scan config; \
recalibrate this exact configuration (recalibrating one bucket will not help)"
}
Self::WorkloadUnclassified => {
"report the batch shape; autoroute cannot bucket it, so no calibration can cover it"
}
Self::BucketAbsent | Self::RuntimeClassUnproved => {
"rerun this same scan once with --autoroute-calibrate --autoroute-gpu to cover \
every bucket listed above, or run keyhog calibrate-autoroute for the core ladder"
}
Self::RouteQuarantined => {
"a persisted route faulted at runtime and was quarantined; recalibrate after \
fixing the fault reported with the quarantine"
}
Self::HealthUnavailable => {
"restart KeyHog, then run keyhog calibrate-autoroute; route-health state could \
not be read"
}
Self::PeerIdentityChanged => {
"the GPU peer changed since calibration; recalibrate on the current device"
}
}
}
fn counter(self) -> &'static AtomicU64 {
&MISSES[self as usize]
}
}
static HITS: AtomicU64 = AtomicU64::new(0);
static CALIBRATION_REUSES: AtomicU64 = AtomicU64::new(0);
#[allow(clippy::declare_interior_mutable_const)]
const ZERO: AtomicU64 = AtomicU64::new(0);
static MISSES: [AtomicU64; AutorouteCacheMiss::ALL.len()] = [ZERO; AutorouteCacheMiss::ALL.len()];
static MISSING_BUCKETS: Mutex<BTreeMap<String, usize>> = Mutex::new(BTreeMap::new());
static MISSING_BUCKETS_ELIDED: AtomicUsize = AtomicUsize::new(0);
const MAX_TRACKED_MISSING_BUCKETS: usize = 64;
pub(crate) fn record_hit() {
HITS.fetch_add(1, Ordering::Relaxed);
keyhog_profile::record_cache_hit(keyhog_profile::CacheId::AutorouteDecision);
}
pub(crate) fn record_calibration_reuse() {
CALIBRATION_REUSES.fetch_add(1, Ordering::Relaxed);
keyhog_profile::record_cache_hit(keyhog_profile::CacheId::AutorouteCalibration);
}
pub(crate) fn record_miss(cause: AutorouteCacheMiss) {
cause.counter().fetch_add(1, Ordering::Relaxed);
keyhog_profile::record_cache_miss(keyhog_profile::CacheId::AutorouteDecision);
}
pub(crate) fn record_bucket_miss(cause: AutorouteCacheMiss, key: &WorkloadKey) {
record_miss(cause);
let Ok(mut buckets) = MISSING_BUCKETS.lock() else {
return;
};
if buckets.len() >= MAX_TRACKED_MISSING_BUCKETS {
let rendered = render_workload_key(key);
match buckets.get_mut(&rendered) {
Some(count) => *count += 1,
None => {
MISSING_BUCKETS_ELIDED.fetch_add(1, Ordering::Relaxed);
}
}
return;
}
*buckets.entry(render_workload_key(key)).or_insert(0) += 1;
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct AutorouteCacheStats {
pub(crate) hits: u64,
pub(crate) misses: u64,
pub(crate) by_cause: Vec<(AutorouteCacheMiss, u64)>,
pub(crate) missing_buckets: Vec<(String, usize)>,
pub(crate) missing_buckets_elided: usize,
}
impl AutorouteCacheStats {
pub(crate) fn lookups(&self) -> u64 {
self.hits + self.misses
}
pub(crate) fn hit_rate_percent(&self) -> Option<f64> {
let lookups = self.lookups();
(lookups > 0).then(|| (self.hits as f64) * 100.0 / (lookups as f64))
}
pub(crate) fn primary_cause(&self) -> Option<AutorouteCacheMiss> {
self.by_cause
.iter()
.max_by_key(|(cause, count)| (*count, std::cmp::Reverse(*cause)))
.map(|(cause, _)| *cause)
}
}
pub(crate) fn snapshot() -> AutorouteCacheStats {
let by_cause: Vec<(AutorouteCacheMiss, u64)> = AutorouteCacheMiss::ALL
.into_iter()
.filter_map(|cause| {
let count = cause.counter().load(Ordering::Relaxed);
(count > 0).then_some((cause, count))
})
.collect();
let missing_buckets = MISSING_BUCKETS
.lock()
.map(|buckets| {
buckets
.iter()
.map(|(key, count)| (key.clone(), *count))
.collect()
})
.unwrap_or_default();
AutorouteCacheStats {
hits: HITS.load(Ordering::Relaxed),
misses: by_cause.iter().map(|(_, count)| *count).sum(),
by_cause,
missing_buckets,
missing_buckets_elided: MISSING_BUCKETS_ELIDED.load(Ordering::Relaxed),
}
}
pub(crate) fn render_summary(stats: &AutorouteCacheStats) -> Option<String> {
let rate = stats.hit_rate_percent()?;
let mut line = format!(
"autoroute cache: {:.1}% hit ({} hit / {} lookup(s))",
rate,
stats.hits,
stats.lookups()
);
if stats.misses > 0 {
line.push_str("; miss lookup(s) left affected batches unscanned; coverage is incomplete");
let causes = stats
.by_cause
.iter()
.map(|(cause, count)| format!("{}={count}", cause.label()))
.collect::<Vec<_>>()
.join(" ");
line.push_str(&format!("; miss causes: {causes}"));
let distinct = stats.missing_buckets.len();
if distinct > 0 {
line.push_str(&format!(
"; {distinct} distinct uncalibrated bucket(s){}",
if stats.missing_buckets_elided > 0 {
format!(" (+{} not listed)", stats.missing_buckets_elided)
} else {
String::new()
}
));
}
if let Some(cause) = stats.primary_cause() {
line.push_str(&format!("; repair: {}", cause.repair()));
}
}
Some(line)
}
pub(crate) fn render_missing_buckets(stats: &AutorouteCacheStats) -> Vec<String> {
let mut ordered: Vec<&(String, usize)> = stats.missing_buckets.iter().collect();
ordered.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
ordered
.into_iter()
.map(|(key, count)| format!("{count} batch(es): {key}"))
.collect()
}
#[cfg(test)]
#[path = "../../../../../tests/unit/autoroute_telemetry.rs"]
mod tests;