Skip to main content

ftui_runtime/
voi_telemetry.rs

1#![forbid(unsafe_code)]
2
3//! VOI debug telemetry snapshots for runtime introspection.
4
5use std::sync::{LazyLock, RwLock};
6
7use crate::voi_sampling::VoiSamplerSnapshot;
8
9static INLINE_AUTO_VOI_SNAPSHOT: LazyLock<RwLock<Option<VoiSamplerSnapshot>>> =
10    LazyLock::new(|| RwLock::new(None));
11
12#[cfg(test)]
13use std::sync::Mutex;
14
15// Global snapshot telemetry is shared state. In tests, we serialize snapshot
16// access to avoid flakiness under parallel test execution.
17#[cfg(test)]
18static TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
19
20/// Store the latest inline-auto VOI snapshot.
21pub fn set_inline_auto_voi_snapshot(snapshot: Option<VoiSamplerSnapshot>) {
22    #[cfg(test)]
23    let _lock = TEST_LOCK.lock().expect("test lock poisoned");
24
25    if let Ok(mut guard) = INLINE_AUTO_VOI_SNAPSHOT.write() {
26        *guard = snapshot;
27    }
28}
29
30/// Fetch the latest inline-auto VOI snapshot.
31#[must_use]
32pub fn inline_auto_voi_snapshot() -> Option<VoiSamplerSnapshot> {
33    #[cfg(test)]
34    let _lock = TEST_LOCK.lock().expect("test lock poisoned");
35
36    INLINE_AUTO_VOI_SNAPSHOT
37        .read()
38        .ok()
39        .and_then(|guard| guard.clone())
40}
41
42/// Clear any stored inline-auto VOI snapshot.
43pub fn clear_inline_auto_voi_snapshot() {
44    set_inline_auto_voi_snapshot(None);
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::voi_sampling::{VoiDecision, VoiLogEntry, VoiObservation, VoiSamplerSnapshot};
51
52    fn set_and_get_snapshot(snapshot: VoiSamplerSnapshot) -> VoiSamplerSnapshot {
53        let _lock = TEST_LOCK.lock().expect("test lock poisoned");
54        let mut guard = INLINE_AUTO_VOI_SNAPSHOT
55            .write()
56            .expect("snapshot lock poisoned");
57        *guard = Some(snapshot);
58        guard.clone().expect("snapshot should be present")
59    }
60
61    fn make_snapshot(captured_ms: u64) -> VoiSamplerSnapshot {
62        VoiSamplerSnapshot {
63            captured_ms,
64            alpha: 2.0,
65            beta: 18.0,
66            posterior_mean: 0.1,
67            posterior_variance: 0.004,
68            expected_variance_after: 0.003,
69            voi_gain: 0.5,
70            last_decision: None,
71            last_observation: None,
72            recent_logs: Vec::new(),
73        }
74    }
75
76    #[test]
77    fn initially_none() {
78        let _lock = TEST_LOCK.lock().expect("test lock poisoned");
79        let mut guard = INLINE_AUTO_VOI_SNAPSHOT
80            .write()
81            .expect("snapshot lock poisoned");
82        *guard = None;
83        assert!(guard.is_none());
84    }
85
86    #[test]
87    fn store_and_retrieve() {
88        let retrieved = set_and_get_snapshot(make_snapshot(1000));
89        assert_eq!(retrieved.captured_ms, 1000);
90        assert!((retrieved.alpha - 2.0).abs() < f64::EPSILON);
91        assert!((retrieved.posterior_mean - 0.1).abs() < f64::EPSILON);
92        clear_inline_auto_voi_snapshot();
93    }
94
95    #[test]
96    fn overwrite_replaces_previous() {
97        let _ = set_and_get_snapshot(make_snapshot(100));
98        let snap = set_and_get_snapshot(make_snapshot(200));
99        assert_eq!(snap.captured_ms, 200);
100        clear_inline_auto_voi_snapshot();
101    }
102
103    #[test]
104    fn clear_removes_snapshot() {
105        let _lock = TEST_LOCK.lock().expect("test lock poisoned");
106        let mut guard = INLINE_AUTO_VOI_SNAPSHOT
107            .write()
108            .expect("snapshot lock poisoned");
109        *guard = Some(make_snapshot(50));
110        *guard = None;
111        assert!(guard.is_none());
112    }
113
114    #[test]
115    fn set_none_clears() {
116        let _lock = TEST_LOCK.lock().expect("test lock poisoned");
117        let mut guard = INLINE_AUTO_VOI_SNAPSHOT
118            .write()
119            .expect("snapshot lock poisoned");
120        *guard = Some(make_snapshot(77));
121        *guard = None;
122        assert!(guard.is_none());
123    }
124
125    #[test]
126    fn snapshot_with_decision() {
127        let mut snap = make_snapshot(500);
128        snap.last_decision = Some(VoiDecision {
129            event_idx: 42,
130            should_sample: true,
131            forced_by_interval: false,
132            blocked_by_min_interval: false,
133            voi_gain: 1.2,
134            score: 0.8,
135            cost: 0.3,
136            log_bayes_factor: 2.5,
137            posterior_mean: 0.1,
138            posterior_variance: 0.004,
139            e_value: 15.0,
140            e_threshold: 20.0,
141            boundary_score: 0.7,
142            events_since_sample: 10,
143            time_since_sample_ms: 500.0,
144            reason: "voi_gain",
145        });
146        let retrieved = set_and_get_snapshot(snap);
147        let decision = retrieved.last_decision.as_ref().unwrap();
148        assert_eq!(decision.event_idx, 42);
149        assert!(decision.should_sample);
150        assert!((decision.voi_gain - 1.2).abs() < f64::EPSILON);
151        clear_inline_auto_voi_snapshot();
152    }
153
154    #[test]
155    fn snapshot_with_observation() {
156        let mut snap = make_snapshot(600);
157        snap.last_observation = Some(VoiObservation {
158            event_idx: 100,
159            sample_idx: 5,
160            violated: true,
161            posterior_mean: 0.15,
162            posterior_variance: 0.003,
163            alpha: 3.0,
164            beta: 17.0,
165            e_value: 25.0,
166            e_threshold: 20.0,
167        });
168        let retrieved = set_and_get_snapshot(snap);
169        let obs = retrieved.last_observation.as_ref().unwrap();
170        assert_eq!(obs.event_idx, 100);
171        assert!(obs.violated);
172        assert!((obs.alpha - 3.0).abs() < f64::EPSILON);
173        clear_inline_auto_voi_snapshot();
174    }
175
176    #[test]
177    fn snapshot_with_recent_logs() {
178        let mut snap = make_snapshot(700);
179        snap.recent_logs = vec![
180            VoiLogEntry::Decision(VoiDecision {
181                event_idx: 1,
182                should_sample: false,
183                forced_by_interval: false,
184                blocked_by_min_interval: true,
185                voi_gain: 0.1,
186                score: 0.2,
187                cost: 0.5,
188                log_bayes_factor: -1.0,
189                posterior_mean: 0.05,
190                posterior_variance: 0.002,
191                e_value: 0.8,
192                e_threshold: 20.0,
193                boundary_score: 0.3,
194                events_since_sample: 2,
195                time_since_sample_ms: 50.0,
196                reason: "blocked_min_interval",
197            }),
198            VoiLogEntry::Observation(VoiObservation {
199                event_idx: 2,
200                sample_idx: 1,
201                violated: false,
202                posterior_mean: 0.06,
203                posterior_variance: 0.002,
204                alpha: 2.0,
205                beta: 18.0,
206                e_value: 1.0,
207                e_threshold: 20.0,
208            }),
209        ];
210        let retrieved = set_and_get_snapshot(snap);
211        assert_eq!(retrieved.recent_logs.len(), 2);
212        clear_inline_auto_voi_snapshot();
213    }
214}