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}