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        clear_inline_auto_voi_snapshot();
79        assert!(inline_auto_voi_snapshot().is_none());
80    }
81
82    #[test]
83    fn store_and_retrieve() {
84        let retrieved = set_and_get_snapshot(make_snapshot(1000));
85        assert_eq!(retrieved.captured_ms, 1000);
86        assert!((retrieved.alpha - 2.0).abs() < f64::EPSILON);
87        assert!((retrieved.posterior_mean - 0.1).abs() < f64::EPSILON);
88        clear_inline_auto_voi_snapshot();
89    }
90
91    #[test]
92    fn overwrite_replaces_previous() {
93        let _ = set_and_get_snapshot(make_snapshot(100));
94        let snap = set_and_get_snapshot(make_snapshot(200));
95        assert_eq!(snap.captured_ms, 200);
96        clear_inline_auto_voi_snapshot();
97    }
98
99    #[test]
100    fn clear_removes_snapshot() {
101        set_inline_auto_voi_snapshot(Some(make_snapshot(50)));
102        clear_inline_auto_voi_snapshot();
103        assert!(inline_auto_voi_snapshot().is_none());
104    }
105
106    #[test]
107    fn set_none_clears() {
108        set_inline_auto_voi_snapshot(Some(make_snapshot(77)));
109        set_inline_auto_voi_snapshot(None);
110        // Global state may be set by concurrent tests; verify set(None)
111        // at least doesn't panic and the API is callable.
112        let _ = inline_auto_voi_snapshot();
113    }
114
115    #[test]
116    fn snapshot_with_decision() {
117        let mut snap = make_snapshot(500);
118        snap.last_decision = Some(VoiDecision {
119            event_idx: 42,
120            should_sample: true,
121            forced_by_interval: false,
122            blocked_by_min_interval: false,
123            voi_gain: 1.2,
124            score: 0.8,
125            cost: 0.3,
126            log_bayes_factor: 2.5,
127            posterior_mean: 0.1,
128            posterior_variance: 0.004,
129            e_value: 15.0,
130            e_threshold: 20.0,
131            boundary_score: 0.7,
132            events_since_sample: 10,
133            time_since_sample_ms: 500.0,
134            reason: "voi_gain",
135        });
136        let retrieved = set_and_get_snapshot(snap);
137        let decision = retrieved.last_decision.as_ref().unwrap();
138        assert_eq!(decision.event_idx, 42);
139        assert!(decision.should_sample);
140        assert!((decision.voi_gain - 1.2).abs() < f64::EPSILON);
141        clear_inline_auto_voi_snapshot();
142    }
143
144    #[test]
145    fn snapshot_with_observation() {
146        let mut snap = make_snapshot(600);
147        snap.last_observation = Some(VoiObservation {
148            event_idx: 100,
149            sample_idx: 5,
150            violated: true,
151            posterior_mean: 0.15,
152            posterior_variance: 0.003,
153            alpha: 3.0,
154            beta: 17.0,
155            e_value: 25.0,
156            e_threshold: 20.0,
157        });
158        let retrieved = set_and_get_snapshot(snap);
159        let obs = retrieved.last_observation.as_ref().unwrap();
160        assert_eq!(obs.event_idx, 100);
161        assert!(obs.violated);
162        assert!((obs.alpha - 3.0).abs() < f64::EPSILON);
163        clear_inline_auto_voi_snapshot();
164    }
165
166    #[test]
167    fn snapshot_with_recent_logs() {
168        let mut snap = make_snapshot(700);
169        snap.recent_logs = vec![
170            VoiLogEntry::Decision(VoiDecision {
171                event_idx: 1,
172                should_sample: false,
173                forced_by_interval: false,
174                blocked_by_min_interval: true,
175                voi_gain: 0.1,
176                score: 0.2,
177                cost: 0.5,
178                log_bayes_factor: -1.0,
179                posterior_mean: 0.05,
180                posterior_variance: 0.002,
181                e_value: 0.8,
182                e_threshold: 20.0,
183                boundary_score: 0.3,
184                events_since_sample: 2,
185                time_since_sample_ms: 50.0,
186                reason: "blocked_min_interval",
187            }),
188            VoiLogEntry::Observation(VoiObservation {
189                event_idx: 2,
190                sample_idx: 1,
191                violated: false,
192                posterior_mean: 0.06,
193                posterior_variance: 0.002,
194                alpha: 2.0,
195                beta: 18.0,
196                e_value: 1.0,
197                e_threshold: 20.0,
198            }),
199        ];
200        let retrieved = set_and_get_snapshot(snap);
201        assert_eq!(retrieved.recent_logs.len(), 2);
202        clear_inline_auto_voi_snapshot();
203    }
204}