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}