Skip to main content

lsp_max/primitives/
rule_latency.rs

1//! Per-rule latency tracking for dynamic EvalBudget reclassification.
2//!
3//! When a Sync-mode rule exceeds 50 ms on 3 consecutive evaluations, it is
4//! promoted to Background for the remainder of the session.
5
6use std::collections::VecDeque;
7use std::time::Duration;
8
9const WINDOW: usize = 3;
10const THRESHOLD: Duration = Duration::from_millis(50);
11
12/// Tracks the last `WINDOW` (3) consecutive execution durations for a Sync rule.
13#[derive(Debug, Default)]
14pub struct RuleLatencyTracker {
15    /// The last up-to-3 observed durations, oldest-first.
16    pub recent_durations: VecDeque<Duration>,
17    /// True once 3 consecutive evaluations all exceeded the 50 ms threshold.
18    pub reclassified: bool,
19}
20
21impl RuleLatencyTracker {
22    /// Record a new observation. Returns `true` on the first reclassification.
23    pub fn record(&mut self, duration: Duration) -> bool {
24        if self.reclassified {
25            return false;
26        }
27        if self.recent_durations.len() >= WINDOW {
28            self.recent_durations.pop_front();
29        }
30        self.recent_durations.push_back(duration);
31
32        if self.recent_durations.len() == WINDOW
33            && self.recent_durations.iter().all(|d| *d > THRESHOLD)
34        {
35            self.reclassified = true;
36            return true;
37        }
38        false
39    }
40
41    #[inline]
42    /// Returns `true` if this rule has been promoted to Background.
43    pub fn is_reclassified(&self) -> bool {
44        self.reclassified
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn reclassifies_after_three_slow_evaluations() {
54        let mut t = RuleLatencyTracker::default();
55        let slow = Duration::from_millis(60);
56        assert!(!t.record(slow));
57        assert!(!t.record(slow));
58        assert!(t.record(slow)); // 3rd slow → reclassified
59        assert!(t.is_reclassified());
60        assert!(!t.record(slow)); // already reclassified; returns false
61    }
62
63    #[test]
64    fn does_not_reclassify_on_mixed_durations() {
65        let mut t = RuleLatencyTracker::default();
66        let fast = Duration::from_millis(10);
67        let slow = Duration::from_millis(60);
68        t.record(slow);
69        t.record(fast); // resets window
70        t.record(slow);
71        assert!(!t.is_reclassified());
72    }
73}