use jdwp_client::{EventSet, JdwpConnection};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
pub type SessionId = String;
#[derive(Debug)]
pub struct DebugSession {
pub connection: JdwpConnection,
pub endpoint: String,
pub breakpoints: HashMap<String, BreakpointInfo>,
pub events: VecDeque<EventRecord>,
pub event_seq: u64,
pub events_dropped: u64,
pub event_listener_task: Option<JoinHandle<()>>,
pub last_thread: Option<u64>,
pub pending_step: Option<(i32, u64)>,
pub suspended_since: Option<std::time::Instant>,
pub suspended_cause: Option<SuspendCause>,
pub thread_suspends: std::collections::BTreeMap<u64, ThreadSuspend>,
pub watchdog_task: Option<JoinHandle<()>>,
pub last_watchdog_note: Option<String>,
pub last_watchdog_seq: Option<u64>,
pub disarmed_traced_requests: VecDeque<i32>,
pub rethrow_chains: HashMap<(i32, u64, u64), RethrowChain>,
pub trace_disarms: std::collections::BTreeMap<String, u32>,
pub trace_disarms_dropped: u64,
pub read_only: bool,
pub source_roots: Vec<std::path::PathBuf>,
pub class_roots: Vec<std::path::PathBuf>,
pub trace_exprs: Vec<String>,
pub redefinitions: std::collections::BTreeMap<String, Redefinition>,
pub pending_breakpoints: Vec<PendingBreakpoint>,
pub pattern_sets: HashMap<String, PatternStopSet>,
pub launched: Option<LaunchedJvm>,
pub exception_requests: HashMap<String, ExceptionRequestInfo>,
pub watchpoints: HashMap<String, WatchpointInfo>,
pub method_exits: HashMap<String, MethodExitRequestInfo>,
pub monitor_requests: HashMap<String, MonitorRequestInfo>,
pub monitor_pending: HashMap<MonitorPairKey, std::time::Instant>,
pub monitor_pending_dropped: u64,
pub traces: VecDeque<TraceRecord>,
pub trace_seq: u64,
pub stop_seq: u64,
pub alerter: crate::protocol::Alerter,
}
pub const MAX_TRACES: usize = 500;
pub const MAX_TRACE_DISARMS: usize = 32;
pub const MAX_MONITOR_PENDING: usize = 256;
pub const MAX_EVENTS: usize = 100;
#[derive(Debug, Clone)]
pub struct EventRecord {
pub seq: u64,
pub set: EventSet,
pub escalation: Option<FailedEscalation>,
}
#[derive(Debug, Clone)]
pub struct FailedEscalation {
pub vm_running: bool,
pub note: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuspendCause {
StopPoint(i32),
ManualPause,
}
#[derive(Debug, Clone)]
pub struct ThreadSuspend {
pub name: String,
pub since: std::time::Instant,
pub issued: u32,
}
#[derive(Debug, Clone)]
pub struct Redefinition {
pub count: u32,
pub at: std::time::Instant,
pub popped_since: bool,
}
impl DebugSession {
pub fn note_redefinition(&mut self, class_name: &str) {
let entry = self.redefinitions.entry(class_name.to_string()).or_insert_with(|| Redefinition {
count: 0,
at: std::time::Instant::now(),
popped_since: false,
});
entry.count += 1;
entry.at = std::time::Instant::now();
entry.popped_since = false;
}
pub fn note_pop(&mut self, class_name: &str) {
if let Some(entry) = self.redefinitions.get_mut(class_name) {
entry.popped_since = true;
}
}
pub fn push_event(&mut self, set: EventSet, escalation: Option<FailedEscalation>) -> u64 {
self.event_seq += 1;
if self.events.len() >= MAX_EVENTS {
self.events.pop_front();
self.events_dropped += 1;
}
self.events.push_back(EventRecord { seq: self.event_seq, set, escalation });
self.event_seq
}
pub fn mark_suspended(&mut self, cause: SuspendCause) {
self.suspended_since = Some(std::time::Instant::now());
self.suspended_cause = Some(cause);
}
pub const fn mark_resumed(&mut self) {
self.suspended_since = None;
self.suspended_cause = None;
}
pub fn note_watchdog(&mut self, note: String) {
self.last_watchdog_note = Some(note);
self.last_watchdog_seq = Some(self.event_seq);
}
pub fn note_disarmed_traced(&mut self, req_id: i32) {
remember_bounded(&mut self.disarmed_traced_requests, req_id, MAX_DISARMED_TRACED);
}
pub fn was_traced_and_disarmed(&self, req_id: i32) -> bool {
self.disarmed_traced_requests.contains(&req_id) && !self.owns_live_request(req_id)
}
fn owns_live_request(&self, req_id: i32) -> bool {
let id = Some(req_id);
self.breakpoints.values().any(|b| b.owns_request(req_id))
|| self.exception_requests.values().any(|e| e.request_id == id)
|| self.watchpoints.values().any(|w| w.request_id == id)
|| self.method_exits.values().any(|m| m.request_id == id)
|| self.pending_breakpoints.iter().any(|p| p.class_prepare_request_id == req_id)
|| self.breakpoints.values().any(|b| b.rearm.watch().is_some_and(|w| w.request_id == req_id))
}
pub fn classify_throw(
&mut self,
req_id: i32,
thread: u64,
exception: Option<u64>,
next_seq: u64,
) -> ThrowKind {
let Some(exc) = exception else {
return ThrowKind::First;
};
let key = (req_id, thread, exc);
if let Some(chain) = self.rethrow_chains.get_mut(&key) {
chain.collapsed = chain.collapsed.saturating_add(1);
let supersedes = chain.rolling_seq.replace(next_seq);
return ThrowKind::Rethrow {
fold: RethrowFold { collapsed: chain.collapsed - 1, first_seq: chain.first_seq },
supersedes,
};
}
if self.rethrow_chains.len() >= MAX_RETHROW_CHAINS {
if let Some(oldest) = self.rethrow_chains.iter().min_by_key(|(_, c)| c.first_seq).map(|(k, _)| *k)
{
self.rethrow_chains.remove(&oldest);
}
}
self.rethrow_chains
.insert(key, RethrowChain { first_seq: next_seq, rolling_seq: None, collapsed: 0 });
ThrowKind::First
}
pub fn open_monitor_pair(&mut self, key: MonitorPairKey, at: std::time::Instant) {
if self.monitor_pending.len() >= MAX_MONITOR_PENDING && !self.monitor_pending.contains_key(&key) {
if let Some(oldest) = self.monitor_pending.iter().min_by_key(|(_, t)| **t).map(|(k, _)| *k) {
self.monitor_pending.remove(&oldest);
self.monitor_pending_dropped = self.monitor_pending_dropped.saturating_add(1);
}
}
self.monitor_pending.insert(key, at);
}
pub fn close_monitor_pair(
&mut self,
key: &MonitorPairKey,
now: std::time::Instant,
) -> Option<std::time::Duration> {
let opened = self.monitor_pending.remove(key)?;
Some(now.saturating_duration_since(opened))
}
pub fn watchdog_note_for(&self, newest_seq: Option<u64>) -> Option<&str> {
let (note, at) = (self.last_watchdog_note.as_deref()?, self.last_watchdog_seq?);
(newest_seq? <= at).then_some(note)
}
pub fn note_trace_disarm(&mut self, note: String) {
if let Some(n) = self.trace_disarms.get_mut(¬e) {
*n += 1;
} else if self.trace_disarms.len() < MAX_TRACE_DISARMS {
self.trace_disarms.insert(note, 1);
} else {
self.trace_disarms_dropped += 1;
}
}
pub fn next_stop_id(&mut self, prefix: &str) -> String {
self.stop_seq += 1;
format!("{prefix}{}", self.stop_seq)
}
}
#[derive(Debug, Clone, Default)]
pub struct TraceCost {
pub captures: u64,
pub total: std::time::Duration,
first: Option<std::time::Instant>,
last: Option<std::time::Instant>,
}
impl TraceCost {
pub fn record(&mut self, started: std::time::Instant, took: std::time::Duration) {
self.captures += 1;
self.total = self.total.saturating_add(took);
self.first.get_or_insert(started);
self.last = Some(started);
}
pub fn mean_capture(&self) -> Option<std::time::Duration> {
(self.captures > 0).then(|| self.total / u32::try_from(self.captures).unwrap_or(u32::MAX))
}
#[allow(clippy::cast_precision_loss)] pub fn observed_rate(&self) -> Option<f64> {
let (first, last) = (self.first?, self.last?);
let window = last.duration_since(first).as_secs_f64();
(self.captures >= 2 && window > 0.0).then(|| (self.captures - 1) as f64 / window)
}
pub fn capture_share(&self) -> Option<f64> {
Some(self.observed_rate()? * self.mean_capture()?.as_secs_f64())
}
}
#[derive(Debug, Clone)]
pub struct TracedValue {
pub name: String,
pub rendered: String,
pub object_id: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct TraceRecord {
pub seq: u64,
pub bp_id: String,
pub thread: u64,
pub class: String,
pub method: String,
pub line: Option<i32>,
pub args: Vec<TracedValue>,
pub captured: Vec<TracedValue>,
pub callers: Vec<String>,
pub expr: Vec<(String, String)>,
pub detail: Vec<(String, String)>,
pub rethrow: Option<RethrowFold>,
}
#[derive(Debug, Clone, Copy)]
pub struct RethrowFold {
pub collapsed: u32,
pub first_seq: u64,
}
#[derive(Debug, Clone, Copy)]
pub enum ThrowKind {
First,
Rethrow { fold: RethrowFold, supersedes: Option<u64> },
}
#[derive(Debug, Clone, Copy)]
pub struct RethrowChain {
pub first_seq: u64,
pub rolling_seq: Option<u64>,
pub collapsed: u32,
}
fn remember_bounded(queue: &mut std::collections::VecDeque<i32>, req_id: i32, cap: usize) {
if queue.contains(&req_id) {
return;
}
if cap == 0 {
return;
}
if queue.len() >= cap {
queue.pop_front();
}
queue.push_back(req_id);
}
pub const MAX_DISARMED_TRACED: usize = 32;
pub const MAX_RETHROW_CHAINS: usize = 64;
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct ExceptionRequestInfo {
pub id: String,
pub request_id: Option<i32>,
pub enabled: bool,
pub spent: bool,
pub hit_count: Option<i32>,
pub hits: u32,
pub ref_type: Option<u64>,
pub class_pattern: String,
pub caught: bool,
pub uncaught: bool,
pub condition: Option<String>,
pub trace: bool,
pub trace_expr: Vec<String>,
pub trace_budget: Option<u32>,
pub trace_frames: usize,
pub trace_max_length: Option<usize>,
pub trace_cost: TraceCost,
pub thread_filter: Option<u64>,
pub instance_filter: Option<u64>,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct WatchpointInfo {
pub request_id: Option<i32>,
pub enabled: bool,
pub spent: bool,
pub hit_count: Option<i32>,
pub hits: u32,
pub arm: (u64, u64),
pub kind: jdwp_client::WatchKind,
pub class_name: String,
pub field_name: String,
pub is_static: bool,
pub condition: Option<String>,
pub trace: bool,
pub trace_expr: Vec<String>,
pub trace_budget: Option<u32>,
pub trace_frames: usize,
pub trace_max_length: Option<usize>,
pub trace_cost: TraceCost,
pub thread_filter: Option<u64>,
pub instance_filter: Option<u64>,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct MethodExitRequestInfo {
pub id: String,
pub request_id: Option<i32>,
pub enabled: bool,
pub spent: bool,
pub hit_count: Option<i32>,
pub hits: u32,
pub discarded: u32,
pub class_pattern: String,
pub exclude_classes: Vec<String>,
pub method: Option<String>,
pub with_return_value: bool,
pub condition: Option<String>,
pub trace: bool,
pub trace_expr: Vec<String>,
pub trace_budget: Option<u32>,
pub trace_frames: usize,
pub trace_max_length: Option<usize>,
pub trace_cost: TraceCost,
pub thread_filter: Option<u64>,
pub instance_filter: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MonitorPair {
Contended,
Wait,
}
impl MonitorPair {
#[must_use]
pub const fn of(kind: jdwp_client::MonitorKind) -> (Self, bool) {
match kind {
jdwp_client::MonitorKind::Blocked => (Self::Contended, true),
jdwp_client::MonitorKind::Acquired => (Self::Contended, false),
jdwp_client::MonitorKind::Wait => (Self::Wait, true),
jdwp_client::MonitorKind::Waited => (Self::Wait, false),
}
}
#[must_use]
pub const fn duration_label(self) -> &'static str {
match self {
Self::Contended => "blocked_for",
Self::Wait => "waited_for",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MonitorPairKey {
pub thread: u64,
pub monitor: u64,
pub pair: MonitorPair,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct MonitorRequestInfo {
pub id: String,
pub request_id: Option<i32>,
pub enabled: bool,
pub spent: bool,
pub hit_count: Option<i32>,
pub hits: u32,
pub kind: jdwp_client::MonitorKind,
pub paired: bool,
pub monitor_class: Option<String>,
pub min_duration_ms: Option<u64>,
pub trace: bool,
pub trace_expr: Vec<String>,
pub trace_budget: Option<u32>,
pub trace_frames: usize,
pub trace_max_length: Option<usize>,
pub trace_cost: TraceCost,
pub thread_filter: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct PendingBreakpoint {
pub bp_id: String,
pub class_prepare_request_id: i32,
pub class_pattern: String,
pub signature: String,
pub line: Option<i32>,
pub method: Option<String>,
pub hit_count: Option<i32>,
pub instance_filter: Option<u64>,
pub thread_filter: Option<u64>,
pub condition: Option<String>,
pub trace: bool,
pub trace_expr: Vec<String>,
pub trace_budget: Option<u32>,
pub trace_frames: usize,
pub trace_max_length: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct PatternStopSet {
pub id: String,
pub class_pattern: String,
pub watch: ClassLoadWatch,
pub enabled: bool,
pub members: Vec<String>,
pub armed_later: Vec<String>,
pub armed_later_total: usize,
pub method: Option<String>,
pub hit_count: Option<i32>,
pub instance_filter: Option<u64>,
pub thread_filter: Option<u64>,
pub condition: Option<String>,
pub trace: bool,
pub trace_expr: Vec<String>,
pub trace_budget: Option<u32>,
pub trace_frames: usize,
pub trace_max_length: Option<usize>,
pub max_classes: usize,
pub skipped_at_cap: usize,
pub no_method: usize,
}
pub const MAX_DEBUGGEE_OUTPUT: usize = 200;
#[derive(Debug)]
pub struct LaunchedJvm {
pub pid: Option<u32>,
pub command: String,
pub child: tokio::process::Child,
pub output: std::sync::Arc<std::sync::Mutex<VecDeque<String>>>,
pub detach_on_disconnect: bool,
}
impl LaunchedJvm {
pub fn tail(&self, n: usize) -> Vec<String> {
let Ok(buf) = self.output.lock() else {
return Vec::new();
};
buf.iter().skip(buf.len().saturating_sub(n)).cloned().collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClassLoadWatch {
Watching(i32),
Parked,
Disabled,
Failed,
}
impl ClassLoadWatch {
pub const fn request_id(&self) -> Option<i32> {
match self {
Self::Watching(req) => Some(*req),
Self::Parked | Self::Disabled | Self::Failed => None,
}
}
pub const fn is_watching(&self) -> bool {
matches!(self, Self::Watching(_))
}
}
const MAX_ARMED_LATER_SAMPLE: usize = 25;
impl PatternStopSet {
pub fn has_room(&self) -> bool {
self.members.len() < self.max_classes
}
pub fn note_armed_later(&mut self, class: &str) {
self.armed_later_total += 1;
if self.armed_later.len() < MAX_ARMED_LATER_SAMPLE {
self.armed_later.push(class.to_string());
}
}
}
#[derive(Debug, Clone)]
pub struct BreakpointInfo {
pub request_ids: Vec<i32>,
pub class_pattern: String,
pub line: u32,
pub method: Option<String>,
pub arm_line: Option<i32>,
pub arm_method: Option<String>,
pub enabled: bool,
pub spent: bool,
pub hits: u32,
pub condition: Option<String>,
pub trace: bool,
pub trace_expr: Vec<String>,
pub trace_budget: Option<u32>,
pub trace_frames: usize,
pub trace_max_length: Option<usize>,
pub trace_cost: TraceCost,
pub drift: crate::handlers::DriftCheck,
pub loaders: Vec<String>,
pub arm: BreakpointArm,
pub rearm: RearmState,
}
#[derive(Debug, Clone)]
pub enum RearmState {
Watching(ReArmWatch),
CoveredByFamily,
Unwatched,
}
impl RearmState {
pub const fn watch(&self) -> Option<&ReArmWatch> {
match self {
Self::Watching(w) => Some(w),
_ => None,
}
}
pub const fn watch_mut(&mut self) -> Option<&mut ReArmWatch> {
match self {
Self::Watching(w) => Some(w),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ReArmWatch {
pub request_id: i32,
pub signature: String,
pub later_copies: usize,
pub line: Option<i32>,
pub method: Option<String>,
}
impl BreakpointInfo {
pub fn owns_request(&self, req: i32) -> bool {
self.request_ids.contains(&req)
}
pub fn is_armed(&self) -> bool {
!self.request_ids.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct BreakpointArm {
pub class_id: u64,
pub method_id: u64,
pub bytecode_index: u64,
pub extra_locations: Vec<ArmedLocation>,
pub suspend_policy: jdwp_client::SuspendPolicy,
pub hit_count: Option<i32>,
pub thread_filter: Option<u64>,
pub instance_filter: Option<u64>,
}
#[derive(Debug, Clone, Copy)]
pub struct ArmedLocation {
pub class_id: u64,
pub method_id: u64,
pub bytecode_index: u64,
}
#[derive(Clone)]
pub struct SessionManager {
sessions: Arc<Mutex<HashMap<SessionId, Arc<Mutex<DebugSession>>>>>,
current_session: Arc<Mutex<Option<SessionId>>>,
alerter: crate::protocol::Alerter,
}
impl SessionManager {
pub fn new(alerter: crate::protocol::Alerter) -> Self {
Self {
sessions: Arc::new(Mutex::new(HashMap::new())),
current_session: Arc::new(Mutex::new(None)),
alerter,
}
}
pub async fn create_session(
&self,
connection: JdwpConnection,
endpoint: String,
read_only: bool,
source_roots: Vec<std::path::PathBuf>,
class_roots: Vec<std::path::PathBuf>,
trace_exprs: Vec<String>,
) -> SessionId {
let session_id = format!("session_{}", uuid::v4());
let session = DebugSession {
connection,
endpoint,
breakpoints: HashMap::new(),
events: VecDeque::new(),
event_seq: 0,
events_dropped: 0,
event_listener_task: None,
last_thread: None,
pending_step: None,
suspended_since: None,
suspended_cause: None,
thread_suspends: std::collections::BTreeMap::new(),
watchdog_task: None,
last_watchdog_note: None,
last_watchdog_seq: None,
disarmed_traced_requests: VecDeque::new(),
rethrow_chains: HashMap::new(),
trace_disarms: std::collections::BTreeMap::new(),
trace_disarms_dropped: 0,
read_only,
source_roots,
class_roots,
trace_exprs,
redefinitions: std::collections::BTreeMap::new(),
pending_breakpoints: Vec::new(),
pattern_sets: HashMap::new(),
launched: None,
exception_requests: HashMap::new(),
watchpoints: HashMap::new(),
method_exits: HashMap::new(),
monitor_requests: HashMap::new(),
monitor_pending: HashMap::new(),
monitor_pending_dropped: 0,
traces: VecDeque::new(),
trace_seq: 0,
stop_seq: 0,
alerter: self.alerter.clone(),
};
let mut sessions = self.sessions.lock().await;
sessions.insert(session_id.clone(), Arc::new(Mutex::new(session)));
drop(sessions);
let mut current = self.current_session.lock().await;
*current = Some(session_id.clone());
session_id
}
pub async fn get_current_session(&self) -> Option<Arc<Mutex<DebugSession>>> {
let current = self.current_session.lock().await;
if let Some(session_id) = current.as_ref() {
let sessions = self.sessions.lock().await;
sessions.get(session_id).cloned()
} else {
None
}
}
pub async fn get_session_by_id(&self, session_id: &str) -> Option<Arc<Mutex<DebugSession>>> {
let sessions = self.sessions.lock().await;
sessions.get(session_id).cloned()
}
pub async fn get_current_session_id(&self) -> Option<SessionId> {
let current = self.current_session.lock().await;
current.clone()
}
pub async fn list(&self) -> (Vec<(SessionId, Arc<Mutex<DebugSession>>)>, Option<SessionId>) {
let sessions = self.sessions.lock().await;
let mut rows: Vec<(SessionId, Arc<Mutex<DebugSession>>)> =
sessions.iter().map(|(k, v)| (k.clone(), Arc::clone(v))).collect();
drop(sessions); rows.sort_by(|a, b| a.0.cmp(&b.0));
(rows, self.get_current_session_id().await)
}
pub async fn remove_session(&self, session_id: &str) {
let mut sessions = self.sessions.lock().await;
if let Some(session_arc) = sessions.get(session_id) {
let mut session = session_arc.lock().await;
if let Some(task) = session.event_listener_task.take() {
task.abort();
}
if let Some(task) = session.watchdog_task.take() {
task.abort();
}
}
sessions.remove(session_id);
drop(sessions);
let mut current = self.current_session.lock().await;
if current.as_ref() == Some(&session_id.to_string()) {
*current = None;
}
}
}
mod uuid {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
pub fn v4() -> String {
let counter = COUNTER.fetch_add(1, Ordering::SeqCst);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
format!("{timestamp:x}{counter:x}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stop_ids_are_sequential_and_prefixed() {
let mut seq = 0u64;
let mut next = |prefix: &str| {
seq += 1;
format!("{prefix}{seq}")
};
assert_eq!(next("bp_"), "bp_1");
assert_eq!(next("exc_"), "exc_2");
assert_eq!(next("watch_modify_"), "watch_modify_3");
assert_eq!(next("bp_"), "bp_4", "ids must never be reused within a session");
}
fn note_into(notes: &mut std::collections::BTreeMap<String, u32>, dropped: &mut u64, n: &str) {
if let Some(c) = notes.get_mut(n) {
*c += 1;
} else if notes.len() < MAX_TRACE_DISARMS {
notes.insert(n.to_string(), 1);
} else {
*dropped += 1;
}
}
#[test]
fn trace_disarm_notes_collapse_repeats_and_stay_bounded() {
let mut notes: std::collections::BTreeMap<String, u32> = std::collections::BTreeMap::new();
let mut dropped = 0u64;
for _ in 0..500 {
note_into(&mut notes, &mut dropped, "watch_3 stopped recording");
}
assert_eq!(notes.len(), 1, "repeats must collapse, not accumulate");
assert_eq!(notes["watch_3 stopped recording"], 500, "the count is what carries the repetition");
assert_eq!(dropped, 0);
for i in 0..MAX_TRACE_DISARMS + 10 {
note_into(&mut notes, &mut dropped, &format!("bp_{i} stopped recording"));
}
assert_eq!(notes.len(), MAX_TRACE_DISARMS, "distinct notes must be capped");
assert!(dropped > 0, "overflow must be counted so a full buffer never reads as a quiet one");
}
#[test]
fn disarmed_traced_ids_are_deduplicated_and_bounded() {
let mut q = std::collections::VecDeque::new();
for _ in 0..500 {
remember_bounded(&mut q, 7, MAX_DISARMED_TRACED);
}
assert_eq!(q.len(), 1, "repeats must not accumulate");
for i in 0..i32::try_from(MAX_DISARMED_TRACED).unwrap_or(i32::MAX) + 5 {
remember_bounded(&mut q, 1000 + i, MAX_DISARMED_TRACED);
}
assert_eq!(q.len(), MAX_DISARMED_TRACED, "the list must stay bounded");
assert!(!q.contains(&7), "the oldest entry must be the one evicted");
let newest = 1000 + i32::try_from(MAX_DISARMED_TRACED).unwrap_or(i32::MAX) + 4;
assert!(q.contains(&newest), "the newest disarm is the one most likely to have a hit in flight");
let mut zero = std::collections::VecDeque::new();
remember_bounded(&mut zero, 1, 0);
assert!(zero.is_empty(), "cap 0 must store nothing rather than push after not evicting");
}
#[test]
fn an_untouched_trace_cost_reports_no_figures_at_all() {
let cost = TraceCost::default();
assert_eq!(cost.captures, 0);
assert!(cost.mean_capture().is_none(), "no captures means no mean, not a zero mean");
assert!(cost.observed_rate().is_none());
assert!(cost.capture_share().is_none());
}
#[test]
fn one_capture_gives_a_cost_but_no_arrival_rate() {
let mut cost = TraceCost::default();
let t0 = std::time::Instant::now();
cost.record(t0, std::time::Duration::from_micros(800));
assert_eq!(cost.mean_capture(), Some(std::time::Duration::from_micros(800)));
assert!(cost.observed_rate().is_none(), "one capture spans no interval");
assert!(cost.capture_share().is_none());
}
#[test]
fn arrival_rate_and_capture_share_are_measured_over_the_observed_window() {
let mut cost = TraceCost::default();
let t0 = std::time::Instant::now();
for i in 0..10u32 {
cost.record(
t0 + std::time::Duration::from_millis(u64::from(i) * 100),
std::time::Duration::from_millis(1),
);
}
assert_eq!(cost.captures, 10);
assert_eq!(cost.mean_capture(), Some(std::time::Duration::from_millis(1)));
let rate = cost.observed_rate().expect("ten captures span nine intervals");
assert!((rate - 10.0).abs() < 0.01, "expected 10/s, got {rate}");
let share = cost.capture_share().expect("both figures are present");
assert!((share - 0.01).abs() < 0.0005, "expected ~1% of the window, got {share}");
}
#[test]
fn suspend_cause_distinguishes_a_stop_point_from_a_manual_pause() {
assert_ne!(SuspendCause::ManualPause, SuspendCause::StopPoint(7));
assert_eq!(SuspendCause::StopPoint(7), SuspendCause::StopPoint(7));
assert_ne!(SuspendCause::StopPoint(7), SuspendCause::StopPoint(8));
}
}