use parking_lot::Mutex;
use perl_parser_core::percentile::nearest_rank_percentile;
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub struct SloConfig {
pub index_init_p95_ms: u64,
pub incremental_update_p95_ms: u64,
pub definition_lookup_p95_ms: u64,
pub completion_p95_ms: u64,
pub hover_p95_ms: u64,
pub max_error_rate: f64,
pub sample_window_size: usize,
}
impl Default for SloConfig {
fn default() -> Self {
Self {
index_init_p95_ms: 5000, incremental_update_p95_ms: 100, definition_lookup_p95_ms: 50, completion_p95_ms: 100, hover_p95_ms: 50, max_error_rate: 0.01, sample_window_size: 1000,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum OperationType {
IndexInitialization,
IncrementalUpdate,
DefinitionLookup,
Completion,
Hover,
FindReferences,
WorkspaceSymbols,
FileIndexing,
}
impl OperationType {
pub fn slo_target_ms(&self, config: &SloConfig) -> u64 {
match self {
OperationType::IndexInitialization => config.index_init_p95_ms,
OperationType::IncrementalUpdate => config.incremental_update_p95_ms,
OperationType::DefinitionLookup => config.definition_lookup_p95_ms,
OperationType::Completion => config.completion_p95_ms,
OperationType::Hover => config.hover_p95_ms,
OperationType::FindReferences => config.definition_lookup_p95_ms, OperationType::WorkspaceSymbols => config.definition_lookup_p95_ms, OperationType::FileIndexing => config.incremental_update_p95_ms, }
}
pub fn name(&self) -> &'static str {
match self {
OperationType::IndexInitialization => "index_initialization",
OperationType::IncrementalUpdate => "incremental_update",
OperationType::DefinitionLookup => "definition_lookup",
OperationType::Completion => "completion",
OperationType::Hover => "hover",
OperationType::FindReferences => "find_references",
OperationType::WorkspaceSymbols => "workspace_symbols",
OperationType::FileIndexing => "file_indexing",
}
}
}
#[derive(Clone, Debug)]
pub enum OperationResult {
Success,
Failure(String),
}
impl OperationResult {
pub fn is_success(&self) -> bool {
matches!(self, OperationResult::Success)
}
}
impl<T, E> From<Result<T, E>> for OperationResult
where
E: std::fmt::Display,
{
fn from(result: Result<T, E>) -> Self {
match result {
Ok(_) => OperationResult::Success,
Err(e) => OperationResult::Failure(e.to_string()),
}
}
}
#[derive(Clone, Debug)]
struct LatencySample {
duration: Duration,
success: bool,
}
#[derive(Clone, Debug)]
pub struct SloStatistics {
pub total_count: u64,
pub success_count: u64,
pub failure_count: u64,
pub error_rate: f64,
pub p50_ms: u64,
pub p95_ms: u64,
pub p99_ms: u64,
pub avg_ms: f64,
pub slo_met: bool,
}
impl Default for SloStatistics {
fn default() -> Self {
Self {
total_count: 0,
success_count: 0,
failure_count: 0,
error_rate: 0.0,
p50_ms: 0,
p95_ms: 0,
p99_ms: 0,
avg_ms: 0.0,
slo_met: true,
}
}
}
#[derive(Debug)]
struct OperationSloTracker {
_operation_type: OperationType,
samples: VecDeque<LatencySample>,
slo_target_ms: u64,
max_error_rate: f64,
max_samples: usize,
}
impl OperationSloTracker {
fn new(operation_type: OperationType, config: &SloConfig) -> Self {
Self {
_operation_type: operation_type,
samples: VecDeque::with_capacity(config.sample_window_size),
slo_target_ms: operation_type.slo_target_ms(config),
max_error_rate: config.max_error_rate,
max_samples: config.sample_window_size,
}
}
fn record(&mut self, duration: Duration, result: OperationResult) {
let success = result.is_success();
let sample = LatencySample { duration, success };
if self.samples.len() >= self.max_samples {
self.samples.pop_front();
}
self.samples.push_back(sample);
}
fn statistics(&self) -> SloStatistics {
if self.samples.is_empty() {
return SloStatistics::default();
}
let total_count = self.samples.len() as u64;
let success_count = self.samples.iter().filter(|s| s.success).count() as u64;
let failure_count = total_count - success_count;
let error_rate =
if total_count > 0 { failure_count as f64 / total_count as f64 } else { 0.0 };
let mut durations_ms: Vec<u64> =
self.samples.iter().map(|s| s.duration.as_millis() as u64).collect();
durations_ms.sort_unstable();
let p50_ms = nearest_rank_percentile(&durations_ms, 50);
let p95_ms = nearest_rank_percentile(&durations_ms, 95);
let p99_ms = nearest_rank_percentile(&durations_ms, 99);
let avg_ms =
durations_ms.iter().map(|&d| d as f64).sum::<f64>() / durations_ms.len() as f64;
let slo_met = p95_ms <= self.slo_target_ms && error_rate <= self.max_error_rate;
SloStatistics {
total_count,
success_count,
failure_count,
error_rate,
p50_ms,
p95_ms,
p99_ms,
avg_ms,
slo_met,
}
}
}
pub struct SloTracker {
config: SloConfig,
trackers: Arc<Mutex<std::collections::HashMap<OperationType, OperationSloTracker>>>,
}
impl SloTracker {
pub fn new(config: SloConfig) -> Self {
let mut trackers = std::collections::HashMap::new();
for op_type in [
OperationType::IndexInitialization,
OperationType::IncrementalUpdate,
OperationType::DefinitionLookup,
OperationType::Completion,
OperationType::Hover,
OperationType::FindReferences,
OperationType::WorkspaceSymbols,
OperationType::FileIndexing,
] {
trackers.insert(op_type, OperationSloTracker::new(op_type, &config));
}
Self { config, trackers: Arc::new(Mutex::new(trackers)) }
}
pub fn start_operation(&self, _operation_type: OperationType) -> Instant {
Instant::now()
}
#[deprecated(
since = "0.13.0",
note = "records to every operation-type tracker at once — use record_operation_type instead"
)]
pub fn record_operation(&self, start: Instant, result: OperationResult) {
let duration = start.elapsed();
let mut trackers = self.trackers.lock();
for tracker in trackers.values_mut() {
tracker.record(duration, result.clone());
}
}
pub fn record_operation_type(
&self,
operation_type: OperationType,
start: Instant,
result: OperationResult,
) {
let duration = start.elapsed();
let mut trackers = self.trackers.lock();
if let Some(tracker) = trackers.get_mut(&operation_type) {
tracker.record(duration, result);
}
}
pub fn statistics(&self, operation_type: OperationType) -> SloStatistics {
let trackers = self.trackers.lock();
trackers.get(&operation_type).map(|t| t.statistics()).unwrap_or_default()
}
pub fn all_statistics(&self) -> std::collections::HashMap<OperationType, SloStatistics> {
let trackers = self.trackers.lock();
trackers.iter().map(|(op_type, tracker)| (*op_type, tracker.statistics())).collect()
}
pub fn all_slos_met(&self) -> bool {
let trackers = self.trackers.lock();
trackers.values().all(|t| t.statistics().slo_met)
}
pub fn sample_count(&self, operation_type: OperationType) -> usize {
let trackers = self.trackers.lock();
trackers.get(&operation_type).map_or(0, |t| t.samples.len())
}
pub fn config(&self) -> &SloConfig {
&self.config
}
pub fn reset(&self) {
let mut trackers = self.trackers.lock();
for tracker in trackers.values_mut() {
tracker.samples.clear();
}
}
}
impl Default for SloTracker {
fn default() -> Self {
Self::new(SloConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slo_tracker_record() {
let tracker = SloTracker::default();
let start = tracker.start_operation(OperationType::DefinitionLookup);
tracker.record_operation_type(
OperationType::DefinitionLookup,
start,
OperationResult::Success,
);
let stats = tracker.statistics(OperationType::DefinitionLookup);
assert_eq!(stats.total_count, 1);
assert_eq!(stats.success_count, 1);
}
#[test]
fn test_slo_statistics() {
let tracker = SloTracker::default();
for _ in 0..10 {
let start = tracker.start_operation(OperationType::DefinitionLookup);
std::thread::sleep(Duration::from_millis(1));
tracker.record_operation_type(
OperationType::DefinitionLookup,
start,
OperationResult::Success,
);
}
let stats = tracker.statistics(OperationType::DefinitionLookup);
assert_eq!(stats.total_count, 10);
assert_eq!(stats.success_count, 10);
assert!(stats.p50_ms > 0);
assert!(stats.p95_ms > 0);
}
#[test]
fn test_slo_met() {
let tracker = SloTracker::default();
for _ in 0..10 {
let start = tracker.start_operation(OperationType::DefinitionLookup);
std::thread::sleep(Duration::from_millis(1));
tracker.record_operation_type(
OperationType::DefinitionLookup,
start,
OperationResult::Success,
);
}
let stats = tracker.statistics(OperationType::DefinitionLookup);
assert!(stats.slo_met);
}
#[test]
fn test_operation_type_name() {
assert_eq!(OperationType::DefinitionLookup.name(), "definition_lookup");
assert_eq!(OperationType::Completion.name(), "completion");
}
#[test]
fn test_slo_target_ms() {
let config = SloConfig::default();
assert_eq!(OperationType::DefinitionLookup.slo_target_ms(&config), 50);
assert_eq!(OperationType::Completion.slo_target_ms(&config), 100);
}
#[test]
fn test_operation_result() {
assert!(OperationResult::Success.is_success());
assert!(!OperationResult::Failure("error".to_string()).is_success());
}
#[test]
fn test_percentile() {
let values = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
assert_eq!(nearest_rank_percentile(&values, 50), 5); assert_eq!(nearest_rank_percentile(&values, 95), 10); }
}