flexaudio_core/
diagnostics.rs1use crate::loss::CaptureRejection;
3use crate::{AudioLoss, Result};
4use std::num::NonZeroU64;
5use std::sync::{
6 atomic::{AtomicU64, Ordering},
7 Arc,
8};
9
10#[cfg(not(target_has_atomic = "64"))]
11compile_error!("capture diagnostics require lock-free 64-bit atomics");
12
13#[derive(Clone)]
15pub struct CaptureDiagnostics {
16 inner: Arc<Pending>,
17}
18struct Pending {
19 summaries: [AtomicU64; 3],
20 rate: u32,
21 channels: u16,
22}
23impl CaptureDiagnostics {
24 pub fn new(rate: u32, channels: u16) -> Self {
26 Self {
27 inner: Arc::new(Pending {
28 summaries: std::array::from_fn(|_| AtomicU64::new(0)),
29 rate,
30 channels,
31 }),
32 }
33 }
34 fn record(&self, index: usize, samples: Option<NonZeroU64>) {
35 let summary = &self.inner.summaries[index];
36 let Some(samples) = samples else {
37 summary.store(u64::MAX, Ordering::Release);
38 return;
39 };
40 let mut previous = summary.load(Ordering::Acquire);
41 for _ in 0..8 {
42 if previous == u64::MAX {
43 return;
44 }
45 let next = previous.saturating_add(samples.get());
46 match summary.compare_exchange_weak(previous, next, Ordering::AcqRel, Ordering::Acquire)
47 {
48 Ok(_) => return,
49 Err(current) => previous = current,
50 }
51 }
52 summary.store(u64::MAX, Ordering::Release);
54 }
55 pub fn record_corrupt_buffer(&self, samples: Option<NonZeroU64>) {
57 self.record(0, samples);
58 }
59 pub fn record_malformed_buffer(&self, samples: Option<NonZeroU64>) {
61 self.record(1, samples);
62 }
63 pub fn record_callback_rejected(&self, samples: Option<NonZeroU64>) {
65 self.record(2, samples);
66 }
67 pub fn drain(&self) -> Result<Vec<AudioLoss>> {
69 let mut losses = Vec::new();
70 for (summary, reason) in self.inner.summaries.iter().zip([
71 CaptureRejection::CorruptBuffer,
72 CaptureRejection::MalformedBuffer,
73 CaptureRejection::CallbackRejected,
74 ]) {
75 let pending = summary.swap(0, Ordering::AcqRel);
76 if pending != 0 {
77 losses.push(AudioLoss::rejected(
78 reason,
79 if pending == u64::MAX {
80 None
81 } else {
82 NonZeroU64::new(pending)
83 },
84 self.inner.rate,
85 self.inner.channels,
86 )?);
87 }
88 }
89 Ok(losses)
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 #[test]
97 fn known_unknown_saturated_and_final_drain_are_observable_once() {
98 let diagnostics = CaptureDiagnostics::new(48_000, 2);
99 diagnostics.record_corrupt_buffer(NonZeroU64::new(100));
100 diagnostics.record_corrupt_buffer(NonZeroU64::new(100));
101 diagnostics.record_malformed_buffer(None);
102 diagnostics.record_malformed_buffer(NonZeroU64::new(7));
103 diagnostics.record_callback_rejected(NonZeroU64::new(u64::MAX - 1));
104 diagnostics.record_callback_rejected(NonZeroU64::new(2));
105 let losses = diagnostics.drain().unwrap();
106 assert_eq!(losses.len(), 3);
107 assert_eq!(losses[0].samples().unwrap().get(), 200);
108 assert_eq!(losses[1].samples(), None);
109 assert_eq!(losses[2].samples(), None);
110 for loss in &losses {
111 assert_eq!((loss.sample_rate(), loss.channels()), (48_000, 2));
112 }
113 assert!(diagnostics.drain().unwrap().is_empty());
114 }
115 #[test]
116 fn concurrent_recording_preserves_every_observation() {
117 let diagnostics = CaptureDiagnostics::new(48_000, 2);
118 let handles: Vec<_> = (0..4)
119 .map(|_| {
120 let diagnostics = diagnostics.clone();
121 std::thread::spawn(move || {
122 for _ in 0..1_000 {
123 diagnostics.record_corrupt_buffer(NonZeroU64::new(1));
124 }
125 })
126 })
127 .collect();
128 for handle in handles {
129 handle.join().unwrap();
130 }
131 let loss = diagnostics.drain().unwrap()[0];
132 assert!(loss.samples().is_none_or(|samples| samples.get() == 4_000));
133 assert!(diagnostics.drain().unwrap().is_empty());
134 }
135}