1use std::collections::{BTreeMap, HashMap};
10use std::task::Poll;
11
12use moq_net::kio::{self, Pending, Waiter};
13use moq_net::stats::{Presence, Role, Tier, Traffic};
14use moq_net::track::Subscribing;
15use moq_net::{PathOwned, origin};
16
17use crate::{Result, SessionsFrame, TrafficFrame, parse_node_path, sessions_track, traffic_track};
18
19#[derive(Debug, Clone)]
27#[non_exhaustive]
28pub struct Config {
29 pub prefix: PathOwned,
32 pub depth: usize,
36 pub compression: bool,
40}
41
42impl Config {
43 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn with_prefix(mut self, prefix: impl Into<PathOwned>) -> Self {
51 self.prefix = prefix.into();
52 self
53 }
54
55 pub fn with_depth(mut self, depth: usize) -> Self {
57 self.depth = depth;
58 self
59 }
60
61 pub fn with_compression(mut self, compression: bool) -> Self {
63 self.compression = compression;
64 self
65 }
66}
67
68impl Default for Config {
69 fn default() -> Self {
70 Self {
71 prefix: PathOwned::from(".stats"),
72 depth: 0,
73 compression: false,
74 }
75 }
76}
77
78pub struct Consumer {
88 origin: origin::Consumer,
89 config: Config,
90}
91
92impl Consumer {
93 pub fn new(origin: origin::Consumer, config: Config) -> Self {
96 Self { origin, config }
97 }
98
99 pub fn traffic(&self, tier: &Tier, role: Role) -> TrafficConsumer {
103 let name = traffic_track(tier, role, self.config.compression);
104 TrafficConsumer {
105 inner: Merged::new(self.origin.announced(), &self.config, name),
106 }
107 }
108
109 pub fn sessions(&self, tier: &Tier) -> SessionsConsumer {
111 let name = sessions_track(tier, self.config.compression);
112 SessionsConsumer {
113 inner: Merged::new(self.origin.announced(), &self.config, name),
114 }
115 }
116}
117
118pub struct TrafficConsumer {
122 inner: Merged<Traffic>,
123}
124
125impl TrafficConsumer {
126 pub async fn next(&mut self) -> Result<Option<TrafficFrame>> {
129 kio::wait(|waiter| self.inner.poll_next(waiter)).await
130 }
131}
132
133pub struct SessionsConsumer {
136 inner: Merged<Presence>,
137}
138
139impl SessionsConsumer {
140 pub async fn next(&mut self) -> Result<Option<SessionsFrame>> {
142 kio::wait(|waiter| self.inner.poll_next(waiter)).await
143 }
144}
145
146trait Mergeable: serde::de::DeserializeOwned + Default + Copy + 'static {
148 fn merge(acc: &mut Self, other: Self);
150}
151
152impl Mergeable for Traffic {
153 fn merge(acc: &mut Self, other: Self) {
154 acc.add(other);
155 }
156}
157
158impl Mergeable for Presence {
159 fn merge(acc: &mut Self, other: Self) {
160 acc.add(other);
161 }
162}
163
164enum Reader<V: Mergeable> {
166 Subscribing(Pending<Subscribing>),
168 Active(Box<moq_json::snapshot::Consumer<BTreeMap<String, V>>>),
171 Ended,
174}
175
176struct Node<V: Mergeable> {
179 reader: Reader<V>,
180 last: Option<BTreeMap<String, V>>,
181}
182
183struct Merged<V: Mergeable> {
185 announce: moq_net::announce::Consumer,
186 prefix: PathOwned,
187 depth: usize,
188 name: String,
190 config: moq_json::snapshot::ConsumerConfig,
191 nodes: HashMap<PathOwned, Node<V>>,
193}
194
195impl<V: Mergeable> Merged<V> {
196 fn new(announce: moq_net::announce::Consumer, config: &Config, name: String) -> Self {
197 Self {
198 announce,
199 prefix: config.prefix.clone(),
200 depth: config.depth,
201 name,
202 config: moq_json::snapshot::ConsumerConfig::default().with_compression(config.compression),
203 nodes: HashMap::new(),
204 }
205 }
206
207 fn poll_next(&mut self, waiter: &Waiter) -> Poll<Result<Option<BTreeMap<String, V>>>> {
211 let mut changed = false;
212
213 loop {
216 match self.announce.poll_next(waiter) {
217 Poll::Ready(Some(update)) => changed |= self.apply_announce(update),
218 Poll::Ready(None) => return Poll::Ready(Ok(None)),
219 Poll::Pending => break,
220 }
221 }
222
223 let config = &self.config;
225 let name = self.name.as_str();
226 for node in self.nodes.values_mut() {
227 changed |= advance(node, config, name, waiter);
228 }
229
230 if changed {
231 Poll::Ready(Ok(Some(self.merged())))
232 } else {
233 Poll::Pending
234 }
235 }
236
237 fn apply_announce(&mut self, update: moq_net::announce::Update) -> bool {
240 let moq_net::announce::Update { path, broadcast } = update;
241 let absolute = self.announce.absolute(&path).to_owned();
242
243 if parse_node_path(&self.prefix, self.depth, &absolute).is_none() {
246 return false;
247 }
248
249 match broadcast {
250 Some(broadcast) => match broadcast.track(&self.name) {
251 Ok(track) => {
252 let node = Node {
253 reader: Reader::Subscribing(track.subscribe(None)),
254 last: None,
255 };
256 self.nodes.insert(absolute, node).is_some_and(|old| old.last.is_some())
259 }
260 Err(err) => {
261 tracing::debug!(?err, node = %absolute, name = %self.name, "stats: node missing track");
262 self.nodes.remove(&absolute).is_some_and(|old| old.last.is_some())
263 }
264 },
265 None => self.nodes.remove(&absolute).is_some_and(|old| old.last.is_some()),
266 }
267 }
268
269 fn merged(&self) -> BTreeMap<String, V> {
271 let mut acc: BTreeMap<String, V> = BTreeMap::new();
272 for node in self.nodes.values() {
273 if let Some(last) = &node.last {
274 for (key, value) in last {
275 V::merge(acc.entry(key.clone()).or_default(), *value);
276 }
277 }
278 }
279 acc
280 }
281}
282
283fn advance<V: Mergeable>(
286 node: &mut Node<V>,
287 config: &moq_json::snapshot::ConsumerConfig,
288 name: &str,
289 waiter: &Waiter,
290) -> bool {
291 let mut changed = false;
292 loop {
293 match &mut node.reader {
294 Reader::Subscribing(pending) => match pending.poll_ok(waiter) {
295 Poll::Ready(Ok(subscriber)) => {
296 node.reader =
297 Reader::Active(Box::new(moq_json::snapshot::Consumer::new(subscriber, config.clone())));
298 }
299 Poll::Ready(Err(err)) => {
300 tracing::debug!(?err, name, "stats: node subscribe failed");
301 node.reader = Reader::Ended;
302 return changed;
303 }
304 Poll::Pending => return changed,
305 },
306 Reader::Active(reader) => match reader.poll_next(waiter) {
307 Poll::Ready(Ok(Some(frame))) => {
308 node.last = Some(frame);
309 changed = true;
310 }
311 Poll::Ready(Ok(None)) => return terminate(node, changed),
312 Poll::Ready(Err(err)) => {
313 tracing::debug!(?err, name, "stats: node read error");
316 return terminate(node, changed);
317 }
318 Poll::Pending => return changed,
319 },
320 Reader::Ended => return changed,
321 }
322 }
323}
324
325fn terminate<V: Mergeable>(node: &mut Node<V>, changed: bool) -> bool {
330 let had_value = node.last.take().is_some();
331 node.reader = Reader::Ended;
332 changed || had_value
333}
334
335#[cfg(test)]
336mod tests {
337 use std::time::Duration;
338
339 use moq_net::{Origin, PathOwned, Timestamp, announce, broadcast, origin, track};
340
341 use crate::{Producer, ProducerConfig};
342
343 use super::*;
344
345 fn node_producer(origin: &origin::Producer, node: &str) -> Producer {
349 Producer::new(
350 ProducerConfig::new()
351 .with_origin(origin.clone())
352 .with_node(PathOwned::from(node.to_string()))
353 .with_depth(1),
354 )
355 }
356
357 #[allow(dead_code)]
360 struct Feed {
361 announced: announce::Consumer,
362 source: broadcast::Producer,
363 consumer: broadcast::Consumer,
364 sub: track::Subscriber,
365 ctx: moq_net::stats::Session,
366 }
367
368 async fn feed(producer: &Producer, tier: Tier, root: &str, path: &str, bytes: usize) -> Feed {
372 let ctx = producer.registry().tier(tier).session(root);
373 let feed_origin = Origin::random().produce();
374 let egress = feed_origin.consume().with_stats(ctx.clone());
375
376 let mut announced = egress.announced();
377 let mut source = feed_origin
378 .create_broadcast(path, broadcast::Route::announced())
379 .expect("create_broadcast");
380 let mut track = source.create_track("video", None).expect("create_track");
381
382 tokio::time::sleep(Duration::from_millis(1)).await;
384 tokio::time::sleep(Duration::from_millis(1)).await;
385
386 let announce::Update { broadcast, .. } = announced.next().await.expect("announce");
387 let consumer = broadcast.expect("active");
388 let mut sub = consumer.track("video").unwrap().subscribe(None).await.unwrap();
389
390 let mut group = track.append_group().unwrap();
391 group.write_frame(Timestamp::ZERO, vec![0u8; bytes]).unwrap();
392 group.finish().unwrap();
393 let mut group = sub.recv_group().await.unwrap().unwrap();
394 while group.read_frame().await.unwrap().is_some() {}
395
396 Feed {
397 announced,
398 source,
399 consumer,
400 sub,
401 ctx,
402 }
403 }
404
405 async fn drive_tick() {
407 tokio::time::advance(Duration::from_millis(1100)).await;
408 for _ in 0..8 {
409 tokio::task::yield_now().await;
410 }
411 }
412
413 async fn read_until_bytes(consumer: &mut TrafficConsumer, path: &str, want: u64) -> TrafficFrame {
416 loop {
417 let frame = consumer.next().await.expect("read").expect("frame");
418 if frame.get(path).map(|t| t.bytes).unwrap_or(0) >= want {
419 return frame;
420 }
421 }
422 }
423
424 #[tokio::test(start_paused = true)]
425 async fn merges_traffic_across_nodes() {
426 let origin = Origin::random().produce();
429 let node_a = node_producer(&origin, "a");
430 let node_b = node_producer(&origin, "b");
431
432 let _fa = feed(&node_a, Tier::default(), "acme", "acme/room", 100).await;
433 let _fb = feed(&node_b, Tier::default(), "acme", "acme/room", 40).await;
434 drive_tick().await;
435
436 let agg = Consumer::new(origin.consume(), Config::new().with_depth(1));
437 let mut traffic = agg.traffic(&Tier::default(), Role::Publisher);
438
439 let frame = read_until_bytes(&mut traffic, "acme/room", 140).await;
440 let snap = frame.get("acme/room").expect("entry");
441 assert_eq!(snap.bytes, 140, "bytes sum across both nodes");
442 assert_eq!(snap.subscriptions, 2, "one subscription per node");
443 assert_eq!(snap.broadcasts, 2, "one viewer per node");
444 }
445
446 #[tokio::test(start_paused = true)]
447 async fn node_drop_regresses_the_merged_view() {
448 let origin = Origin::random().produce();
451 let node_a = node_producer(&origin, "a");
452 let node_b = node_producer(&origin, "b");
453
454 let _fa = feed(&node_a, Tier::default(), "acme", "acme/room", 100).await;
455 let fb = feed(&node_b, Tier::default(), "acme", "acme/room", 40).await;
456 drive_tick().await;
457
458 let agg = Consumer::new(origin.consume(), Config::new().with_depth(1));
459 let mut traffic = agg.traffic(&Tier::default(), Role::Publisher);
460 read_until_bytes(&mut traffic, "acme/room", 140).await;
461
462 drop(fb);
464 drop(node_b);
465 drive_tick().await;
466
467 loop {
469 let frame = traffic.next().await.expect("read").expect("frame");
470 if frame.get("acme/room").map(|t| t.bytes) == Some(100) {
471 break;
472 }
473 }
474 }
475
476 #[tokio::test(start_paused = true)]
477 async fn merges_sessions_across_nodes() {
478 let origin = Origin::random().produce();
480 let node_a = node_producer(&origin, "a");
481 let node_b = node_producer(&origin, "b");
482
483 let _fa = feed(&node_a, Tier::default(), "acme", "acme/room", 8).await;
486 let _fb = feed(&node_b, Tier::default(), "acme", "acme/room", 8).await;
487 let _sa = node_a.registry().tier(Tier::default()).session("acme");
488 let _sb = node_b.registry().tier(Tier::default()).session("acme");
489 drive_tick().await;
490
491 let agg = Consumer::new(origin.consume(), Config::new().with_depth(1));
492 let mut sessions = agg.sessions(&Tier::default());
493
494 loop {
495 let frame = sessions.next().await.expect("read").expect("frame");
496 if frame.get("acme").map(|p| p.active()) >= Some(4) {
499 break;
500 }
501 }
502 }
503}