use std::sync::{Mutex, OnceLock};
use std::time::Duration;
const FLUSH_WINDOW: Duration = Duration::from_secs(3);
#[derive(Debug, Clone)]
struct Record {
label: String,
elapsed_ms: u64,
}
#[derive(Default)]
struct State {
records: Vec<Record>,
timer_armed: bool,
generation: u64,
threshold_ms: u64,
}
fn state() -> &'static Mutex<State> {
static STATE: OnceLock<Mutex<State>> = OnceLock::new();
STATE.get_or_init(|| Mutex::new(State::default()))
}
pub fn record(label: &str, elapsed_ms: u64, threshold_ms: u64) {
let needs_arm = {
let Ok(mut g) = state().lock() else {
return;
};
g.records.push(Record {
label: label.to_string(),
elapsed_ms,
});
g.threshold_ms = threshold_ms;
!g.timer_armed
};
if needs_arm {
try_arm_flush_timer();
}
}
fn try_arm_flush_timer() {
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return;
};
let captured_generation = {
let Ok(mut g) = state().lock() else {
return;
};
g.timer_armed = true;
g.generation
};
handle.spawn(async move {
tokio::time::sleep(FLUSH_WINDOW).await;
drain_window_if_current(captured_generation);
});
}
fn drain_window_if_current(captured_generation: u64) {
let (records, threshold_ms) = {
let Ok(mut g) = state().lock() else {
return;
};
if g.generation != captured_generation {
return;
}
let records = std::mem::take(&mut g.records);
g.timer_armed = false;
g.generation = g.generation.wrapping_add(1);
(records, g.threshold_ms)
};
emit(records, threshold_ms);
}
fn emit(records: Vec<Record>, threshold_ms: u64) {
let count = records.len();
if count == 0 {
return;
}
let slowest = records
.iter()
.max_by_key(|r| r.elapsed_ms)
.expect("count > 0 implies a slowest record");
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_SLOW_METADATA,
"registry slow: {count} metadata fetches over {threshold_ms}ms (slowest: {})",
slowest.label,
);
}
pub fn flush_summary() {
let (records, threshold_ms) = {
let Ok(mut g) = state().lock() else {
return;
};
let records = std::mem::take(&mut g.records);
g.timer_armed = false;
g.generation = g.generation.wrapping_add(1);
(records, g.threshold_ms)
};
emit(records, threshold_ms);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn debounced_grouping_lifecycle() {
{
let mut g = state().lock().unwrap();
g.records.clear();
g.timer_armed = false;
g.generation = 0;
g.threshold_ms = 0;
}
flush_summary();
assert!(state().lock().unwrap().records.is_empty());
record("packument a", 11_000, 10_000);
record("packument b", 13_500, 10_000);
{
let g = state().lock().unwrap();
assert_eq!(g.records.len(), 2, "both events buffered into the group");
assert!(g.timer_armed, "first event armed the flush timer");
}
tokio::time::sleep(FLUSH_WINDOW + Duration::from_millis(50)).await;
for _ in 0..4 {
tokio::task::yield_now().await;
}
{
let g = state().lock().unwrap();
assert!(
g.records.is_empty(),
"timer must drain the window after FLUSH_WINDOW",
);
assert!(!g.timer_armed, "timer must clear the armed flag on drain");
}
record("packument c", 14_000, 10_000);
assert!(state().lock().unwrap().timer_armed);
let generation_before_flush = state().lock().unwrap().generation;
flush_summary();
{
let g = state().lock().unwrap();
assert!(g.records.is_empty(), "flush_summary drains the tail");
assert!(!g.timer_armed, "flush_summary clears the armed flag");
assert_ne!(
g.generation, generation_before_flush,
"flush_summary must bump the generation to invalidate the pending timer",
);
}
record("packument d", 15_000, 10_000);
let stolen_window_records = state().lock().unwrap().records.len();
tokio::time::sleep(FLUSH_WINDOW + Duration::from_millis(50)).await;
for _ in 0..4 {
tokio::task::yield_now().await;
}
assert_eq!(
stolen_window_records, 1,
"fresh window started with one record",
);
assert!(
state().lock().unwrap().records.is_empty(),
"new window's timer must drain its own records",
);
}
}