Skip to main content

perl_workspace_index_slo/
lib.rs

1//! Service Level Objectives (SLOs) for workspace index operations.
2//!
3//! This module defines performance targets and monitoring infrastructure
4//! for critical workspace index operations. SLOs provide measurable
5//! quality targets for production deployments.
6//!
7//! # SLO Targets
8//!
9//! - **Index Initialization**: <5s for 10K files (P95)
10//! - **Incremental Update**: <100ms for single file change (P95)
11//! - **Definition Lookup**: <50ms (P95)
12//! - **Completion**: <100ms (P95)
13//! - **Hover**: <50ms (P95)
14//!
15//! # Performance Monitoring
16//!
17//! - Latency tracking with percentiles (P50, P95, P99)
18//! - Error rate monitoring
19//! - Throughput metrics
20//! - SLO compliance reporting
21//!
22//! # Usage
23//!
24//! ```rust
25//! use perl_workspace_index_slo::{SloConfig, SloTracker};
26//! use perl_workspace_index_slo::{OperationResult, OperationType};
27//!
28//! let config = SloConfig::default();
29//! let tracker = SloTracker::new(config);
30//!
31//! let start = tracker.start_operation(OperationType::DefinitionLookup);
32//! // ... perform operation ...
33//! tracker.record_operation(start, OperationResult::Success);
34//! ```
35
36use parking_lot::Mutex;
37use perl_percentile::nearest_rank_percentile;
38use std::collections::VecDeque;
39use std::sync::Arc;
40use std::time::{Duration, Instant};
41
42/// SLO configuration for workspace index operations.
43///
44/// Defines latency targets and monitoring parameters for each operation type.
45#[derive(Clone, Debug)]
46pub struct SloConfig {
47    /// Target latency for index initialization (P95)
48    pub index_init_p95_ms: u64,
49    /// Target latency for incremental updates (P95)
50    pub incremental_update_p95_ms: u64,
51    /// Target latency for definition lookup (P95)
52    pub definition_lookup_p95_ms: u64,
53    /// Target latency for completion (P95)
54    pub completion_p95_ms: u64,
55    /// Target latency for hover (P95)
56    pub hover_p95_ms: u64,
57    /// Maximum acceptable error rate (0.0 to 1.0)
58    pub max_error_rate: f64,
59    /// Number of samples to keep for percentile calculation
60    pub sample_window_size: usize,
61}
62
63impl Default for SloConfig {
64    fn default() -> Self {
65        Self {
66            index_init_p95_ms: 5000,        // 5 seconds
67            incremental_update_p95_ms: 100, // 100ms
68            definition_lookup_p95_ms: 50,   // 50ms
69            completion_p95_ms: 100,         // 100ms
70            hover_p95_ms: 50,               // 50ms
71            max_error_rate: 0.01,           // 1% error rate
72            sample_window_size: 1000,
73        }
74    }
75}
76
77/// Operation types tracked by SLO monitoring.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub enum OperationType {
80    /// Initial workspace index construction
81    IndexInitialization,
82    /// Incremental index update for file changes
83    IncrementalUpdate,
84    /// Go-to-definition lookup
85    DefinitionLookup,
86    /// Code completion request
87    Completion,
88    /// Hover information request
89    Hover,
90    /// Find references request
91    FindReferences,
92    /// Workspace symbol search
93    WorkspaceSymbols,
94    /// File indexing operation
95    FileIndexing,
96}
97
98impl OperationType {
99    /// Get the SLO target for this operation type.
100    pub fn slo_target_ms(&self, config: &SloConfig) -> u64 {
101        match self {
102            OperationType::IndexInitialization => config.index_init_p95_ms,
103            OperationType::IncrementalUpdate => config.incremental_update_p95_ms,
104            OperationType::DefinitionLookup => config.definition_lookup_p95_ms,
105            OperationType::Completion => config.completion_p95_ms,
106            OperationType::Hover => config.hover_p95_ms,
107            OperationType::FindReferences => config.definition_lookup_p95_ms, // Same as definition
108            OperationType::WorkspaceSymbols => config.definition_lookup_p95_ms, // Same as definition
109            OperationType::FileIndexing => config.incremental_update_p95_ms, // Same as incremental
110        }
111    }
112
113    /// Get a human-readable name for this operation.
114    pub fn name(&self) -> &'static str {
115        match self {
116            OperationType::IndexInitialization => "index_initialization",
117            OperationType::IncrementalUpdate => "incremental_update",
118            OperationType::DefinitionLookup => "definition_lookup",
119            OperationType::Completion => "completion",
120            OperationType::Hover => "hover",
121            OperationType::FindReferences => "find_references",
122            OperationType::WorkspaceSymbols => "workspace_symbols",
123            OperationType::FileIndexing => "file_indexing",
124        }
125    }
126}
127
128/// Result of an SLO operation.
129#[derive(Clone, Debug)]
130pub enum OperationResult {
131    /// Operation succeeded
132    Success,
133    /// Operation failed with an error message
134    Failure(String),
135}
136
137impl OperationResult {
138    /// Check if the operation was successful.
139    pub fn is_success(&self) -> bool {
140        matches!(self, OperationResult::Success)
141    }
142}
143
144impl<T, E> From<Result<T, E>> for OperationResult
145where
146    E: std::fmt::Display,
147{
148    fn from(result: Result<T, E>) -> Self {
149        match result {
150            Ok(_) => OperationResult::Success,
151            Err(e) => OperationResult::Failure(e.to_string()),
152        }
153    }
154}
155
156/// Latency sample for SLO tracking.
157#[derive(Clone, Debug)]
158struct LatencySample {
159    /// Duration of the operation
160    duration: Duration,
161    /// Whether the operation succeeded
162    success: bool,
163    /// When the sample was recorded
164    _timestamp: Instant,
165}
166
167/// SLO statistics for a specific operation type.
168#[derive(Clone, Debug)]
169pub struct SloStatistics {
170    /// Total number of operations
171    pub total_count: u64,
172    /// Number of successful operations
173    pub success_count: u64,
174    /// Number of failed operations
175    pub failure_count: u64,
176    /// Error rate (failures / total)
177    pub error_rate: f64,
178    /// P50 latency (median)
179    pub p50_ms: u64,
180    /// P95 latency
181    pub p95_ms: u64,
182    /// P99 latency
183    pub p99_ms: u64,
184    /// Average latency
185    pub avg_ms: f64,
186    /// Whether SLO is being met
187    pub slo_met: bool,
188}
189
190impl Default for SloStatistics {
191    fn default() -> Self {
192        Self {
193            total_count: 0,
194            success_count: 0,
195            failure_count: 0,
196            error_rate: 0.0,
197            p50_ms: 0,
198            p95_ms: 0,
199            p99_ms: 0,
200            avg_ms: 0.0,
201            slo_met: true,
202        }
203    }
204}
205
206/// Per-operation SLO tracker.
207#[derive(Debug)]
208struct OperationSloTracker {
209    /// Operation type being tracked
210    _operation_type: OperationType,
211    /// Latency samples (most recent first)
212    samples: VecDeque<LatencySample>,
213    /// SLO target for this operation
214    slo_target_ms: u64,
215    /// Maximum error rate
216    max_error_rate: f64,
217    /// Maximum number of samples to keep
218    max_samples: usize,
219}
220
221impl OperationSloTracker {
222    /// Create a new operation SLO tracker.
223    fn new(operation_type: OperationType, config: &SloConfig) -> Self {
224        Self {
225            _operation_type: operation_type,
226            samples: VecDeque::with_capacity(config.sample_window_size),
227            slo_target_ms: operation_type.slo_target_ms(config),
228            max_error_rate: config.max_error_rate,
229            max_samples: config.sample_window_size,
230        }
231    }
232
233    /// Record an operation result.
234    fn record(&mut self, duration: Duration, result: OperationResult) {
235        let success = result.is_success();
236        let sample = LatencySample { duration, success, _timestamp: Instant::now() };
237
238        // Add sample
239        if self.samples.len() >= self.max_samples {
240            self.samples.pop_front();
241        }
242        self.samples.push_back(sample);
243    }
244
245    /// Calculate SLO statistics for this operation.
246    fn statistics(&self) -> SloStatistics {
247        if self.samples.is_empty() {
248            return SloStatistics::default();
249        }
250
251        let total_count = self.samples.len() as u64;
252        let success_count = self.samples.iter().filter(|s| s.success).count() as u64;
253        let failure_count = total_count - success_count;
254        let error_rate =
255            if total_count > 0 { failure_count as f64 / total_count as f64 } else { 0.0 };
256
257        // Calculate percentiles
258        let mut durations_ms: Vec<u64> =
259            self.samples.iter().map(|s| s.duration.as_millis() as u64).collect();
260        durations_ms.sort_unstable();
261
262        let p50_ms = nearest_rank_percentile(&durations_ms, 50);
263        let p95_ms = nearest_rank_percentile(&durations_ms, 95);
264        let p99_ms = nearest_rank_percentile(&durations_ms, 99);
265
266        let avg_ms =
267            durations_ms.iter().map(|&d| d as f64).sum::<f64>() / durations_ms.len() as f64;
268
269        // Check if SLO is met
270        let slo_met = p95_ms <= self.slo_target_ms && error_rate <= self.max_error_rate;
271
272        SloStatistics {
273            total_count,
274            success_count,
275            failure_count,
276            error_rate,
277            p50_ms,
278            p95_ms,
279            p99_ms,
280            avg_ms,
281            slo_met,
282        }
283    }
284}
285
286/// SLO tracker for workspace index operations.
287///
288/// Tracks latency and success/failure for all operation types,
289/// providing SLO compliance monitoring and reporting.
290pub struct SloTracker {
291    /// SLO configuration
292    config: SloConfig,
293    /// Per-operation trackers
294    trackers: Arc<Mutex<std::collections::HashMap<OperationType, OperationSloTracker>>>,
295}
296
297impl SloTracker {
298    /// Create a new SLO tracker with the given configuration.
299    ///
300    /// # Arguments
301    ///
302    /// * `config` - SLO configuration
303    ///
304    /// # Returns
305    ///
306    /// A new SLO tracker instance.
307    ///
308    /// # Examples
309    ///
310    /// ```rust
311    /// use perl_workspace_index_slo::{SloConfig, SloTracker};
312    ///
313    /// let config = SloConfig::default();
314    /// let tracker = SloTracker::new(config);
315    /// ```
316    pub fn new(config: SloConfig) -> Self {
317        let mut trackers = std::collections::HashMap::new();
318
319        // Initialize trackers for all operation types
320        for op_type in [
321            OperationType::IndexInitialization,
322            OperationType::IncrementalUpdate,
323            OperationType::DefinitionLookup,
324            OperationType::Completion,
325            OperationType::Hover,
326            OperationType::FindReferences,
327            OperationType::WorkspaceSymbols,
328            OperationType::FileIndexing,
329        ] {
330            trackers.insert(op_type, OperationSloTracker::new(op_type, &config));
331        }
332
333        Self { config, trackers: Arc::new(Mutex::new(trackers)) }
334    }
335
336    /// Start tracking an operation.
337    ///
338    /// # Arguments
339    ///
340    /// * `operation_type` - Type of operation to track
341    ///
342    /// # Returns
343    ///
344    /// A timestamp that should be passed to `record_operation`.
345    ///
346    /// # Examples
347    ///
348    /// ```rust
349    /// use perl_workspace_index_slo::{OperationResult, OperationType, SloTracker};
350    ///
351    /// let tracker = SloTracker::default();
352    /// let start = tracker.start_operation(OperationType::DefinitionLookup);
353    /// // ... perform operation ...
354    /// tracker.record_operation(start, OperationResult::Success);
355    /// ```
356    pub fn start_operation(&self, _operation_type: OperationType) -> Instant {
357        Instant::now()
358    }
359
360    /// Record the completion of an operation.
361    ///
362    /// # Arguments
363    ///
364    /// * `start` - Timestamp returned from `start_operation`
365    /// * `result` - Operation result (success or failure)
366    ///
367    /// # Examples
368    ///
369    /// ```rust
370    /// use perl_workspace_index_slo::{SloTracker, OperationType, OperationResult};
371    ///
372    /// let tracker = SloTracker::default();
373    /// let start = tracker.start_operation(OperationType::DefinitionLookup);
374    /// // ... perform operation ...
375    /// tracker.record_operation(start, OperationResult::Success);
376    /// ```
377    pub fn record_operation(&self, start: Instant, result: OperationResult) {
378        let duration = start.elapsed();
379        let mut trackers = self.trackers.lock();
380
381        // Record for all operation types (simplified - in practice you'd pass the type)
382        for tracker in trackers.values_mut() {
383            tracker.record(duration, result.clone());
384        }
385    }
386
387    /// Record the completion of a specific operation type.
388    ///
389    /// # Arguments
390    ///
391    /// * `operation_type` - Type of operation
392    /// * `start` - Timestamp returned from `start_operation`
393    /// * `result` - Operation result
394    ///
395    /// # Examples
396    ///
397    /// ```rust
398    /// use perl_workspace_index_slo::{SloTracker, OperationType, OperationResult};
399    ///
400    /// let tracker = SloTracker::default();
401    /// let start = tracker.start_operation(OperationType::DefinitionLookup);
402    /// // ... perform operation ...
403    /// tracker.record_operation_type(OperationType::DefinitionLookup, start, OperationResult::Success);
404    /// ```
405    pub fn record_operation_type(
406        &self,
407        operation_type: OperationType,
408        start: Instant,
409        result: OperationResult,
410    ) {
411        let duration = start.elapsed();
412        let mut trackers = self.trackers.lock();
413
414        if let Some(tracker) = trackers.get_mut(&operation_type) {
415            tracker.record(duration, result);
416        }
417    }
418
419    /// Get SLO statistics for a specific operation type.
420    ///
421    /// # Arguments
422    ///
423    /// * `operation_type` - Type of operation
424    ///
425    /// # Returns
426    ///
427    /// SLO statistics for the operation type.
428    ///
429    /// # Examples
430    ///
431    /// ```rust
432    /// use perl_workspace_index_slo::{SloTracker, OperationType};
433    ///
434    /// let tracker = SloTracker::default();
435    /// let stats = tracker.statistics(OperationType::DefinitionLookup);
436    /// ```
437    pub fn statistics(&self, operation_type: OperationType) -> SloStatistics {
438        let trackers = self.trackers.lock();
439        trackers.get(&operation_type).map(|t| t.statistics()).unwrap_or_default()
440    }
441
442    /// Get SLO statistics for all operation types.
443    ///
444    /// # Returns
445    ///
446    /// A map of operation type to SLO statistics.
447    ///
448    /// # Examples
449    ///
450    /// ```rust
451    /// use perl_workspace_index_slo::SloTracker;
452    ///
453    /// let tracker = SloTracker::default();
454    /// let all_stats = tracker.all_statistics();
455    /// ```
456    pub fn all_statistics(&self) -> std::collections::HashMap<OperationType, SloStatistics> {
457        let trackers = self.trackers.lock();
458        trackers.iter().map(|(op_type, tracker)| (*op_type, tracker.statistics())).collect()
459    }
460
461    /// Check if all SLOs are being met.
462    ///
463    /// # Returns
464    ///
465    /// `true` if all operation types are meeting their SLOs, `false` otherwise.
466    ///
467    /// # Examples
468    ///
469    /// ```rust
470    /// use perl_workspace_index_slo::SloTracker;
471    ///
472    /// let tracker = SloTracker::default();
473    /// let all_met = tracker.all_slos_met();
474    /// ```
475    pub fn all_slos_met(&self) -> bool {
476        let trackers = self.trackers.lock();
477        trackers.values().all(|t| t.statistics().slo_met)
478    }
479
480    /// Get the SLO configuration.
481    ///
482    /// # Returns
483    ///
484    /// The SLO configuration.
485    pub fn config(&self) -> &SloConfig {
486        &self.config
487    }
488
489    /// Reset all statistics.
490    ///
491    /// # Examples
492    ///
493    /// ```rust
494    /// use perl_workspace_index_slo::SloTracker;
495    ///
496    /// let tracker = SloTracker::default();
497    /// tracker.reset();
498    /// ```
499    pub fn reset(&self) {
500        let mut trackers = self.trackers.lock();
501        for tracker in trackers.values_mut() {
502            tracker.samples.clear();
503        }
504    }
505}
506
507impl Default for SloTracker {
508    fn default() -> Self {
509        Self::new(SloConfig::default())
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[test]
518    fn test_slo_tracker_record() {
519        let tracker = SloTracker::default();
520        let start = tracker.start_operation(OperationType::DefinitionLookup);
521        tracker.record_operation_type(
522            OperationType::DefinitionLookup,
523            start,
524            OperationResult::Success,
525        );
526
527        let stats = tracker.statistics(OperationType::DefinitionLookup);
528        assert_eq!(stats.total_count, 1);
529        assert_eq!(stats.success_count, 1);
530    }
531
532    #[test]
533    fn test_slo_statistics() {
534        let tracker = SloTracker::default();
535
536        // Record some operations
537        for _ in 0..10 {
538            let start = tracker.start_operation(OperationType::DefinitionLookup);
539            std::thread::sleep(Duration::from_millis(1));
540            tracker.record_operation_type(
541                OperationType::DefinitionLookup,
542                start,
543                OperationResult::Success,
544            );
545        }
546
547        let stats = tracker.statistics(OperationType::DefinitionLookup);
548        assert_eq!(stats.total_count, 10);
549        assert_eq!(stats.success_count, 10);
550        assert!(stats.p50_ms > 0);
551        assert!(stats.p95_ms > 0);
552    }
553
554    #[test]
555    fn test_slo_met() {
556        let tracker = SloTracker::default();
557
558        // Record fast operations (should meet SLO)
559        for _ in 0..10 {
560            let start = tracker.start_operation(OperationType::DefinitionLookup);
561            std::thread::sleep(Duration::from_millis(1));
562            tracker.record_operation_type(
563                OperationType::DefinitionLookup,
564                start,
565                OperationResult::Success,
566            );
567        }
568
569        let stats = tracker.statistics(OperationType::DefinitionLookup);
570        assert!(stats.slo_met);
571    }
572
573    #[test]
574    fn test_operation_type_name() {
575        assert_eq!(OperationType::DefinitionLookup.name(), "definition_lookup");
576        assert_eq!(OperationType::Completion.name(), "completion");
577    }
578
579    #[test]
580    fn test_slo_target_ms() {
581        let config = SloConfig::default();
582        assert_eq!(OperationType::DefinitionLookup.slo_target_ms(&config), 50);
583        assert_eq!(OperationType::Completion.slo_target_ms(&config), 100);
584    }
585
586    #[test]
587    fn test_operation_result() {
588        assert!(OperationResult::Success.is_success());
589        assert!(!OperationResult::Failure("error".to_string()).is_success());
590    }
591
592    #[test]
593    fn test_percentile() {
594        let values = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
595        assert_eq!(nearest_rank_percentile(&values, 50), 5); // Median
596        assert_eq!(nearest_rank_percentile(&values, 95), 10); // P95
597    }
598}