Skip to main content

web_audio_api/
capacity.rs

1use crossbeam_channel::Sender;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use crate::context::{BaseAudioContext, ConcreteBaseAudioContext};
6use crate::events::{EventDispatch, EventHandler, EventPayload, EventType};
7use crate::stats::{AudioStats, AudioStatsSnapshot};
8use crate::Event;
9
10/// Options for constructing an `AudioRenderCapacity`
11#[derive(Clone, Debug)]
12pub struct AudioRenderCapacityOptions {
13    /// An update interval (in seconds) for dispatching [`AudioRenderCapacityEvent`]s
14    pub update_interval: f64,
15}
16
17impl Default for AudioRenderCapacityOptions {
18    fn default() -> Self {
19        Self {
20            update_interval: 1.,
21        }
22    }
23}
24
25/// Performance metrics of the rendering thread
26#[derive(Clone, Debug)]
27pub struct AudioRenderCapacityEvent {
28    /// The start time of the data collection period in terms of the associated AudioContext's currentTime
29    pub timestamp: f64,
30    /// An average of collected load values over the given update interval
31    pub average_load: f64,
32    /// A maximum value from collected load values over the given update interval.
33    pub peak_load: f64,
34    /// A ratio between the number of buffer underruns and the total number of system-level audio callbacks over the given update interval.
35    pub underrun_ratio: f64,
36    /// Inherits from this base Event
37    pub event: Event,
38}
39
40impl AudioRenderCapacityEvent {
41    fn new(timestamp: f64, average_load: f64, peak_load: f64, underrun_ratio: f64) -> Self {
42        // We are limiting the precision here conform
43        // https://webaudio.github.io/web-audio-api/#dom-audiorendercapacityevent-averageload
44        Self {
45            timestamp,
46            average_load: (average_load * 100.).round() / 100.,
47            peak_load: (peak_load * 100.).round() / 100.,
48            underrun_ratio: (underrun_ratio * 100.).ceil() / 100.,
49            event: Event {
50                type_: "AudioRenderCapacityEvent",
51            },
52        }
53    }
54}
55
56/// Provider for rendering performance metrics
57///
58/// A load value is computed for each system-level audio callback, by dividing its execution
59/// duration by the system-level audio callback buffer size divided by the sample rate.
60///
61/// Ideally the load value is below 1.0, meaning that it took less time to render the audio than it
62/// took to play it out. An audio buffer underrun happens when this load value is greater than 1.0: the
63/// system could not render audio fast enough for real-time.
64#[derive(Clone)]
65pub struct AudioRenderCapacity {
66    context: ConcreteBaseAudioContext,
67    stats: AudioStats,
68    stop_send: Arc<Mutex<Option<Sender<()>>>>,
69}
70
71impl std::fmt::Debug for AudioRenderCapacity {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.debug_struct("AudioRenderCapacity")
74            .field(
75                "context",
76                &format!("BaseAudioContext@{}", self.context.address()),
77            )
78            .finish_non_exhaustive()
79    }
80}
81
82impl AudioRenderCapacity {
83    pub(crate) fn new(context: ConcreteBaseAudioContext, stats: AudioStats) -> Self {
84        let stop_send = Arc::new(Mutex::new(None));
85
86        Self {
87            context,
88            stats,
89            stop_send,
90        }
91    }
92
93    /// Start metric collection and analysis
94    #[allow(clippy::missing_panics_doc)]
95    pub fn start(&self, options: AudioRenderCapacityOptions) {
96        // stop current metric collection, if any
97        self.stop();
98
99        let (stop_send, stop_recv) = crossbeam_channel::bounded(0);
100        *self.stop_send.lock().unwrap() = Some(stop_send);
101
102        let mut timestamp: f64 = self.context.current_time();
103        let update_interval = Duration::from_secs_f64(options.update_interval.max(0.001));
104        let base_context = self.context.clone();
105        let stats = self.stats.clone();
106        let mut previous = stats.snapshot();
107        stats.take_peak_load();
108        std::thread::spawn(move || loop {
109            if stop_recv.recv_timeout(update_interval).is_ok() {
110                return;
111            }
112
113            let next = stats.snapshot();
114            if next.callback_count == previous.callback_count {
115                continue;
116            }
117
118            let peak_load = stats.take_peak_load();
119            let event = render_capacity_event(timestamp, previous, next, peak_load);
120
121            let send_result = base_context.send_event(EventDispatch::render_capacity(event));
122            if send_result.is_err() {
123                break;
124            };
125
126            previous = next;
127            timestamp = base_context.current_time();
128        });
129    }
130
131    /// Stop metric collection and analysis
132    #[allow(clippy::missing_panics_doc)]
133    pub fn stop(&self) {
134        // halt callback thread
135        if let Some(stop_send) = self.stop_send.lock().unwrap().take() {
136            let _ = stop_send.send(());
137        }
138    }
139
140    /// The EventHandler for [`AudioRenderCapacityEvent`].
141    ///
142    /// Only a single event handler is active at any time. Calling this method multiple times will
143    /// override the previous event handler.
144    pub fn set_onupdate<F: FnMut(AudioRenderCapacityEvent) + Send + 'static>(
145        &self,
146        mut callback: F,
147    ) {
148        let callback = move |v| match v {
149            EventPayload::RenderCapacity(v) => callback(v),
150            _ => unreachable!(),
151        };
152
153        self.context.set_event_handler(
154            EventType::RenderCapacity,
155            EventHandler::Multiple(Box::new(callback)),
156        );
157    }
158
159    /// Unset the EventHandler for [`AudioRenderCapacityEvent`].
160    pub fn clear_onupdate(&self) {
161        self.context.clear_event_handler(EventType::RenderCapacity);
162    }
163}
164
165fn render_capacity_event(
166    timestamp: f64,
167    previous: AudioStatsSnapshot,
168    next: AudioStatsSnapshot,
169    peak_load: f64,
170) -> AudioRenderCapacityEvent {
171    let callback_count = next
172        .callback_count
173        .saturating_sub(previous.callback_count)
174        .max(1);
175    let render_duration = next
176        .render_duration_ns_total
177        .saturating_sub(previous.render_duration_ns_total);
178    let callback_budget = next
179        .callback_budget_ns_total
180        .saturating_sub(previous.callback_budget_ns_total);
181    let underruns = next.underrun_count.saturating_sub(previous.underrun_count);
182
183    let average_load = if callback_budget == 0 {
184        0.
185    } else {
186        render_duration as f64 / callback_budget as f64
187    };
188
189    AudioRenderCapacityEvent::new(
190        timestamp,
191        average_load,
192        peak_load,
193        underruns as f64 / callback_count as f64,
194    )
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::context::{AudioContext, AudioContextOptions};
201
202    #[test]
203    fn test_same_instance() {
204        let options = AudioContextOptions {
205            sink_id: "none".into(),
206            ..AudioContextOptions::default()
207        };
208        let context = AudioContext::new(options);
209
210        let rc1 = context.render_capacity();
211        let rc2 = context.render_capacity();
212        let rc3 = rc2.clone();
213
214        // assert all items are actually the same instance
215        assert!(Arc::ptr_eq(&rc1.stop_send, &rc2.stop_send));
216        assert!(Arc::ptr_eq(&rc1.stop_send, &rc3.stop_send));
217    }
218
219    #[test]
220    fn test_stop_when_not_running() {
221        let options = AudioContextOptions {
222            sink_id: "none".into(),
223            ..AudioContextOptions::default()
224        };
225        let context = AudioContext::new(options);
226
227        let rc = context.render_capacity();
228        rc.stop();
229    }
230
231    #[test]
232    fn test_render_capacity() {
233        let options = AudioContextOptions {
234            sink_id: "none".into(),
235            ..AudioContextOptions::default()
236        };
237        let context = AudioContext::new(options);
238
239        let rc = context.render_capacity();
240        let (send, recv) = crossbeam_channel::bounded(1);
241        rc.set_onupdate(move |e| send.send(e).unwrap());
242        rc.start(AudioRenderCapacityOptions {
243            update_interval: 0.05,
244        });
245        let event = recv.recv().unwrap();
246
247        assert!(event.timestamp >= 0.);
248        assert!(event.average_load >= 0.);
249        assert!(event.peak_load >= 0.);
250        assert!(event.underrun_ratio >= 0.);
251
252        assert!(event.timestamp.is_finite());
253        assert!(event.average_load.is_finite());
254        assert!(event.peak_load.is_finite());
255        assert!(event.underrun_ratio.is_finite());
256
257        assert_eq!(event.event.type_, "AudioRenderCapacityEvent");
258    }
259
260    #[test]
261    fn test_render_capacity_stops_on_close() {
262        let options = AudioContextOptions {
263            sink_id: "none".into(),
264            ..AudioContextOptions::default()
265        };
266        let context = AudioContext::new(options);
267
268        let rc = context.render_capacity();
269        let (send, recv) = crossbeam_channel::unbounded();
270        rc.set_onupdate(move |e| send.send(e).unwrap());
271        rc.start(AudioRenderCapacityOptions {
272            update_interval: 0.01,
273        });
274
275        recv.recv().unwrap();
276        while recv.try_recv().is_ok() {}
277
278        context.close_sync();
279        std::thread::sleep(std::time::Duration::from_millis(100));
280
281        assert_eq!(recv.try_iter().count(), 0);
282    }
283}