web_audio_api/
capacity.rs1use 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#[derive(Clone, Debug)]
12pub struct AudioRenderCapacityOptions {
13 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#[derive(Clone, Debug)]
27pub struct AudioRenderCapacityEvent {
28 pub timestamp: f64,
30 pub average_load: f64,
32 pub peak_load: f64,
34 pub underrun_ratio: f64,
36 pub event: Event,
38}
39
40impl AudioRenderCapacityEvent {
41 fn new(timestamp: f64, average_load: f64, peak_load: f64, underrun_ratio: f64) -> Self {
42 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#[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 #[allow(clippy::missing_panics_doc)]
95 pub fn start(&self, options: AudioRenderCapacityOptions) {
96 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 #[allow(clippy::missing_panics_doc)]
133 pub fn stop(&self) {
134 if let Some(stop_send) = self.stop_send.lock().unwrap().take() {
136 let _ = stop_send.send(());
137 }
138 }
139
140 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 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!(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}