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 self.pending_parses.fetch_sub(1, Ordering::SeqCst) - 1
230 }
231
232 pub fn is_parse_storm(&self) -> bool {
234 self.pending_count() > self.parse_storm_threshold
235 }
236
237 pub fn parse_storm_threshold(&self) -> usize {
239 self.parse_storm_threshold
240 }
241}
242
243impl Default for IndexMetrics {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249#[derive(Debug)]
250struct IndexInstrumentationState {
251 current_state: IndexStateKind,
252 current_phase: IndexPhase,
253 state_started_at: Instant,
254 phase_started_at: Instant,
255 state_durations_ms: HashMap<IndexStateKind, u64>,
256 phase_durations_ms: HashMap<IndexPhase, u64>,
257 state_transition_counts: HashMap<IndexStateTransition, u64>,
258 phase_transition_counts: HashMap<IndexPhaseTransition, u64>,
259 early_exit_counts: HashMap<EarlyExitReason, u64>,
260 last_early_exit: Option<EarlyExitRecord>,
261}
262
263impl IndexInstrumentationState {
264 fn new() -> Self {
265 let now = Instant::now();
266 Self {
267 current_state: IndexStateKind::Building,
268 current_phase: IndexPhase::Idle,
269 state_started_at: now,
270 phase_started_at: now,
271 state_durations_ms: HashMap::new(),
272 phase_durations_ms: HashMap::new(),
273 state_transition_counts: HashMap::new(),
274 phase_transition_counts: HashMap::new(),
275 early_exit_counts: HashMap::new(),
276 last_early_exit: None,
277 }
278 }
279}
280
281#[derive(Debug)]
283pub struct IndexInstrumentation {
284 inner: Mutex<IndexInstrumentationState>,
285}
286
287impl IndexInstrumentation {
288 pub fn new() -> Self {
290 Self { inner: Mutex::new(IndexInstrumentationState::new()) }
291 }
292
293 pub fn record_state_transition(&self, from: IndexStateKind, to: IndexStateKind) {
295 let now = Instant::now();
296 let mut inner = self.inner.lock();
297 let elapsed_ms = now.duration_since(inner.state_started_at).as_millis() as u64;
298 *inner.state_durations_ms.entry(from).or_default() += elapsed_ms;
299
300 let transition = IndexStateTransition { from, to };
301 *inner.state_transition_counts.entry(transition).or_default() += 1;
302
303 if from == IndexStateKind::Building {
304 let phase_elapsed = now.duration_since(inner.phase_started_at).as_millis() as u64;
305 let current_phase = inner.current_phase;
306 *inner.phase_durations_ms.entry(current_phase).or_default() += phase_elapsed;
307 }
308
309 inner.current_state = to;
310 inner.state_started_at = now;
311
312 if to == IndexStateKind::Building || from == IndexStateKind::Building {
313 inner.current_phase = IndexPhase::Idle;
314 inner.phase_started_at = now;
315 }
316 }
317
318 pub fn record_phase_transition(&self, from: IndexPhase, to: IndexPhase) {
320 let now = Instant::now();
321 let mut inner = self.inner.lock();
322 let elapsed_ms = now.duration_since(inner.phase_started_at).as_millis() as u64;
323 *inner.phase_durations_ms.entry(from).or_default() += elapsed_ms;
324
325 let transition = IndexPhaseTransition { from, to };
326 *inner.phase_transition_counts.entry(transition).or_default() += 1;
327
328 inner.current_phase = to;
329 inner.phase_started_at = now;
330 }
331
332 pub fn record_early_exit(&self, record: EarlyExitRecord) {
334 let mut inner = self.inner.lock();
335 *inner.early_exit_counts.entry(record.reason).or_default() += 1;
336 inner.last_early_exit = Some(record);
337 }
338
339 pub fn snapshot(&self) -> IndexInstrumentationSnapshot {
341 let now = Instant::now();
342 let inner = self.inner.lock();
343 let mut state_durations_ms = inner.state_durations_ms.clone();
344 let mut phase_durations_ms = inner.phase_durations_ms.clone();
345
346 let state_elapsed = now.duration_since(inner.state_started_at).as_millis() as u64;
347 *state_durations_ms.entry(inner.current_state).or_default() += state_elapsed;
348
349 if inner.current_state == IndexStateKind::Building {
350 let phase_elapsed = now.duration_since(inner.phase_started_at).as_millis() as u64;
351 *phase_durations_ms.entry(inner.current_phase).or_default() += phase_elapsed;
352 }
353
354 IndexInstrumentationSnapshot {
355 state_durations_ms,
356 phase_durations_ms,
357 state_transition_counts: inner.state_transition_counts.clone(),
358 phase_transition_counts: inner.phase_transition_counts.clone(),
359 early_exit_counts: inner.early_exit_counts.clone(),
360 last_early_exit: inner.last_early_exit.clone(),
361 }
362 }
363}
364
365impl Default for IndexInstrumentation {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use anyhow::Result;
375 use std::thread;
376 use std::time::Duration;
377
378 #[test]
379 fn test_metrics_threshold_and_parse_storm_detection() -> Result<()> {
380 let metrics = IndexMetrics::with_threshold(2);
381 assert_eq!(metrics.pending_count(), 0);
382 assert!(!metrics.is_parse_storm());
383
384 assert_eq!(metrics.increment_pending_parses(), 1);
385 assert_eq!(metrics.increment_pending_parses(), 2);
386 assert!(!metrics.is_parse_storm());
387
388 assert_eq!(metrics.increment_pending_parses(), 3);
389 assert!(metrics.is_parse_storm());
390 assert_eq!(metrics.parse_storm_threshold(), 2);
391
392 assert_eq!(metrics.decrement_pending_parses(), 2);
393 assert!(!metrics.is_parse_storm());
394 Ok(())
395 }
396
397 #[test]
398 fn test_instrumentation_records_transitions_and_early_exits() -> Result<()> {
399 let instrumentation = IndexInstrumentation::new();
400
401 instrumentation.record_phase_transition(IndexPhase::Idle, IndexPhase::Scanning);
402 thread::sleep(Duration::from_millis(1));
403 instrumentation.record_phase_transition(IndexPhase::Scanning, IndexPhase::Indexing);
404
405 instrumentation.record_state_transition(IndexStateKind::Building, IndexStateKind::Ready);
406
407 let record = EarlyExitRecord {
408 reason: EarlyExitReason::FileLimit,
409 elapsed_ms: 17,
410 indexed_files: 100,
411 total_files: 200,
412 };
413 instrumentation.record_early_exit(record.clone());
414
415 let snapshot = instrumentation.snapshot();
416
417 assert_eq!(
418 snapshot
419 .phase_transition_counts
420 .get(&IndexPhaseTransition { from: IndexPhase::Idle, to: IndexPhase::Scanning }),
421 Some(&1)
422 );
423 assert_eq!(
424 snapshot.phase_transition_counts.get(&IndexPhaseTransition {
425 from: IndexPhase::Scanning,
426 to: IndexPhase::Indexing,
427 }),
428 Some(&1)
429 );
430 assert_eq!(
431 snapshot.state_transition_counts.get(&IndexStateTransition {
432 from: IndexStateKind::Building,
433 to: IndexStateKind::Ready,
434 }),
435 Some(&1)
436 );
437 assert_eq!(snapshot.early_exit_counts.get(&EarlyExitReason::FileLimit), Some(&1));
438 assert_eq!(snapshot.last_early_exit, Some(record));
439
440 Ok(())
441 }
442
443 #[test]
444 fn test_snapshot_includes_active_state_duration() -> Result<()> {
445 let instrumentation = IndexInstrumentation::new();
446 thread::sleep(Duration::from_millis(1));
447 let snapshot = instrumentation.snapshot();
448
449 let building_duration =
450 snapshot.state_durations_ms.get(&IndexStateKind::Building).copied().unwrap_or_default();
451 let idle_phase_duration =
452 snapshot.phase_durations_ms.get(&IndexPhase::Idle).copied().unwrap_or_default();
453
454 assert!(building_duration >= 1);
455 assert!(idle_phase_duration >= 1);
456 Ok(())
457 }
458}