use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum CommandKind {
AssertEdge,
RetireEdge,
UpsertConcept,
WriteBulkAtomic,
RebuildCurrent,
RegisterModel,
Shutdown,
BulkImportChunk,
WriteConceptsChunk,
WriteAnalyticsChunk,
UpsertEmbeddingChunk,
Archive,
RebuildFts,
ShadowRebuild,
}
impl CommandKind {
pub const ALL: &'static [CommandKind] = &[
CommandKind::AssertEdge,
CommandKind::RetireEdge,
CommandKind::UpsertConcept,
CommandKind::WriteBulkAtomic,
CommandKind::RebuildCurrent,
CommandKind::RegisterModel,
CommandKind::Shutdown,
CommandKind::BulkImportChunk,
CommandKind::WriteConceptsChunk,
CommandKind::WriteAnalyticsChunk,
CommandKind::UpsertEmbeddingChunk,
CommandKind::Archive,
CommandKind::RebuildFts,
CommandKind::ShadowRebuild,
];
pub const COUNT: usize = CommandKind::ALL.len();
pub const fn index(self) -> usize {
self as usize
}
pub const fn as_str(self) -> &'static str {
match self {
CommandKind::AssertEdge => "assert_edge",
CommandKind::RetireEdge => "retire_edge",
CommandKind::UpsertConcept => "upsert_concept",
CommandKind::WriteBulkAtomic => "write_bulk_atomic",
CommandKind::RebuildCurrent => "rebuild_current",
CommandKind::RegisterModel => "register_model",
CommandKind::Shutdown => "shutdown",
CommandKind::BulkImportChunk => "bulk_import_chunk",
CommandKind::WriteConceptsChunk => "write_concepts_chunk",
CommandKind::WriteAnalyticsChunk => "write_analytics_chunk",
CommandKind::UpsertEmbeddingChunk => "upsert_embedding_chunk",
CommandKind::Archive => "archive",
CommandKind::RebuildFts => "rebuild_fts",
CommandKind::ShadowRebuild => "shadow_rebuild",
}
}
pub const fn exempt_from_budget(self) -> bool {
matches!(
self,
CommandKind::WriteBulkAtomic | CommandKind::Archive | CommandKind::RebuildCurrent
)
}
}
impl std::fmt::Display for CommandKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub const BUCKET_BOUNDS_MICROS: &[u64] = &[
100, 300, 1_000, 3_000, 10_000, 30_000, 100_000, 300_000, 1_000_000,
];
pub const BUCKET_COUNT: usize = BUCKET_BOUNDS_MICROS.len() + 1;
#[allow(dead_code)] fn bucket_of(micros: u64) -> usize {
BUCKET_BOUNDS_MICROS
.iter()
.position(|&bound| micros <= bound)
.unwrap_or(BUCKET_BOUNDS_MICROS.len())
}
pub struct HoldTimer {
#[cfg(feature = "metrics")]
start: std::time::Instant,
}
impl HoldTimer {
#[inline]
pub fn start() -> Self {
Self {
#[cfg(feature = "metrics")]
start: std::time::Instant::now(),
}
}
#[inline]
pub fn elapsed(&self) -> Duration {
#[cfg(feature = "metrics")]
{
self.start.elapsed()
}
#[cfg(not(feature = "metrics"))]
{
Duration::ZERO
}
}
}
#[cfg(feature = "metrics")]
mod imp {
use super::{bucket_of, CommandKind, BUCKET_COUNT};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
#[derive(Debug, Default)]
struct Kind {
turns: AtomicU64,
total_micros: AtomicU64,
over_budget: AtomicU64,
longest_micros: AtomicU64,
buckets: [AtomicU64; BUCKET_COUNT],
}
#[derive(Debug, Default)]
pub struct ActorMetrics {
kinds: [Kind; CommandKind::COUNT],
longest: AtomicU64,
depth_samples: AtomicU64,
high_depth_sum: AtomicU64,
high_depth_max: AtomicU64,
low_depth_sum: AtomicU64,
low_depth_max: AtomicU64,
}
const MICROS_SHIFT: u32 = 8;
const KIND_MASK: u64 = (1 << MICROS_SHIFT) - 1;
impl ActorMetrics {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn record_turn(&self, high_depth: usize, low_depth: usize) {
self.depth_samples.fetch_add(1, Ordering::Relaxed);
for (sum, max, depth) in [
(
&self.high_depth_sum,
&self.high_depth_max,
high_depth as u64,
),
(&self.low_depth_sum, &self.low_depth_max, low_depth as u64),
] {
sum.fetch_add(depth, Ordering::Relaxed);
max.fetch_max(depth, Ordering::Relaxed);
}
}
#[inline]
pub fn record_hold(&self, kind: CommandKind, held: Duration) {
let micros = held.as_micros().min(super::MICROS_CEILING as u128) as u64;
let k = &self.kinds[kind.index()];
k.turns.fetch_add(1, Ordering::Relaxed);
k.total_micros.fetch_add(micros, Ordering::Relaxed);
k.buckets[bucket_of(micros)].fetch_add(1, Ordering::Relaxed);
k.longest_micros.fetch_max(micros, Ordering::Relaxed);
if !kind.exempt_from_budget() && held > crate::CHUNK_BUDGET {
k.over_budget.fetch_add(1, Ordering::Relaxed);
}
self.longest.fetch_max(
(micros << MICROS_SHIFT) | kind.index() as u64,
Ordering::Relaxed,
);
}
pub fn snapshot(&self) -> super::MetricsSnapshot {
let samples = self.depth_samples.load(Ordering::Relaxed);
let mean = |sum: &AtomicU64| {
if samples == 0 {
0.0
} else {
sum.load(Ordering::Relaxed) as f64 / samples as f64
}
};
let packed = self.longest.load(Ordering::Relaxed);
let longest_micros = packed >> MICROS_SHIFT;
let longest = (longest_micros > 0)
.then(|| {
let idx = (packed & KIND_MASK) as usize;
CommandKind::ALL
.get(idx)
.map(|&kind| (kind, Duration::from_micros(longest_micros)))
})
.flatten();
let kinds: Vec<_> = CommandKind::ALL
.iter()
.map(|&kind| {
let k = &self.kinds[kind.index()];
let turns = k.turns.load(Ordering::Relaxed);
let total = k.total_micros.load(Ordering::Relaxed);
super::KindSnapshot {
kind,
turns,
over_budget: k.over_budget.load(Ordering::Relaxed),
mean: total
.checked_div(turns)
.map_or(Duration::ZERO, Duration::from_micros),
longest: Duration::from_micros(k.longest_micros.load(Ordering::Relaxed)),
buckets: std::array::from_fn(|i| k.buckets[i].load(Ordering::Relaxed)),
}
})
.collect();
super::MetricsSnapshot {
turns: kinds.iter().map(|k| k.turns).sum(),
depth_samples: samples,
high_depth_mean: mean(&self.high_depth_sum),
high_depth_max: self.high_depth_max.load(Ordering::Relaxed),
low_depth_mean: mean(&self.low_depth_sum),
low_depth_max: self.low_depth_max.load(Ordering::Relaxed),
longest,
kinds,
}
}
}
}
#[cfg(not(feature = "metrics"))]
mod imp {
use super::CommandKind;
use std::time::Duration;
#[derive(Debug, Default)]
pub struct ActorMetrics;
impl ActorMetrics {
pub fn new() -> Self {
Self
}
#[inline]
pub fn record_turn(&self, _high_depth: usize, _low_depth: usize) {}
#[inline]
pub fn record_hold(&self, _kind: CommandKind, _held: Duration) {}
}
}
pub use imp::ActorMetrics;
#[allow(dead_code)]
const MICROS_CEILING: u64 = (1u64 << 56) - 1;
#[cfg(feature = "metrics")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KindSnapshot {
pub kind: CommandKind,
pub turns: u64,
pub over_budget: u64,
pub mean: Duration,
pub longest: Duration,
pub buckets: [u64; BUCKET_COUNT],
}
#[cfg(feature = "metrics")]
#[derive(Debug, Clone, PartialEq)]
pub struct MetricsSnapshot {
pub turns: u64,
pub depth_samples: u64,
pub high_depth_mean: f64,
pub high_depth_max: u64,
pub low_depth_mean: f64,
pub low_depth_max: u64,
pub longest: Option<(CommandKind, Duration)>,
pub kinds: Vec<KindSnapshot>,
}
#[cfg(feature = "metrics")]
impl MetricsSnapshot {
pub fn budget_violations(&self) -> Vec<&KindSnapshot> {
let mut v: Vec<_> = self.kinds.iter().filter(|k| k.over_budget > 0).collect();
v.sort_by_key(|k| std::cmp::Reverse(k.over_budget));
v
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_kind_indexes_to_its_own_slot() {
for (i, &kind) in CommandKind::ALL.iter().enumerate() {
assert_eq!(kind.index(), i, "{kind} is out of order in ALL");
}
assert_eq!(CommandKind::COUNT, CommandKind::ALL.len());
}
#[test]
fn the_chunk_budget_is_exactly_a_bucket_boundary() {
let budget = crate::CHUNK_BUDGET.as_micros() as u64;
assert!(
BUCKET_BOUNDS_MICROS.contains(&budget),
"CHUNK_BUDGET is {budget} µs, which is not a bucket bound: \
{BUCKET_BOUNDS_MICROS:?}"
);
assert_eq!(bucket_of(budget), bucket_of(budget - 1));
assert_eq!(bucket_of(budget + 1), bucket_of(budget) + 1);
}
#[test]
fn the_overflow_bucket_catches_everything_past_the_last_bound() {
let last = *BUCKET_BOUNDS_MICROS.last().unwrap();
assert_eq!(bucket_of(last), BUCKET_BOUNDS_MICROS.len() - 1);
assert_eq!(bucket_of(last + 1), BUCKET_COUNT - 1);
assert_eq!(bucket_of(u64::MAX), BUCKET_COUNT - 1);
}
#[test]
fn the_packing_leaves_room_for_both_fields() {
assert!(
(CommandKind::COUNT as u64) <= 0xFF,
"the kind index must fit in the low 8 bits"
);
assert_eq!(MICROS_CEILING.checked_shl(8), Some(MICROS_CEILING << 8));
assert_eq!((MICROS_CEILING << 8) >> 8, MICROS_CEILING);
}
#[cfg(feature = "metrics")]
#[test]
fn the_longest_hold_names_the_command_that_caused_it() {
let m = ActorMetrics::new();
m.record_hold(CommandKind::AssertEdge, Duration::from_micros(500));
m.record_hold(CommandKind::Archive, Duration::from_millis(40));
m.record_hold(CommandKind::UpsertConcept, Duration::from_micros(900));
let snap = m.snapshot();
assert_eq!(
snap.longest,
Some((CommandKind::Archive, Duration::from_millis(40)))
);
}
#[cfg(feature = "metrics")]
#[test]
fn a_later_declared_kind_does_not_outrank_a_longer_hold() {
let long = CommandKind::AssertEdge; let short = CommandKind::RebuildFts; assert!(short.index() > long.index(), "the fixture needs the gap");
let m = ActorMetrics::new();
m.record_hold(long, Duration::from_millis(40));
m.record_hold(short, Duration::from_micros(1));
assert_eq!(
m.snapshot().longest,
Some((long, Duration::from_millis(40))),
"the max is being taken over the kind index, not the duration"
);
}
#[cfg(feature = "metrics")]
#[test]
fn an_exempt_kind_over_budget_is_not_a_violation() {
let m = ActorMetrics::new();
m.record_hold(CommandKind::Archive, Duration::from_millis(40));
m.record_hold(CommandKind::AssertEdge, Duration::from_millis(40));
let snap = m.snapshot();
let violations = snap.budget_violations();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].kind, CommandKind::AssertEdge);
assert_eq!(violations[0].over_budget, 1);
let archive = snap
.kinds
.iter()
.find(|k| k.kind == CommandKind::Archive)
.unwrap();
assert_eq!(archive.turns, 1);
assert_eq!(archive.mean, Duration::from_millis(40));
}
#[cfg(feature = "metrics")]
#[test]
fn queue_depth_is_a_mean_and_a_high_water_mark() {
let m = ActorMetrics::new();
m.record_turn(0, 4);
m.record_turn(10, 0);
let snap = m.snapshot();
assert_eq!(snap.turns, 0);
assert_eq!(snap.depth_samples, 2);
assert_eq!(snap.high_depth_mean, 5.0);
assert_eq!(snap.high_depth_max, 10);
assert_eq!(snap.low_depth_mean, 2.0);
assert_eq!(snap.low_depth_max, 4);
}
}