1use parking_lot::Mutex;
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
26use std::time::Instant;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum IndexPhase {
31 Idle,
33 Scanning,
35 Indexing,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41pub enum IndexStateKind {
42 Building,
44 Ready,
46 Degraded,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52pub struct IndexStateTransition {
53 pub from: IndexStateKind,
55 pub to: IndexStateKind,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
61pub struct IndexPhaseTransition {
62 pub from: IndexPhase,
64 pub to: IndexPhase,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70pub enum EarlyExitReason {
71 InitialTimeBudget,
73 IncrementalTimeBudget,
75 FileLimit,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct EarlyExitRecord {
82 pub reason: EarlyExitReason,
84 pub elapsed_ms: u64,
86 pub indexed_files: usize,
88 pub total_files: usize,
90}
91
92#[derive(Clone, Debug)]
94pub struct IndexInstrumentationSnapshot {
95 pub state_durations_ms: HashMap<IndexStateKind, u64>,
97 pub phase_durations_ms: HashMap<IndexPhase, u64>,
99 pub state_transition_counts: HashMap<IndexStateTransition, u64>,
101 pub phase_transition_counts: HashMap<IndexPhaseTransition, u64>,
103 pub early_exit_counts: HashMap<EarlyExitReason, u64>,
105 pub last_early_exit: Option<EarlyExitRecord>,
107}
108
109#[derive(Clone, Debug, PartialEq)]
111pub enum ResourceKind {
112 MaxFiles,
114 MaxSymbols,
116 MaxCacheBytes,
118}
119
120#[derive(Clone, Debug)]
122pub enum DegradationReason {
123 ParseStorm {
125 pending_parses: usize,
127 },
128 IoError {
130 message: String,
132 },
133 ScanTimeout {
135 elapsed_ms: u64,
137 },
138 ResourceLimit {
140 kind: ResourceKind,
142 },
143}
144
145#[derive(Clone, Debug)]
147pub struct IndexResourceLimits {
148 pub max_files: usize,
150 pub max_symbols_per_file: usize,
152 pub max_total_symbols: usize,
154 pub max_ast_cache_bytes: usize,
156 pub max_ast_cache_items: usize,
158 pub max_scan_duration_ms: u64,
160}
161
162impl Default for IndexResourceLimits {
163 fn default() -> Self {
164 Self {
165 max_files: 10_000,
166 max_symbols_per_file: 5_000,
167 max_total_symbols: 500_000,
168 max_ast_cache_bytes: 256 * 1024 * 1024,
169 max_ast_cache_items: 100,
170 max_scan_duration_ms: 30_000,
171 }
172 }
173}
174
175#[derive(Clone, Debug)]
177pub struct IndexPerformanceCaps {
178 pub initial_scan_budget_ms: u64,
180 pub incremental_budget_ms: u64,
182}
183
184impl Default for IndexPerformanceCaps {
185 fn default() -> Self {
186 Self { initial_scan_budget_ms: 500, incremental_budget_ms: 10 }
187 }
188}
189
190pub struct IndexMetrics {
192 pending_parses: AtomicUsize,
193 parse_storm_threshold: usize,
194 #[allow(dead_code)]
195 last_indexed: AtomicU64,
196}
197
198impl IndexMetrics {
199 pub fn new() -> Self {
201 Self {
202 pending_parses: AtomicUsize::new(0),
203 parse_storm_threshold: 10,
204 last_indexed: AtomicU64::new(0),
205 }
206 }
207
208 pub fn with_threshold(threshold: usize) -> Self {
210 Self {
211 pending_parses: AtomicUsize::new(0),
212 parse_storm_threshold: threshold,
213 last_indexed: AtomicU64::new(0),
214 }
215 }
216
217 pub fn pending_count(&self) -> usize {
219 self.pending_parses.load(Ordering::SeqCst)
220 }
221
222 pub fn increment_pending_parses(&self) -> usize {
224 self.pending_parses.fetch_add(1, Ordering::SeqCst) + 1
225 }
226
227 pub fn decrement_pending_parses(&self) -> usize {
229 match self
230 .pending_parses
231 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| current.checked_sub(1))
232 {
233 Ok(previous) => previous.saturating_sub(1),
234 Err(current) => current,
235 }
236 }
237
238 pub fn is_parse_storm(&self) -> bool {
240 self.pending_count() > self.parse_storm_threshold
241 }
242
243 pub fn parse_storm_threshold(&self) -> usize {
245 self.parse_storm_threshold
246 }
247}
248
249impl Default for IndexMetrics {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255#[derive(Debug)]
256struct IndexInstrumentationState {
257 current_state: IndexStateKind,
258 current_phase: IndexPhase,
259 state_started_at: Instant,
260 phase_started_at: Instant,
261 state_durations_ms: HashMap<IndexStateKind, u64>,
262 phase_durations_ms: HashMap<IndexPhase, u64>,
263 state_transition_counts: HashMap<IndexStateTransition, u64>,
264 phase_transition_counts: HashMap<IndexPhaseTransition, u64>,
265 early_exit_counts: HashMap<EarlyExitReason, u64>,
266 last_early_exit: Option<EarlyExitRecord>,
267}
268
269impl IndexInstrumentationState {
270 fn new() -> Self {
271 let now = Instant::now();
272 Self {
273 current_state: IndexStateKind::Building,
274 current_phase: IndexPhase::Idle,
275 state_started_at: now,
276 phase_started_at: now,
277 state_durations_ms: HashMap::new(),
278 phase_durations_ms: HashMap::new(),
279 state_transition_counts: HashMap::new(),
280 phase_transition_counts: HashMap::new(),
281 early_exit_counts: HashMap::new(),
282 last_early_exit: None,
283 }
284 }
285}
286
287#[derive(Debug)]
289pub struct IndexInstrumentation {
290 inner: Mutex<IndexInstrumentationState>,
291}
292
293impl IndexInstrumentation {
294 pub fn new() -> Self {
296 Self { inner: Mutex::new(IndexInstrumentationState::new()) }
297 }
298
299 pub fn record_state_transition(&self, from: IndexStateKind, to: IndexStateKind) {
301 let now = Instant::now();
302 let mut inner = self.inner.lock();
303 let elapsed_ms = now.duration_since(inner.state_started_at).as_millis() as u64;
304 *inner.state_durations_ms.entry(from).or_default() += elapsed_ms;
305
306 let transition = IndexStateTransition { from, to };
307 *inner.state_transition_counts.entry(transition).or_default() += 1;
308
309 if from == IndexStateKind::Building {
310 let phase_elapsed = now.duration_since(inner.phase_started_at).as_millis() as u64;
311 let current_phase = inner.current_phase;
312 *inner.phase_durations_ms.entry(current_phase).or_default() += phase_elapsed;
313 }
314
315 inner.current_state = to;
316 inner.state_started_at = now;
317
318 if to == IndexStateKind::Building || from == IndexStateKind::Building {
319 inner.current_phase = IndexPhase::Idle;
320 inner.phase_started_at = now;
321 }
322 }
323
324 pub fn record_phase_transition(&self, from: IndexPhase, to: IndexPhase) {
326 let now = Instant::now();
327 let mut inner = self.inner.lock();
328 let elapsed_ms = now.duration_since(inner.phase_started_at).as_millis() as u64;
329 *inner.phase_durations_ms.entry(from).or_default() += elapsed_ms;
330
331 let transition = IndexPhaseTransition { from, to };
332 *inner.phase_transition_counts.entry(transition).or_default() += 1;
333
334 inner.current_phase = to;
335 inner.phase_started_at = now;
336 }
337
338 pub fn record_early_exit(&self, record: EarlyExitRecord) {
340 let mut inner = self.inner.lock();
341 *inner.early_exit_counts.entry(record.reason).or_default() += 1;
342 inner.last_early_exit = Some(record);
343 }
344
345 pub fn snapshot(&self) -> IndexInstrumentationSnapshot {
347 let now = Instant::now();
348 let inner = self.inner.lock();
349 let mut state_durations_ms = inner.state_durations_ms.clone();
350 let mut phase_durations_ms = inner.phase_durations_ms.clone();
351
352 let state_elapsed = now.duration_since(inner.state_started_at).as_millis() as u64;
353 *state_durations_ms.entry(inner.current_state).or_default() += state_elapsed;
354
355 if inner.current_state == IndexStateKind::Building {
356 let phase_elapsed = now.duration_since(inner.phase_started_at).as_millis() as u64;
357 *phase_durations_ms.entry(inner.current_phase).or_default() += phase_elapsed;
358 }
359
360 IndexInstrumentationSnapshot {
361 state_durations_ms,
362 phase_durations_ms,
363 state_transition_counts: inner.state_transition_counts.clone(),
364 phase_transition_counts: inner.phase_transition_counts.clone(),
365 early_exit_counts: inner.early_exit_counts.clone(),
366 last_early_exit: inner.last_early_exit.clone(),
367 }
368 }
369}
370
371impl Default for IndexInstrumentation {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use anyhow::Result;
381 use std::thread;
382 use std::time::Duration;
383
384 #[test]
385 fn test_metrics_threshold_and_parse_storm_detection() -> Result<()> {
386 let metrics = IndexMetrics::with_threshold(2);
387 assert_eq!(metrics.pending_count(), 0);
388 assert!(!metrics.is_parse_storm());
389
390 assert_eq!(metrics.increment_pending_parses(), 1);
391 assert_eq!(metrics.increment_pending_parses(), 2);
392 assert!(!metrics.is_parse_storm());
393
394 assert_eq!(metrics.increment_pending_parses(), 3);
395 assert!(metrics.is_parse_storm());
396 assert_eq!(metrics.parse_storm_threshold(), 2);
397
398 assert_eq!(metrics.decrement_pending_parses(), 2);
399 assert!(!metrics.is_parse_storm());
400 Ok(())
401 }
402
403 #[test]
404 fn test_instrumentation_records_transitions_and_early_exits() -> Result<()> {
405 let instrumentation = IndexInstrumentation::new();
406
407 instrumentation.record_phase_transition(IndexPhase::Idle, IndexPhase::Scanning);
408 thread::sleep(Duration::from_millis(1));
409 instrumentation.record_phase_transition(IndexPhase::Scanning, IndexPhase::Indexing);
410
411 instrumentation.record_state_transition(IndexStateKind::Building, IndexStateKind::Ready);
412
413 let record = EarlyExitRecord {
414 reason: EarlyExitReason::FileLimit,
415 elapsed_ms: 17,
416 indexed_files: 100,
417 total_files: 200,
418 };
419 instrumentation.record_early_exit(record.clone());
420
421 let snapshot = instrumentation.snapshot();
422
423 assert_eq!(
424 snapshot
425 .phase_transition_counts
426 .get(&IndexPhaseTransition { from: IndexPhase::Idle, to: IndexPhase::Scanning }),
427 Some(&1)
428 );
429 assert_eq!(
430 snapshot.phase_transition_counts.get(&IndexPhaseTransition {
431 from: IndexPhase::Scanning,
432 to: IndexPhase::Indexing,
433 }),
434 Some(&1)
435 );
436 assert_eq!(
437 snapshot.state_transition_counts.get(&IndexStateTransition {
438 from: IndexStateKind::Building,
439 to: IndexStateKind::Ready,
440 }),
441 Some(&1)
442 );
443 assert_eq!(snapshot.early_exit_counts.get(&EarlyExitReason::FileLimit), Some(&1));
444 assert_eq!(snapshot.last_early_exit, Some(record));
445
446 Ok(())
447 }
448
449 #[test]
450 fn test_snapshot_includes_active_state_duration() -> Result<()> {
451 let instrumentation = IndexInstrumentation::new();
452 thread::sleep(Duration::from_millis(1));
453 let snapshot = instrumentation.snapshot();
454
455 let building_duration =
456 snapshot.state_durations_ms.get(&IndexStateKind::Building).copied().unwrap_or_default();
457 let idle_phase_duration =
458 snapshot.phase_durations_ms.get(&IndexPhase::Idle).copied().unwrap_or_default();
459
460 assert!(building_duration >= 1);
461 assert!(idle_phase_duration >= 1);
462 Ok(())
463 }
464}