Skip to main content

perl_workspace/slo/
mod.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::slo::{SloConfig, SloTracker};
26//! use perl_workspace::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_type(OperationType::DefinitionLookup, start, OperationResult::Success);
34//! ```
35
36use parking_lot::Mutex;
37use perl_parser_core::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}
164
165/// SLO statistics for a specific operation type.
166#[derive(Clone, Debug)]
167pub struct SloStatistics {
168    /// Total number of operations
169    pub total_count: u64,
170    /// Number of successful operations
171    pub success_count: u64,
172    /// Number of failed operations
173    pub failure_count: u64,
174    /// Error rate (failures / total)
175    pub error_rate: f64,
176    /// P50 latency (median)
177    pub p50_ms: u64,
178    /// P95 latency
179    pub p95_ms: u64,
180    /// P99 latency
181    pub p99_ms: u64,
182    /// Average latency
183    pub avg_ms: f64,
184    /// Whether SLO is being met
185    pub slo_met: bool,
186}
187
188impl Default for SloStatistics {
189    fn default() -> Self {
190        Self {
191            total_count: 0,
192            success_count: 0,
193            failure_count: 0,
194            error_rate: 0.0,
195            p50_ms: 0,
196            p95_ms: 0,
197            p99_ms: 0,
198            avg_ms: 0.0,
199            slo_met: true,
200        }
201    }
202}
203
204/// Per-operation SLO tracker.
205#[derive(Debug)]
206struct OperationSloTracker {
207    /// Operation type being tracked
208    _operation_type: OperationType,
209    /// Latency samples (most recent first)
210    samples: VecDeque<LatencySample>,
211    /// SLO target for this operation
212    slo_target_ms: u64,
213    /// Maximum error rate
214    max_error_rate: f64,
215    /// Maximum number of samples to keep
216    max_samples: usize,
217}
218
219impl OperationSloTracker {
220    /// Create a new operation SLO tracker.
221    fn new(operation_type: OperationType, config: &SloConfig) -> Self {
222        Self {
223            _operation_type: operation_type,
224            samples: VecDeque::with_capacity(config.sample_window_size),
225            slo_target_ms: operation_type.slo_target_ms(config),
226            max_error_rate: config.max_error_rate,
227            max_samples: config.sample_window_size,
228        }
229    }
230
231    /// Record an operation result.
232    fn record(&mut self, duration: Duration, result: OperationResult) {
233        let success = result.is_success();
234        let sample = LatencySample { duration, success };
235
236        // Add sample
237        if self.samples.len() >= self.max_samples {
238            self.samples.pop_front();
239        }
240        self.samples.push_back(sample);
241    }
242
243    /// Calculate SLO statistics for this operation.
244    fn statistics(&self) -> SloStatistics {
245        if self.samples.is_empty() {
246            return SloStatistics::default();
247        }
248
249        let total_count = self.samples.len() as u64;
250        let success_count = self.samples.iter().filter(|s| s.success).count() as u64;
251        let failure_count = total_count - success_count;
252        let error_rate =
253            if total_count > 0 { failure_count as f64 / total_count as f64 } else { 0.0 };
254
255        // Calculate percentiles
256        let mut durations_ms: Vec<u64> =
257            self.samples.iter().map(|s| s.duration.as_millis() as u64).collect();
258        durations_ms.sort_unstable();
259
260        let p50_ms = nearest_rank_percentile(&durations_ms, 50);
261        let p95_ms = nearest_rank_percentile(&durations_ms, 95);
262        let p99_ms = nearest_rank_percentile(&durations_ms, 99);
263
264        let avg_ms =
265            durations_ms.iter().map(|&d| d as f64).sum::<f64>() / durations_ms.len() as f64;
266
267        // Check if SLO is met
268        let slo_met = p95_ms <= self.slo_target_ms && error_rate <= self.max_error_rate;
269
270        SloStatistics {
271            total_count,
272            success_count,
273            failure_count,
274            error_rate,
275            p50_ms,
276            p95_ms,
277            p99_ms,
278            avg_ms,
279            slo_met,
280        }
281    }
282}
283
284/// SLO tracker for workspace index operations.
285///
286/// Tracks latency and success/failure for all operation types,
287/// providing SLO compliance monitoring and reporting.
288pub struct SloTracker {
289    /// SLO configuration
290    config: SloConfig,
291    /// Per-operation trackers
292    trackers: Arc<Mutex<std::collections::HashMap<OperationType, OperationSloTracker>>>,
293}
294
295impl SloTracker {
296    /// Create a new SLO tracker with the given configuration.
297    ///
298    /// # Arguments
299    ///
300    /// * `config` - SLO configuration
301    ///
302    /// # Returns
303    ///
304    /// A new SLO tracker instance.
305    ///
306    /// # Examples
307    ///
308    /// ```rust
309    /// use perl_workspace::slo::{SloConfig, SloTracker};
310    ///
311    /// let config = SloConfig::default();
312    /// let tracker = SloTracker::new(config);
313    /// ```
314    pub fn new(config: SloConfig) -> Self {
315        let mut trackers = std::collections::HashMap::new();
316
317        // Initialize trackers for all operation types
318        for op_type in [
319            OperationType::IndexInitialization,
320            OperationType::IncrementalUpdate,
321            OperationType::DefinitionLookup,
322            OperationType::Completion,
323            OperationType::Hover,
324            OperationType::FindReferences,
325            OperationType::WorkspaceSymbols,
326            OperationType::FileIndexing,
327        ] {
328            trackers.insert(op_type, OperationSloTracker::new(op_type, &config));
329        }
330
331        Self { config, trackers: Arc::new(Mutex::new(trackers)) }
332    }
333
334    /// Start tracking an operation.
335    ///
336    /// Returns an [`Instant`] to pass to [`Self::record_operation_type`].
337    /// The `operation_type` parameter is accepted for call-site readability
338    /// but is not encoded in the returned timestamp — always pair with
339    /// `record_operation_type` using the same type.
340    ///
341    /// # Examples
342    ///
343    /// ```rust
344    /// use perl_workspace::slo::{OperationResult, OperationType, SloTracker};
345    ///
346    /// let tracker = SloTracker::default();
347    /// let start = tracker.start_operation(OperationType::DefinitionLookup);
348    /// // ... perform operation ...
349    /// tracker.record_operation_type(OperationType::DefinitionLookup, start, OperationResult::Success);
350    /// ```
351    pub fn start_operation(&self, _operation_type: OperationType) -> Instant {
352        Instant::now()
353    }
354
355    /// Record the completion of an operation.
356    ///
357    /// # Deprecation
358    ///
359    /// This method records the duration to **every** operation-type tracker
360    /// simultaneously, which pollutes all per-type statistics.  Use
361    /// [`Self::record_operation_type`] instead to target the correct tracker.
362    ///
363    /// # Examples
364    ///
365    /// ```rust
366    /// use perl_workspace::slo::{SloTracker, OperationType, OperationResult};
367    ///
368    /// let tracker = SloTracker::default();
369    /// let start = tracker.start_operation(OperationType::DefinitionLookup);
370    /// // ... perform operation ...
371    /// // Prefer: tracker.record_operation_type(OperationType::DefinitionLookup, start, OperationResult::Success);
372    /// #[allow(deprecated)]
373    /// tracker.record_operation(start, OperationResult::Success);
374    /// ```
375    #[deprecated(
376        since = "0.13.0",
377        note = "records to every operation-type tracker at once — use record_operation_type instead"
378    )]
379    pub fn record_operation(&self, start: Instant, result: OperationResult) {
380        let duration = start.elapsed();
381        let mut trackers = self.trackers.lock();
382
383        for tracker in trackers.values_mut() {
384            tracker.record(duration, result.clone());
385        }
386    }
387
388    /// Record the completion of a specific operation type.
389    ///
390    /// # Arguments
391    ///
392    /// * `operation_type` - Type of operation
393    /// * `start` - Timestamp returned from `start_operation`
394    /// * `result` - Operation result
395    ///
396    /// # Examples
397    ///
398    /// ```rust
399    /// use perl_workspace::slo::{SloTracker, OperationType, OperationResult};
400    ///
401    /// let tracker = SloTracker::default();
402    /// let start = tracker.start_operation(OperationType::DefinitionLookup);
403    /// // ... perform operation ...
404    /// tracker.record_operation_type(OperationType::DefinitionLookup, start, OperationResult::Success);
405    /// ```
406    pub fn record_operation_type(
407        &self,
408        operation_type: OperationType,
409        start: Instant,
410        result: OperationResult,
411    ) {
412        let duration = start.elapsed();
413        let mut trackers = self.trackers.lock();
414
415        if let Some(tracker) = trackers.get_mut(&operation_type) {
416            tracker.record(duration, result);
417        }
418    }
419
420    /// Get SLO statistics for a specific operation type.
421    ///
422    /// # Arguments
423    ///
424    /// * `operation_type` - Type of operation
425    ///
426    /// # Returns
427    ///
428    /// SLO statistics for the operation type.
429    ///
430    /// # Examples
431    ///
432    /// ```rust
433    /// use perl_workspace::slo::{SloTracker, OperationType};
434    ///
435    /// let tracker = SloTracker::default();
436    /// let stats = tracker.statistics(OperationType::DefinitionLookup);
437    /// ```
438    pub fn statistics(&self, operation_type: OperationType) -> SloStatistics {
439        let trackers = self.trackers.lock();
440        trackers.get(&operation_type).map(|t| t.statistics()).unwrap_or_default()
441    }
442
443    /// Get SLO statistics for all operation types.
444    ///
445    /// # Returns
446    ///
447    /// A map of operation type to SLO statistics.
448    ///
449    /// # Examples
450    ///
451    /// ```rust
452    /// use perl_workspace::slo::SloTracker;
453    ///
454    /// let tracker = SloTracker::default();
455    /// let all_stats = tracker.all_statistics();
456    /// ```
457    pub fn all_statistics(&self) -> std::collections::HashMap<OperationType, SloStatistics> {
458        let trackers = self.trackers.lock();
459        trackers.iter().map(|(op_type, tracker)| (*op_type, tracker.statistics())).collect()
460    }
461
462    /// Check if all SLOs are being met.
463    ///
464    /// # Returns
465    ///
466    /// `true` if all operation types are meeting their SLOs, `false` otherwise.
467    ///
468    /// # Examples
469    ///
470    /// ```rust
471    /// use perl_workspace::slo::SloTracker;
472    ///
473    /// let tracker = SloTracker::default();
474    /// let all_met = tracker.all_slos_met();
475    /// ```
476    pub fn all_slos_met(&self) -> bool {
477        let trackers = self.trackers.lock();
478        trackers.values().all(|t| t.statistics().slo_met)
479    }
480
481    /// Return the number of samples currently in the window for `operation_type`.
482    ///
483    /// Useful for testing and monitoring — confirms that samples are being
484    /// recorded without computing full percentile statistics.
485    ///
486    /// # Examples
487    ///
488    /// ```rust
489    /// use perl_workspace::slo::{OperationResult, OperationType, SloTracker};
490    ///
491    /// let tracker = SloTracker::default();
492    /// assert_eq!(tracker.sample_count(OperationType::DefinitionLookup), 0);
493    /// let start = tracker.start_operation(OperationType::DefinitionLookup);
494    /// tracker.record_operation_type(OperationType::DefinitionLookup, start, OperationResult::Success);
495    /// assert_eq!(tracker.sample_count(OperationType::DefinitionLookup), 1);
496    /// ```
497    pub fn sample_count(&self, operation_type: OperationType) -> usize {
498        let trackers = self.trackers.lock();
499        trackers.get(&operation_type).map_or(0, |t| t.samples.len())
500    }
501
502    /// Get the SLO configuration.
503    ///
504    /// # Returns
505    ///
506    /// The SLO configuration.
507    pub fn config(&self) -> &SloConfig {
508        &self.config
509    }
510
511    /// Reset all statistics.
512    ///
513    /// # Examples
514    ///
515    /// ```rust
516    /// use perl_workspace::slo::SloTracker;
517    ///
518    /// let tracker = SloTracker::default();
519    /// tracker.reset();
520    /// ```
521    pub fn reset(&self) {
522        let mut trackers = self.trackers.lock();
523        for tracker in trackers.values_mut() {
524            tracker.samples.clear();
525        }
526    }
527}
528
529impl Default for SloTracker {
530    fn default() -> Self {
531        Self::new(SloConfig::default())
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538
539    #[test]
540    fn test_slo_tracker_record() {
541        let tracker = SloTracker::default();
542        let start = tracker.start_operation(OperationType::DefinitionLookup);
543        tracker.record_operation_type(
544            OperationType::DefinitionLookup,
545            start,
546            OperationResult::Success,
547        );
548
549        let stats = tracker.statistics(OperationType::DefinitionLookup);
550        assert_eq!(stats.total_count, 1);
551        assert_eq!(stats.success_count, 1);
552    }
553
554    #[test]
555    fn test_slo_statistics() {
556        let tracker = SloTracker::default();
557
558        // Record some operations
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_eq!(stats.total_count, 10);
571        assert_eq!(stats.success_count, 10);
572        assert!(stats.p50_ms > 0);
573        assert!(stats.p95_ms > 0);
574    }
575
576    #[test]
577    fn test_slo_met() {
578        let tracker = SloTracker::default();
579
580        // Record fast operations (should meet SLO)
581        for _ in 0..10 {
582            let start = tracker.start_operation(OperationType::DefinitionLookup);
583            std::thread::sleep(Duration::from_millis(1));
584            tracker.record_operation_type(
585                OperationType::DefinitionLookup,
586                start,
587                OperationResult::Success,
588            );
589        }
590
591        let stats = tracker.statistics(OperationType::DefinitionLookup);
592        assert!(stats.slo_met);
593    }
594
595    #[test]
596    fn test_operation_type_name() {
597        assert_eq!(OperationType::DefinitionLookup.name(), "definition_lookup");
598        assert_eq!(OperationType::Completion.name(), "completion");
599    }
600
601    #[test]
602    fn test_slo_target_ms() {
603        let config = SloConfig::default();
604        assert_eq!(OperationType::DefinitionLookup.slo_target_ms(&config), 50);
605        assert_eq!(OperationType::Completion.slo_target_ms(&config), 100);
606    }
607
608    #[test]
609    fn test_operation_result() {
610        assert!(OperationResult::Success.is_success());
611        assert!(!OperationResult::Failure("error".to_string()).is_success());
612    }
613
614    #[test]
615    fn test_percentile() {
616        let values = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
617        assert_eq!(nearest_rank_percentile(&values, 50), 5); // Median
618        assert_eq!(nearest_rank_percentile(&values, 95), 10); // P95
619    }
620}