use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::Instant;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum IndexPhase {
Idle,
Scanning,
Indexing,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum IndexStateKind {
Building,
Ready,
Degraded,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct IndexStateTransition {
pub from: IndexStateKind,
pub to: IndexStateKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct IndexPhaseTransition {
pub from: IndexPhase,
pub to: IndexPhase,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EarlyExitReason {
InitialTimeBudget,
IncrementalTimeBudget,
FileLimit,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EarlyExitRecord {
pub reason: EarlyExitReason,
pub elapsed_ms: u64,
pub indexed_files: usize,
pub total_files: usize,
}
#[derive(Clone, Debug)]
pub struct IndexInstrumentationSnapshot {
pub state_durations_ms: HashMap<IndexStateKind, u64>,
pub phase_durations_ms: HashMap<IndexPhase, u64>,
pub state_transition_counts: HashMap<IndexStateTransition, u64>,
pub phase_transition_counts: HashMap<IndexPhaseTransition, u64>,
pub early_exit_counts: HashMap<EarlyExitReason, u64>,
pub last_early_exit: Option<EarlyExitRecord>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ResourceKind {
MaxFiles,
MaxSymbols,
MaxCacheBytes,
}
#[derive(Clone, Debug)]
pub enum DegradationReason {
ParseStorm {
pending_parses: usize,
},
IoError {
message: String,
},
ScanTimeout {
elapsed_ms: u64,
},
ResourceLimit {
kind: ResourceKind,
},
}
#[derive(Clone, Debug)]
pub struct IndexResourceLimits {
pub max_files: usize,
pub max_symbols_per_file: usize,
pub max_total_symbols: usize,
pub max_ast_cache_bytes: usize,
pub max_ast_cache_items: usize,
pub max_scan_duration_ms: u64,
}
impl Default for IndexResourceLimits {
fn default() -> Self {
Self {
max_files: 10_000,
max_symbols_per_file: 5_000,
max_total_symbols: 500_000,
max_ast_cache_bytes: 256 * 1024 * 1024,
max_ast_cache_items: 100,
max_scan_duration_ms: 30_000,
}
}
}
#[derive(Clone, Debug)]
pub struct IndexPerformanceCaps {
pub initial_scan_budget_ms: u64,
pub incremental_budget_ms: u64,
}
impl Default for IndexPerformanceCaps {
fn default() -> Self {
Self { initial_scan_budget_ms: 500, incremental_budget_ms: 10 }
}
}
pub struct IndexMetrics {
pending_parses: AtomicUsize,
parse_storm_threshold: usize,
#[allow(dead_code)]
last_indexed: AtomicU64,
}
impl IndexMetrics {
pub fn new() -> Self {
Self {
pending_parses: AtomicUsize::new(0),
parse_storm_threshold: 10,
last_indexed: AtomicU64::new(0),
}
}
pub fn with_threshold(threshold: usize) -> Self {
Self {
pending_parses: AtomicUsize::new(0),
parse_storm_threshold: threshold,
last_indexed: AtomicU64::new(0),
}
}
pub fn pending_count(&self) -> usize {
self.pending_parses.load(Ordering::SeqCst)
}
pub fn increment_pending_parses(&self) -> usize {
self.pending_parses.fetch_add(1, Ordering::SeqCst) + 1
}
pub fn decrement_pending_parses(&self) -> usize {
self.pending_parses.fetch_sub(1, Ordering::SeqCst) - 1
}
pub fn is_parse_storm(&self) -> bool {
self.pending_count() > self.parse_storm_threshold
}
pub fn parse_storm_threshold(&self) -> usize {
self.parse_storm_threshold
}
}
impl Default for IndexMetrics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
struct IndexInstrumentationState {
current_state: IndexStateKind,
current_phase: IndexPhase,
state_started_at: Instant,
phase_started_at: Instant,
state_durations_ms: HashMap<IndexStateKind, u64>,
phase_durations_ms: HashMap<IndexPhase, u64>,
state_transition_counts: HashMap<IndexStateTransition, u64>,
phase_transition_counts: HashMap<IndexPhaseTransition, u64>,
early_exit_counts: HashMap<EarlyExitReason, u64>,
last_early_exit: Option<EarlyExitRecord>,
}
impl IndexInstrumentationState {
fn new() -> Self {
let now = Instant::now();
Self {
current_state: IndexStateKind::Building,
current_phase: IndexPhase::Idle,
state_started_at: now,
phase_started_at: now,
state_durations_ms: HashMap::new(),
phase_durations_ms: HashMap::new(),
state_transition_counts: HashMap::new(),
phase_transition_counts: HashMap::new(),
early_exit_counts: HashMap::new(),
last_early_exit: None,
}
}
}
#[derive(Debug)]
pub struct IndexInstrumentation {
inner: Mutex<IndexInstrumentationState>,
}
impl IndexInstrumentation {
pub fn new() -> Self {
Self { inner: Mutex::new(IndexInstrumentationState::new()) }
}
pub fn record_state_transition(&self, from: IndexStateKind, to: IndexStateKind) {
let now = Instant::now();
let mut inner = self.inner.lock();
let elapsed_ms = now.duration_since(inner.state_started_at).as_millis() as u64;
*inner.state_durations_ms.entry(from).or_default() += elapsed_ms;
let transition = IndexStateTransition { from, to };
*inner.state_transition_counts.entry(transition).or_default() += 1;
if from == IndexStateKind::Building {
let phase_elapsed = now.duration_since(inner.phase_started_at).as_millis() as u64;
let current_phase = inner.current_phase;
*inner.phase_durations_ms.entry(current_phase).or_default() += phase_elapsed;
}
inner.current_state = to;
inner.state_started_at = now;
if to == IndexStateKind::Building || from == IndexStateKind::Building {
inner.current_phase = IndexPhase::Idle;
inner.phase_started_at = now;
}
}
pub fn record_phase_transition(&self, from: IndexPhase, to: IndexPhase) {
let now = Instant::now();
let mut inner = self.inner.lock();
let elapsed_ms = now.duration_since(inner.phase_started_at).as_millis() as u64;
*inner.phase_durations_ms.entry(from).or_default() += elapsed_ms;
let transition = IndexPhaseTransition { from, to };
*inner.phase_transition_counts.entry(transition).or_default() += 1;
inner.current_phase = to;
inner.phase_started_at = now;
}
pub fn record_early_exit(&self, record: EarlyExitRecord) {
let mut inner = self.inner.lock();
*inner.early_exit_counts.entry(record.reason).or_default() += 1;
inner.last_early_exit = Some(record);
}
pub fn snapshot(&self) -> IndexInstrumentationSnapshot {
let now = Instant::now();
let inner = self.inner.lock();
let mut state_durations_ms = inner.state_durations_ms.clone();
let mut phase_durations_ms = inner.phase_durations_ms.clone();
let state_elapsed = now.duration_since(inner.state_started_at).as_millis() as u64;
*state_durations_ms.entry(inner.current_state).or_default() += state_elapsed;
if inner.current_state == IndexStateKind::Building {
let phase_elapsed = now.duration_since(inner.phase_started_at).as_millis() as u64;
*phase_durations_ms.entry(inner.current_phase).or_default() += phase_elapsed;
}
IndexInstrumentationSnapshot {
state_durations_ms,
phase_durations_ms,
state_transition_counts: inner.state_transition_counts.clone(),
phase_transition_counts: inner.phase_transition_counts.clone(),
early_exit_counts: inner.early_exit_counts.clone(),
last_early_exit: inner.last_early_exit.clone(),
}
}
}
impl Default for IndexInstrumentation {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use std::thread;
use std::time::Duration;
#[test]
fn test_metrics_threshold_and_parse_storm_detection() -> Result<()> {
let metrics = IndexMetrics::with_threshold(2);
assert_eq!(metrics.pending_count(), 0);
assert!(!metrics.is_parse_storm());
assert_eq!(metrics.increment_pending_parses(), 1);
assert_eq!(metrics.increment_pending_parses(), 2);
assert!(!metrics.is_parse_storm());
assert_eq!(metrics.increment_pending_parses(), 3);
assert!(metrics.is_parse_storm());
assert_eq!(metrics.parse_storm_threshold(), 2);
assert_eq!(metrics.decrement_pending_parses(), 2);
assert!(!metrics.is_parse_storm());
Ok(())
}
#[test]
fn test_instrumentation_records_transitions_and_early_exits() -> Result<()> {
let instrumentation = IndexInstrumentation::new();
instrumentation.record_phase_transition(IndexPhase::Idle, IndexPhase::Scanning);
thread::sleep(Duration::from_millis(1));
instrumentation.record_phase_transition(IndexPhase::Scanning, IndexPhase::Indexing);
instrumentation.record_state_transition(IndexStateKind::Building, IndexStateKind::Ready);
let record = EarlyExitRecord {
reason: EarlyExitReason::FileLimit,
elapsed_ms: 17,
indexed_files: 100,
total_files: 200,
};
instrumentation.record_early_exit(record.clone());
let snapshot = instrumentation.snapshot();
assert_eq!(
snapshot
.phase_transition_counts
.get(&IndexPhaseTransition { from: IndexPhase::Idle, to: IndexPhase::Scanning }),
Some(&1)
);
assert_eq!(
snapshot.phase_transition_counts.get(&IndexPhaseTransition {
from: IndexPhase::Scanning,
to: IndexPhase::Indexing,
}),
Some(&1)
);
assert_eq!(
snapshot.state_transition_counts.get(&IndexStateTransition {
from: IndexStateKind::Building,
to: IndexStateKind::Ready,
}),
Some(&1)
);
assert_eq!(snapshot.early_exit_counts.get(&EarlyExitReason::FileLimit), Some(&1));
assert_eq!(snapshot.last_early_exit, Some(record));
Ok(())
}
#[test]
fn test_snapshot_includes_active_state_duration() -> Result<()> {
let instrumentation = IndexInstrumentation::new();
thread::sleep(Duration::from_millis(1));
let snapshot = instrumentation.snapshot();
let building_duration =
snapshot.state_durations_ms.get(&IndexStateKind::Building).copied().unwrap_or_default();
let idle_phase_duration =
snapshot.phase_durations_ms.get(&IndexPhase::Idle).copied().unwrap_or_default();
assert!(building_duration >= 1);
assert!(idle_phase_duration >= 1);
Ok(())
}
}