1use std::collections::{HashMap, HashSet};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, Weak};
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use std::task::Poll;
9
10use moq_net::stats::{Presence, Registry, Report, Role, Tier, Traffic};
11use moq_net::{Path, PathOwned, broadcast, kio, origin, track};
12use serde::Serialize;
13use web_async::spawn;
14use web_async::time::Instant;
15
16use crate::{COMPRESSED_SUFFIX, sessions_track, traffic_track};
17
18#[derive(Clone)]
27#[non_exhaustive]
28pub struct Config {
29 pub origin: Option<origin::Producer>,
32 pub prefix: PathOwned,
37 pub node: Option<PathOwned>,
43 pub interval: Duration,
45 pub depth: usize,
54 pub linger: Duration,
62}
63
64impl Config {
65 pub fn new() -> Self {
69 Self {
70 origin: None,
71 prefix: PathOwned::from(".stats"),
72 node: None,
73 interval: Duration::from_secs(1),
74 depth: 0,
75 linger: DEFAULT_LINGER,
76 }
77 }
78
79 pub fn with_origin(mut self, origin: impl Into<Option<origin::Producer>>) -> Self {
82 self.origin = origin.into();
83 self
84 }
85
86 pub fn with_prefix(mut self, prefix: impl Into<PathOwned>) -> Self {
88 self.prefix = prefix.into();
89 self
90 }
91
92 pub fn with_interval(mut self, interval: Duration) -> Self {
94 self.interval = interval;
95 self
96 }
97
98 pub fn with_node(mut self, node: impl Into<Option<PathOwned>>) -> Self {
100 self.node = node.into();
101 self
102 }
103
104 pub fn with_depth(mut self, depth: usize) -> Self {
106 self.depth = depth;
107 self
108 }
109
110 pub fn with_linger(mut self, linger: Duration) -> Self {
113 self.linger = linger;
114 self
115 }
116}
117
118const DEFAULT_LINGER: Duration = Duration::from_secs(300);
121
122impl Default for Config {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128const MAX_REQUESTED_TRACKS: usize = 64;
138
139const MAX_PARKED_REQUESTS: usize = 256;
146
147struct Keepalive;
150
151#[derive(Clone)]
159pub struct Producer {
160 registry: Registry,
161 _keepalive: Option<Arc<Keepalive>>,
164}
165
166impl Producer {
167 pub fn new(config: Config) -> Self {
175 let Config {
176 origin,
177 prefix,
178 node,
179 interval,
180 depth,
181 linger,
182 } = config;
183 let node = node.filter(|p| !p.is_empty());
188
189 let Some(origin) = origin else {
190 return Self {
191 registry: Registry::disabled(),
192 _keepalive: None,
193 };
194 };
195
196 let exclude = moq_net::Pattern::subtree(prefix.as_str()).expect("the stats prefix is a literal path");
198 let registry = Registry::new(moq_net::stats::Config::new().with_exclude(exclude));
199 let keepalive = Arc::new(Keepalive);
200 let task = Task {
201 registry: registry.clone(),
202 origin,
203 prefix,
204 node,
205 depth,
206 linger,
207 interval,
208 sequence: Arc::new(AtomicU64::new(first_sequence(SystemTime::now()))),
209 };
210 spawn(task.run(Arc::downgrade(&keepalive)));
211
212 Self {
213 registry,
214 _keepalive: Some(keepalive),
215 }
216 }
217
218 pub fn registry(&self) -> &Registry {
222 &self.registry
223 }
224}
225
226fn first_sequence(now: SystemTime) -> u64 {
236 now.duration_since(UNIX_EPOCH)
237 .map_or(0, |since| since.as_micros() as u64)
238}
239
240struct Task {
241 registry: Registry,
242 origin: origin::Producer,
243 prefix: PathOwned,
244 node: Option<PathOwned>,
245 depth: usize,
246 linger: Duration,
247 interval: Duration,
248 sequence: Arc<AtomicU64>,
249}
250
251impl Task {
252 async fn run(self, weak: Weak<Keepalive>) {
255 let interval = self.interval;
256 let Some(mut drain) = Drain::new(self) else {
257 return;
258 };
259
260 let mut ticker = web_async::time::interval(interval);
261 ticker.set_missed_tick_behavior(web_async::time::MissedTickBehavior::Delay);
262
263 loop {
264 ticker.tick().await;
265
266 if weak.upgrade().is_none() {
267 drain.finish();
268 return;
269 }
270
271 drain.collect();
272 drain.publish(Instant::now());
273 }
274 }
275 fn node(&self) -> Option<&str> {
276 self.node.as_ref().map(moq_net::Path::as_str)
277 }
278}
279
280struct Drain {
283 task: Task,
284 groups: HashMap<String, GroupPublisher>,
286 report: Report,
288 refused: Vec<String>,
291 tick: u64,
294}
295
296impl Drain {
297 fn new(task: Task) -> Option<Self> {
301 let mut groups = HashMap::new();
302 if task.depth == 0 {
303 let group = GroupPublisher::create(&task, &Path::empty())?;
304 groups.insert(String::new(), group);
305 }
306 Some(Self {
307 task,
308 groups,
309 report: Report::default(),
310 refused: Vec::new(),
311 tick: 0,
312 })
313 }
314
315 fn collect(&mut self) {
318 self.task.registry.report(&mut self.report);
319 self.tick += 1;
320 self.refused.clear();
321
322 for group in self.groups.values_mut() {
323 group.traffic_rows.clear();
324 group.session_rows.clear();
325 }
326
327 for (i, entry) in self.report.traffic.iter().enumerate() {
328 let key = group_key(entry.path.as_str(), self.task.depth);
329 if let Some(group) = Self::group(&self.task, &mut self.groups, &mut self.refused, key) {
330 group.traffic_rows.push(i);
331 }
332 }
333 for (i, entry) in self.report.sessions.iter().enumerate() {
334 let key = group_key(entry.root.as_str(), self.task.depth);
335 if let Some(group) = Self::group(&self.task, &mut self.groups, &mut self.refused, key) {
336 group.session_rows.push(i);
337 }
338 }
339
340 for group in self.groups.values_mut() {
341 group.collect(&self.report, self.tick);
342 }
343 }
344
345 fn group<'a>(
348 task: &Task,
349 groups: &'a mut HashMap<String, GroupPublisher>,
350 refused: &mut Vec<String>,
351 key: &str,
352 ) -> Option<&'a mut GroupPublisher> {
353 if !groups.contains_key(key) {
354 if refused.iter().any(|name| name == key) {
355 return None;
356 }
357 match GroupPublisher::create(task, &Path::new(key)) {
358 Some(group) => {
359 groups.insert(key.to_string(), group);
360 }
361 None => {
362 refused.push(key.to_string());
363 return None;
364 }
365 }
366 }
367 groups.get_mut(key)
368 }
369
370 fn publish(&mut self, now: Instant) {
373 let Task { depth, linger, .. } = self.task;
376 for (_, group) in self
377 .groups
378 .extract_if(|_, group| depth > 0 && group.lingered(now, linger))
379 {
380 group.finish();
383 }
384
385 for group in self.groups.values_mut() {
386 group.flush(self.tick);
387
388 group.serve_requests();
394 }
395 }
396
397 fn finish(self) {
398 for (_, group) in self.groups {
399 group.finish();
400 }
401 }
402}
403
404struct Frame<V> {
408 entries: Vec<(PathOwned, V)>,
409}
410
411impl<V> Default for Frame<V> {
412 fn default() -> Self {
413 Self { entries: Vec::new() }
414 }
415}
416
417impl<V: Serialize> Serialize for Frame<V> {
418 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
419 serializer.collect_map(self.entries.iter().map(|(path, value)| (path.as_str(), value)))
420 }
421}
422
423struct Snapshot<V> {
428 track: track::Producer,
429 encoder: moq_json::snapshot::Encoder<Frame<V>>,
430 group: Option<moq_net::group::Producer>,
431 sequence: Arc<AtomicU64>,
432 deltas: bool,
433}
434
435impl<V: Serialize> Snapshot<V> {
436 fn new(track: track::Producer, config: moq_json::snapshot::Config, sequence: Arc<AtomicU64>) -> Self {
437 Self {
438 track,
439 deltas: config.delta_ratio != 0,
440 encoder: moq_json::snapshot::Encoder::new(config),
441 group: None,
442 sequence,
443 }
444 }
445
446 fn is_used(&self) -> bool {
447 self.track.is_used()
448 }
449
450 fn update(&mut self, value: &Frame<V>) -> moq_json::Result<()> {
451 let Some(frame) = self.encoder.update(value)? else {
452 return Ok(());
453 };
454 if frame.payload.len() as u64 > moq_net::group::MAX_CACHE_BYTES {
455 return Err(moq_net::Error::FrameTooLarge.into());
456 }
457 if frame.keyframe {
458 if let Some(group) = self.group.take() {
459 group.finish()?;
460 }
461 self.group = Some(self.track.create_group(moq_net::group::Info {
462 sequence: self.sequence.fetch_add(1, Ordering::Relaxed),
463 })?);
464 }
465 let group = self.group.as_mut().expect("a delta follows a snapshot");
466 if let Err(err) = group.write_frame(moq_net::Timestamp::now(), frame.payload.clone()) {
467 let _ = self.group.take().unwrap().finish();
468 return Err(err.into());
469 }
470 if !self.deltas {
471 self.group.take().unwrap().finish()?;
472 }
473 frame.commit();
474 Ok(())
475 }
476
477 fn finish(&mut self) -> moq_json::Result<()> {
478 if let Some(group) = self.group.take() {
479 group.finish()?;
480 }
481 Ok(self.track.finish()?)
482 }
483}
484
485struct TrackPair<V> {
490 plain: Snapshot<V>,
491 compressed: Snapshot<V>,
492 frame: Frame<V>,
494}
495
496impl<V: Serialize> TrackPair<V> {
497 fn create(broadcast: &broadcast::Producer, name: &str, sequence: Arc<AtomicU64>) -> Result<Self, moq_net::Error> {
498 let plain_track = broadcast.create_track(name, None)?;
499 let compressed_track = broadcast.create_track(format!("{name}{COMPRESSED_SUFFIX}").as_str(), None)?;
500 Ok(Self::from_tracks(plain_track, compressed_track, sequence))
501 }
502
503 fn from_tracks(plain_track: track::Producer, compressed_track: track::Producer, sequence: Arc<AtomicU64>) -> Self {
504 let plain_config = moq_json::snapshot::Config::default().with_delta_ratio(0);
505 let mut compressed_config = moq_json::snapshot::Config::default();
506 compressed_config.compression = moq_json::Compression::Deflate;
507
508 Self {
509 plain: Snapshot::new(plain_track, plain_config, sequence.clone()),
510 compressed: Snapshot::new(compressed_track, compressed_config, sequence),
511 frame: Frame::default(),
512 }
513 }
514
515 fn is_used(&self) -> bool {
517 self.plain.is_used() || self.compressed.is_used()
518 }
519
520 fn publish(&mut self, name: &str) {
523 self.frame.entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
524 if let Err(err) = self.plain.update(&self.frame) {
525 tracing::debug!(?err, name, "stats: failed to write frame");
526 }
527 if let Err(err) = self.compressed.update(&self.frame) {
528 tracing::debug!(?err, name, "stats: failed to write compressed frame");
529 }
530 self.frame.entries.clear();
531 }
532
533 fn finish(&mut self) {
537 let _ = self.plain.finish();
538 let _ = self.compressed.finish();
539 }
540}
541
542#[derive(Default)]
545struct PendingPair {
546 plain: Option<track::Request>,
547 compressed: Option<track::Request>,
548}
549
550impl PendingPair {
551 fn adopt<V: Serialize>(
557 self,
558 broadcast: &broadcast::Producer,
559 name: &str,
560 sequence: Arc<AtomicU64>,
561 ) -> Result<TrackPair<V>, moq_net::Error> {
562 let PendingPair { plain, compressed } = self;
563 let plain_track = match plain {
564 Some(request) => request.accept(None),
565 None => broadcast.create_track(name, None)?,
566 };
567 let compressed_track = match compressed {
568 Some(request) => request.accept(None),
569 None => broadcast.create_track(format!("{name}{COMPRESSED_SUFFIX}").as_str(), None)?,
570 };
571 Ok(TrackPair::from_tracks(plain_track, compressed_track, sequence))
572 }
573
574 fn reject(self, err: moq_net::Error) {
575 if let Some(request) = self.plain {
576 request.reject(err.clone());
577 }
578 if let Some(request) = self.compressed {
579 request.reject(err);
580 }
581 }
582
583 fn is_used(&self, waiter: &kio::Waiter) -> bool {
588 self.plain
589 .iter()
590 .chain(self.compressed.iter())
591 .any(|request| request.poll_unused(waiter).is_pending())
592 }
593}
594
595struct TrackFamily<V> {
598 sequence: Arc<AtomicU64>,
599 tracks: HashMap<String, TrackPair<V>>,
600 parked: HashMap<String, PendingPair>,
604}
605
606impl<V: Serialize> TrackFamily<V> {
607 fn new(sequence: Arc<AtomicU64>) -> Self {
608 Self {
609 sequence,
610 tracks: HashMap::new(),
611 parked: HashMap::new(),
612 }
613 }
614
615 fn push(
625 &mut self,
626 broadcast: &broadcast::Producer,
627 requested: &mut HashSet<String>,
628 name: &str,
629 path: PathOwned,
630 value: V,
631 ) {
632 if !self.tracks.contains_key(name) {
633 let result = match self.parked.remove(name) {
634 Some(pending) => pending.adopt(broadcast, name, self.sequence.clone()),
635 None => TrackPair::create(broadcast, name, self.sequence.clone()),
636 };
637 match result {
638 Ok(pair) => {
639 self.tracks.insert(name.to_string(), pair);
640 }
641 Err(err) => {
642 tracing::warn!(?err, name, "stats: failed to create track");
643 return;
644 }
645 }
646 }
647 if !requested.is_empty() {
648 requested.remove(name);
649 }
650 let pair = self.tracks.get_mut(name).expect("just ensured");
651 pair.frame.entries.push((path, value));
652 }
653
654 fn flush(&mut self) {
658 for (name, pair) in self.tracks.iter_mut() {
659 pair.publish(name);
660 }
661 }
662
663 fn reclaim(&mut self, requested: &mut HashSet<String>) {
669 self.tracks.retain(|name, pair| {
670 if !requested.contains(name) || pair.is_used() {
671 return true;
672 }
673 requested.remove(name);
674 pair.finish();
675 false
676 });
677 }
678
679 fn park(&mut self, plain: String, compressed: bool, request: track::Request, full: bool) {
682 match self.parked.get_mut(&plain) {
683 Some(pending) => {
684 let slot = match compressed {
685 true => &mut pending.compressed,
686 false => &mut pending.plain,
687 };
688 if slot.is_none() {
693 *slot = Some(request);
694 }
695 }
696 None if full => request.reject(moq_net::Error::NotFound),
697 None => {
698 let mut pending = PendingPair::default();
699 match compressed {
700 true => pending.compressed = Some(request),
701 false => pending.plain = Some(request),
702 }
703 self.parked.insert(plain, pending);
704 }
705 }
706 }
707
708 fn adopt_parked(&mut self, broadcast: &broadcast::Producer, requested: &mut HashSet<String>) {
713 let noop = kio::Waiter::noop();
714 let mut parked = std::mem::take(&mut self.parked);
715 parked.retain(|plain, pending| {
716 if !pending.is_used(&noop) {
717 return false;
718 }
719 if requested.len() >= MAX_REQUESTED_TRACKS {
720 return true;
721 }
722 self.adopt_pair(broadcast, requested, plain.clone(), std::mem::take(pending));
723 false
724 });
725 self.parked = parked;
726 }
727
728 fn adopt_pair(
732 &mut self,
733 broadcast: &broadcast::Producer,
734 requested: &mut HashSet<String>,
735 plain: String,
736 pending: PendingPair,
737 ) {
738 if self.tracks.contains_key(&plain) {
743 pending.reject(moq_net::Error::NotFound);
744 return;
745 }
746 match pending.adopt(broadcast, &plain, self.sequence.clone()) {
747 Ok(mut pair) => {
748 pair.publish(&plain);
750 self.tracks.insert(plain.clone(), pair);
751 requested.insert(plain);
752 }
753 Err(err) => tracing::warn!(?err, name = %plain, "stats: failed to adopt requested track"),
754 }
755 }
756
757 fn finish(&mut self) {
759 for pair in self.tracks.values_mut() {
760 pair.finish();
761 }
762 }
763}
764
765struct GroupPublisher {
767 broadcast: broadcast::Producer,
768 dynamic: broadcast::Dynamic,
772 requested: HashSet<String>,
777 traffic: TrackFamily<Traffic>,
778 sessions: TrackFamily<Presence>,
779 local: HashMap<PathOwned, HashMap<Tier, SideSlots>>,
780 session_local: HashMap<Tier, HashMap<PathOwned, SessionSlotState>>,
781 names: HashMap<Tier, TierNames>,
783 traffic_rows: Vec<usize>,
785 session_rows: Vec<usize>,
786 empty_since: Option<Instant>,
788}
789
790struct TierNames {
792 publisher: String,
793 subscriber: String,
794 sessions: String,
795}
796
797impl TierNames {
798 fn new(tier: &Tier) -> Self {
799 Self {
800 publisher: traffic_track(tier, Role::Publisher, false),
801 subscriber: traffic_track(tier, Role::Subscriber, false),
802 sessions: sessions_track(tier, false),
803 }
804 }
805}
806
807impl GroupPublisher {
808 fn create(task: &Task, group: &Path) -> Option<Self> {
809 let advertised = advertised_path(&task.prefix, group, task.node());
810 let broadcast = match task.origin.publish(&advertised, origin::Route::default()) {
811 Ok(broadcast) => broadcast,
812 Err(err) => {
813 tracing::warn!(advertised = %advertised, ?err, "stats: origin rejected stats broadcast");
814 return None;
815 }
816 };
817 tracing::debug!(advertised = %advertised, "stats: publishing broadcast");
818
819 let mut traffic = TrackFamily::new(task.sequence.clone());
820 let mut sessions = TrackFamily::new(task.sequence.clone());
821
822 let tier = Tier::default();
824 for role in [Role::Publisher, Role::Subscriber] {
825 let name = traffic_track(&tier, role, false);
826 match TrackPair::create(&broadcast, &name, task.sequence.clone()) {
827 Ok(pair) => {
828 traffic.tracks.insert(name, pair);
829 }
830 Err(err) => {
831 tracing::warn!(?err, name, "stats: failed to create track");
832 return None;
833 }
834 }
835 }
836 let name = sessions_track(&tier, false);
837 match TrackPair::create(&broadcast, &name, task.sequence.clone()) {
838 Ok(pair) => {
839 sessions.tracks.insert(name, pair);
840 }
841 Err(err) => {
842 tracing::warn!(?err, name, "stats: failed to create track");
843 return None;
844 }
845 }
846
847 let dynamic = broadcast.dynamic();
848
849 Some(Self {
850 broadcast,
851 dynamic,
852 requested: HashSet::new(),
853 traffic,
854 sessions,
855 local: HashMap::new(),
856 session_local: HashMap::new(),
857 names: HashMap::new(),
858 traffic_rows: Vec::new(),
859 session_rows: Vec::new(),
860 empty_since: None,
861 })
862 }
863
864 fn lingered(&mut self, now: Instant, linger: Duration) -> bool {
867 if !self.traffic_rows.is_empty() || !self.session_rows.is_empty() {
868 self.empty_since = None;
869 return false;
870 }
871 let since = *self.empty_since.get_or_insert(now);
872 now.saturating_duration_since(since) >= linger
873 }
874
875 fn collect(&mut self, report: &Report, tick: u64) {
877 let Self {
878 broadcast,
879 requested,
880 traffic,
881 sessions,
882 local,
883 session_local,
884 names,
885 traffic_rows,
886 session_rows,
887 ..
888 } = self;
889
890 for &i in traffic_rows.iter() {
891 let entry = &report.traffic[i];
892 let names = names
893 .entry(entry.tier.clone())
894 .or_insert_with(|| TierNames::new(&entry.tier));
895 let slots = local
896 .entry(entry.path.clone())
897 .or_default()
898 .entry(entry.tier.clone())
899 .or_default();
900 slots.seen = tick;
901 process_slot(entry.publisher, &mut slots.publisher, |snap| {
902 traffic.push(broadcast, requested, &names.publisher, entry.path.clone(), snap);
903 });
904 process_slot(entry.subscriber, &mut slots.subscriber, |snap| {
905 traffic.push(broadcast, requested, &names.subscriber, entry.path.clone(), snap);
906 });
907 }
908
909 for &i in session_rows.iter() {
910 let entry = &report.sessions[i];
911 let names = names
912 .entry(entry.tier.clone())
913 .or_insert_with(|| TierNames::new(&entry.tier));
914 let state = session_local
915 .entry(entry.tier.clone())
916 .or_default()
917 .entry(entry.root.clone())
918 .or_default();
919 state.seen = tick;
920 process_session_slot(entry.presence, state, |snap| {
921 sessions.push(broadcast, requested, &names.sessions, entry.root.clone(), snap);
922 });
923 }
924 }
925
926 fn flush(&mut self, tick: u64) {
929 self.traffic.flush();
930 self.sessions.flush();
931
932 self.local.retain(|_, tiers| {
933 tiers.retain(|_, slots| slots.seen == tick);
934 !tiers.is_empty()
935 });
936 self.session_local.retain(|_, roots| {
937 roots.retain(|_, state| state.seen == tick);
938 !roots.is_empty()
939 });
940 }
941
942 fn serve_requests(&mut self) {
954 self.traffic.reclaim(&mut self.requested);
957 self.sessions.reclaim(&mut self.requested);
958
959 let noop = kio::Waiter::noop();
963 while let Poll::Ready(Ok(request)) = self.dynamic.poll_requested_track(&noop) {
964 let Some(shape) = requested_track_shape(request.name()) else {
965 request.reject(moq_net::Error::NotFound);
966 continue;
967 };
968 let full = self.traffic.parked.len() + self.sessions.parked.len() >= MAX_PARKED_REQUESTS;
969 match shape.sessions {
970 true => self.sessions.park(shape.plain, shape.compressed, request, full),
971 false => self.traffic.park(shape.plain, shape.compressed, request, full),
972 }
973 }
974
975 self.traffic.adopt_parked(&self.broadcast, &mut self.requested);
976 self.sessions.adopt_parked(&self.broadcast, &mut self.requested);
977 }
978
979 fn finish(mut self) {
981 self.traffic.finish();
982 self.sessions.finish();
983 self.broadcast.close();
984 }
985}
986
987struct RequestedShape {
989 plain: String,
991 compressed: bool,
993 sessions: bool,
995}
996
997fn requested_track_shape(name: &str) -> Option<RequestedShape> {
1001 let (base, compressed) = match name.strip_suffix(COMPRESSED_SUFFIX) {
1002 Some(base) => (base, true),
1003 None => (name, false),
1004 };
1005 let (tier, kind) = match base.rsplit_once('/') {
1006 Some((tier, kind)) => (Some(tier), kind),
1007 None => (None, base),
1008 };
1009 let sessions = match kind {
1010 "publisher.json" | "subscriber.json" => false,
1011 "sessions.json" => true,
1012 _ => return None,
1013 };
1014 if let Some(tier) = tier
1017 && (tier.is_empty() || tier.starts_with('/') || tier.ends_with('/') || tier.contains("//"))
1018 {
1019 return None;
1020 }
1021 Some(RequestedShape {
1022 plain: base.to_string(),
1023 compressed,
1024 sessions,
1025 })
1026}
1027
1028#[derive(Default)]
1031struct SlotState {
1032 moved: bool,
1035}
1036
1037#[derive(Default)]
1039struct SideSlots {
1040 publisher: SlotState,
1041 subscriber: SlotState,
1042 seen: u64,
1044}
1045
1046#[derive(Default)]
1048struct SessionSlotState {
1049 prev_emitted: Option<Presence>,
1050 seen: u64,
1052}
1053
1054fn process_slot(snap: Traffic, slot_state: &mut SlotState, emit: impl FnOnce(Traffic)) {
1057 slot_state.moved |= snap != Traffic::default();
1068 if slot_state.moved {
1069 emit(snap);
1070 }
1071}
1072
1073fn process_session_slot(snap: Presence, slot_state: &mut SessionSlotState, emit: impl FnOnce(Presence)) {
1077 let live = snap.active() > 0;
1078 let prev_snap = slot_state.prev_emitted.unwrap_or_default();
1079 let changed = snap != prev_snap;
1080 if changed {
1081 slot_state.prev_emitted = Some(snap);
1082 }
1083 if live || changed {
1084 emit(snap);
1085 }
1086}
1087
1088fn group_key(path: &str, depth: usize) -> &str {
1090 if depth == 0 {
1091 return "";
1092 }
1093 match path.match_indices('/').nth(depth - 1) {
1094 Some((end, _)) => &path[..end],
1095 None => path,
1096 }
1097}
1098
1099fn advertised_path(prefix: &Path, group: &Path, node: Option<&str>) -> PathOwned {
1100 let mut out = prefix.as_str().to_string();
1104 if !group.is_empty() {
1105 out.push('/');
1106 out.push_str(group.as_str());
1107 }
1108 out.push_str("/node");
1109 if let Some(node) = node {
1110 out.push('/');
1111 out.push_str(node);
1112 }
1113 PathOwned::from(out)
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 #[tokio::test(start_paused = true)]
1119 async fn reclaimed_requested_tracks_resume_after_the_last_group() {
1120 let sequence = Arc::new(AtomicU64::new(0));
1121 resumes_after_the_last_group(sequence.clone(), sequence).await;
1122 }
1123
1124 #[tokio::test(start_paused = true)]
1125 async fn restarted_producers_resume_after_the_last_group() {
1126 let start = UNIX_EPOCH + Duration::from_secs(1_790_000_000);
1127 resumes_after_the_last_group(
1128 Arc::new(AtomicU64::new(first_sequence(start))),
1129 Arc::new(AtomicU64::new(first_sequence(start + Duration::from_secs(1)))),
1130 )
1131 .await;
1132 }
1133
1134 async fn resumes_after_the_last_group(before: Arc<AtomicU64>, after: Arc<AtomicU64>) {
1137 use futures::FutureExt;
1138 for name in ["idle/publisher.json", "idle/publisher.json.z"] {
1139 let broadcast = moq_net::broadcast::Info::new().produce();
1140 let _dynamic = broadcast.dynamic();
1141 let consumer = broadcast.consume();
1142 let mut family = TrackFamily::<Traffic>::new(before.clone());
1143 let mut requested = HashSet::new();
1144 let subscribing = consumer.track(name).unwrap().subscribe(None);
1145 family.adopt_pair(
1146 &broadcast,
1147 &mut requested,
1148 "idle/publisher.json".into(),
1149 PendingPair::default(),
1150 );
1151 let mut subscriber = subscribing.await.unwrap();
1152 let group = subscriber.recv_group().await.unwrap().unwrap();
1153 let floor = group.sequence + 1;
1154 drop(group);
1155 drop(subscriber);
1156 family.reclaim(&mut requested);
1157 assert!(family.tracks.is_empty());
1158 family.sequence = after.clone();
1159
1160 let subscribing = consumer
1161 .track(name)
1162 .unwrap()
1163 .subscribe(track::Subscription::default().with_start(track::Position::group(floor)));
1164 family.adopt_pair(
1165 &broadcast,
1166 &mut requested,
1167 "idle/publisher.json".into(),
1168 PendingPair::default(),
1169 );
1170 let mut subscriber = subscribing.await.unwrap();
1171 let group = subscriber
1172 .recv_group()
1173 .now_or_never()
1174 .expect("a recreated stats track must advance beyond its cached groups")
1175 .unwrap()
1176 .unwrap();
1177 assert!(group.sequence >= floor);
1178 }
1179 }
1180
1181 fn produce_origin() -> moq_net::origin::Producer {
1183 let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
1184 if tokio::runtime::Handle::try_current().is_ok() {
1185 tokio::spawn(moq_net::time::run(driver));
1186 } else {
1187 std::mem::forget(driver);
1190 }
1191 producer
1192 }
1193
1194 use std::collections::BTreeMap;
1195
1196 use moq_net::stats::{Registry, Tier};
1197 use moq_net::{Timestamp, announce, broadcast, track};
1198
1199 use super::*;
1200
1201 fn test_producer(node: Option<&str>) -> (Producer, origin::Producer) {
1202 let origin = produce_origin();
1203 let producer = Producer::new(
1204 Config::new()
1205 .with_origin(origin.clone())
1206 .with_node(node.map(|s| PathOwned::from(s.to_string()))),
1207 );
1208 (producer, origin)
1209 }
1210
1211 #[allow(dead_code)]
1214 struct Feed {
1215 announced: announce::Consumer,
1216 source: broadcast::Producer,
1217 consumer: broadcast::Consumer,
1218 sub: Option<track::Subscriber>,
1219 }
1220
1221 async fn feed(
1228 registry: &Registry,
1229 tier: Tier,
1230 path: &str,
1231 subscribe: bool,
1232 frames: usize,
1233 frame_size: usize,
1234 ) -> Feed {
1235 let ctx = registry.tier(tier).session("feed");
1236 let origin = produce_origin();
1237 let egress = origin.consume().with_stats(ctx);
1239
1240 let mut announced = egress.announced();
1241 let source = origin.create_broadcast(path).expect("create_broadcast");
1242 source.announce(origin::Route::default()).expect("announce");
1243 let producer = source.create_track("video", None).expect("create_track");
1244
1245 let update = announced.next().await.expect("announce");
1246 assert!(update.kind.is_active());
1247 let consumer = egress.request_broadcast(path).await.expect("resolve");
1248
1249 let sub = if subscribe {
1250 let mut sub = consumer
1251 .track("video")
1252 .expect("track")
1253 .subscribe(None)
1254 .await
1255 .expect("subscribe");
1256
1257 if frames > 0 {
1258 let mut group = producer.append_group().expect("group");
1259 for _ in 0..frames {
1260 group
1261 .write_frame(Timestamp::ZERO, vec![0u8; frame_size])
1262 .expect("write");
1263 }
1264 group.finish().expect("finish");
1265
1266 let mut group = sub.recv_group().await.expect("recv").expect("group");
1267 while group.read_frame().await.expect("read").is_some() {}
1268 }
1269 Some(sub)
1270 } else {
1271 None
1272 };
1273
1274 Feed {
1275 announced,
1276 source,
1277 consumer,
1278 sub,
1279 }
1280 }
1281
1282 async fn announced(origin: &origin::Producer) -> (String, moq_net::broadcast::Consumer) {
1284 let mut consumer = origin.consume().with_hidden(true).announced();
1285 tokio::time::advance(Duration::from_millis(1)).await;
1286 let update = consumer.next().await.expect("expected announce");
1287 assert!(update.kind.is_active());
1288 let broadcast = origin
1289 .consume()
1290 .request_broadcast(moq_net::Path::new(update.prefix.as_str()))
1291 .await
1292 .expect("resolve");
1293 (update.prefix.as_str().to_string(), broadcast)
1294 }
1295
1296 async fn drive_tick() {
1298 tokio::time::advance(Duration::from_millis(1100)).await;
1299 for _ in 0..4 {
1302 tokio::task::yield_now().await;
1303 }
1304 }
1305
1306 async fn read_frame(broadcast: &moq_net::broadcast::Consumer, name: &str) -> BTreeMap<String, Traffic> {
1309 let mut track = subscribe(broadcast, name).await;
1310 let frame = next_frame(&mut track).await;
1311 serde_json::from_slice(&frame.payload).expect("json parse")
1312 }
1313
1314 async fn read_last_frame(broadcast: &moq_net::broadcast::Consumer, name: &str) -> BTreeMap<String, Traffic> {
1318 let mut track = subscribe(broadcast, name).await;
1319 let mut last = next_frame(&mut track).await;
1320 while let Some(frame) = try_next_frame(&mut track) {
1321 last = frame;
1322 }
1323 serde_json::from_slice(&last.payload).expect("json parse")
1324 }
1325
1326 async fn read_session_frame(broadcast: &moq_net::broadcast::Consumer, name: &str) -> BTreeMap<String, Presence> {
1327 let mut track = subscribe(broadcast, name).await;
1328 let frame = next_frame(&mut track).await;
1329 serde_json::from_slice(&frame.payload).expect("json parse")
1330 }
1331
1332 async fn read_session_frame_last(
1333 broadcast: &moq_net::broadcast::Consumer,
1334 name: &str,
1335 ) -> BTreeMap<String, Presence> {
1336 let mut track = subscribe(broadcast, name).await;
1337 let mut last = next_frame(&mut track).await;
1338 while let Some(frame) = try_next_frame(&mut track) {
1339 last = frame;
1340 }
1341 serde_json::from_slice(&last.payload).expect("json parse")
1342 }
1343
1344 async fn subscribe(broadcast: &moq_net::broadcast::Consumer, name: &str) -> track::Ordered {
1345 broadcast
1346 .track(name)
1347 .expect("track")
1348 .subscribe(None)
1349 .await
1350 .expect("subscribe")
1351 .ordered()
1352 }
1353
1354 async fn next_frame(track: &mut track::Ordered) -> moq_net::frame::Frame {
1357 let mut group = track.next_group().await.expect("ok").expect("group");
1358 group.read_frame().await.expect("ok").expect("frame")
1359 }
1360
1361 fn try_next_frame(track: &mut track::Ordered) -> Option<moq_net::frame::Frame> {
1363 use futures::FutureExt;
1364 let mut group = track.next_group().now_or_never()?.expect("ok")?;
1365 group.read_frame().now_or_never()?.expect("ok")
1366 }
1367
1368 #[tokio::test(start_paused = true)]
1372 async fn new_normalizes_and_drops_empty_node() {
1373 let (_producer, origin) = test_producer(Some("/sjc//1/"));
1374 assert_eq!(announced(&origin).await.0, ".stats/node/sjc/1");
1375
1376 let (_producer, origin) = test_producer(Some("///"));
1377 assert_eq!(announced(&origin).await.0, ".stats/node");
1378 }
1379
1380 #[tokio::test(start_paused = true)]
1381 async fn single_broadcast_path_announced() {
1382 let (producer, origin) = test_producer(Some("sjc/1"));
1385
1386 let _f1 = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 8).await;
1387 let _f2 = feed(producer.registry(), Tier::default(), "baz/qux", true, 1, 8).await;
1388
1389 assert_eq!(announced(&origin).await.0, ".stats/node/sjc/1");
1390 }
1391
1392 #[tokio::test(start_paused = true)]
1393 async fn task_announces_without_node_suffix() {
1394 let (producer, origin) = test_producer(None);
1395 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 8).await;
1396 assert_eq!(announced(&origin).await.0, ".stats/node");
1397 }
1398
1399 #[tokio::test(start_paused = true)]
1400 async fn frame_emits_expected_counters() {
1401 let (producer, origin) = test_producer(Some("sjc"));
1402 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
1404
1405 drive_tick().await;
1406
1407 let (_, broadcast) = announced(&origin).await;
1408 let frame = read_last_frame(&broadcast, "publisher.json").await;
1409 let snap = frame.get("foo/bar").expect("foo/bar entry");
1410 assert_eq!(
1411 snap.announces_started, 1,
1412 "egress announce stream bumps announces_started"
1413 );
1414 assert_eq!(snap.broadcasts_started, 1, "one session subscribed");
1415 assert_eq!(snap.subscriptions_started, 1);
1416 assert_eq!(snap.bytes, 42);
1417 assert_eq!(snap.frames, 1);
1418 }
1419
1420 #[tokio::test(start_paused = true)]
1421 async fn announced_bytes_surfaces_in_frame() {
1422 let (producer, origin) = test_producer(Some("sjc"));
1423 let _f = feed(producer.registry(), Tier::default(), "foo/bar", false, 0, 0).await;
1425
1426 drive_tick().await;
1427
1428 let (_, broadcast) = announced(&origin).await;
1429 let frame = read_last_frame(&broadcast, "publisher.json").await;
1430 let snap = frame.get("foo/bar").expect("foo/bar entry");
1431 assert_eq!(snap.announces_started, 1);
1432 assert_eq!(
1433 snap.announced_bytes,
1434 "foo/bar".len() as u64,
1435 "name length recorded on announce"
1436 );
1437 }
1438
1439 #[tokio::test(start_paused = true)]
1440 async fn announced_decouples_from_broadcasts() {
1441 let (producer, origin) = test_producer(Some("sjc"));
1444 let _f = feed(producer.registry(), Tier::default(), "foo/bar", false, 0, 0).await;
1445
1446 drive_tick().await;
1447
1448 let (_, broadcast) = announced(&origin).await;
1449 let frame = read_last_frame(&broadcast, "publisher.json").await;
1450 let snap = frame.get("foo/bar").expect("foo/bar entry");
1451 assert_eq!(snap.announces_started, 1);
1452 assert_eq!(snap.broadcasts_started, 0, "no subscription, no broadcasts sentinel");
1453 assert_eq!(snap.subscriptions_started, 0);
1454 }
1455
1456 #[tokio::test(start_paused = true)]
1457 async fn short_lived_sub_is_surfaced() {
1458 let (producer, origin) = test_producer(Some("sjc"));
1463 {
1464 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 123).await;
1467 }
1468
1469 drive_tick().await;
1470
1471 let (_, broadcast) = announced(&origin).await;
1472 let frame = read_last_frame(&broadcast, "publisher.json").await;
1473 let snap = frame.get("foo/bar").expect("foo/bar entry");
1474 assert_eq!(snap.subscriptions_started, 1);
1476 assert_eq!(snap.subscriptions_ended, 1);
1477 assert_eq!(snap.broadcasts_started, 1, "one session subscribed");
1478 assert_eq!(snap.broadcasts_ended, 1);
1479 assert_eq!(snap.bytes, 123);
1480 assert_eq!(snap.frames, 1);
1481 }
1482
1483 #[tokio::test(start_paused = true)]
1484 async fn a_path_stays_in_the_frame_while_its_counters_live() {
1485 let (producer, origin) = test_producer(Some("sjc"));
1490 let registry = producer.registry();
1491 let data = produce_origin();
1492 let source = data
1493 .clone()
1494 .with_stats(registry.tier(Tier::default()).session("publisher"))
1495 .publish("foo/bar", origin::Route::default())
1496 .expect("publish");
1497 let mut video = source.create_track("video", None).expect("create_track");
1498 let egress = data
1499 .consume()
1500 .with_stats(registry.tier(Tier::default()).session("viewer"));
1501
1502 async fn view(egress: &origin::Consumer, video: &mut track::Producer, size: usize) {
1503 let broadcast = egress.request_broadcast("foo/bar").await.expect("resolve");
1504 let mut sub = broadcast
1505 .track("video")
1506 .expect("track")
1507 .subscribe(None)
1508 .await
1509 .expect("subscribe");
1510 let mut group = video.append_group().expect("group");
1511 group.write_frame(Timestamp::ZERO, vec![0u8; size]).expect("write");
1512 group.finish().expect("finish");
1513 let mut group = sub.recv_group().await.expect("recv").expect("group");
1514 while group.read_frame().await.expect("read").is_some() {}
1515 }
1516
1517 view(&egress, &mut video, 1000).await;
1518 let (_, stats) = announced(&origin).await;
1519 for _ in 0..3 {
1520 drive_tick().await;
1521 let frame = read_last_frame(&stats, "publisher.json").await;
1522 let snap = frame.get("foo/bar").expect("kept while its counters live");
1523 assert_eq!(snap.bytes, 1000);
1524 assert!(snap.is_idle(), "the viewer left");
1525 }
1526
1527 view(&egress, &mut video, 500).await;
1528 drive_tick().await;
1529 let frame = read_last_frame(&stats, "publisher.json").await;
1530 assert_eq!(frame["foo/bar"].bytes, 1500, "resumed, not restarted");
1531
1532 drop((video, source));
1534 drive_tick().await;
1535 drive_tick().await;
1536 let frame = read_last_frame(&stats, "publisher.json").await;
1537 assert!(
1538 !frame.contains_key("foo/bar"),
1539 "dropped with its counters, got {frame:?}"
1540 );
1541 }
1542
1543 fn grouped_producer(linger: Duration) -> (Producer, origin::Producer, announce::Consumer) {
1546 let origin = produce_origin();
1547 let events = origin.consume().with_hidden(true).announced();
1548 let producer = Producer::new(
1549 Config::new()
1550 .with_origin(origin.clone())
1551 .with_node(PathOwned::from("sjc"))
1552 .with_depth(1)
1553 .with_linger(linger),
1554 );
1555 (producer, origin, events)
1556 }
1557
1558 fn take_events(events: &mut announce::Consumer) -> Vec<(String, bool)> {
1560 use futures::FutureExt;
1561 let mut out = Vec::new();
1562 while let Some(Some(update)) = events.next().now_or_never() {
1563 out.push((update.prefix.as_str().to_string(), update.kind.is_active()));
1564 }
1565 out
1566 }
1567
1568 const ACME: &str = ".stats/acme/node/sjc";
1569 const FEED: &str = ".stats/feed/node/sjc";
1570
1571 #[tokio::test(start_paused = true)]
1572 async fn empty_group_lingers_then_unannounces() {
1573 let (producer, origin, mut events) = grouped_producer(Duration::from_secs(10));
1574 let registry = producer.registry();
1575
1576 let first = feed(registry, Tier::default(), "acme/live", true, 1, 100).await;
1579 drive_tick().await;
1580 let mut started = take_events(&mut events);
1581 started.sort();
1582 assert_eq!(started, [(ACME.to_string(), true), (FEED.to_string(), true)]);
1583 let acme = origin.consume().request_broadcast(ACME).await.expect("resolve");
1584 let sessions = origin.consume().request_broadcast(FEED).await.expect("resolve");
1585 assert_eq!(read_last_frame(&acme, "publisher.json").await["acme/live"].bytes, 100);
1586
1587 drop(first);
1589 for _ in 0..3 {
1590 drive_tick().await;
1591 }
1592 assert_eq!(take_events(&mut events), [], "an empty group lingers");
1593 let frame = read_last_frame(&acme, "publisher.json").await;
1594 assert!(frame.is_empty(), "no live counters while lingering, got {frame:?}");
1595 let frame = read_session_frame_last(&sessions, "sessions.json").await;
1596 assert!(frame.is_empty(), "no presence while lingering, got {frame:?}");
1597
1598 let second = feed(registry, Tier::default(), "acme/live", true, 1, 50).await;
1602 drive_tick().await;
1603 assert_eq!(
1604 take_events(&mut events),
1605 [],
1606 "a return within the linger re-announces nothing"
1607 );
1608 assert_eq!(read_last_frame(&acme, "publisher.json").await["acme/live"].bytes, 50);
1609
1610 drop(second);
1613 for _ in 0..8 {
1614 drive_tick().await;
1615 }
1616 assert_eq!(take_events(&mut events), [], "the return re-armed the linger");
1617 for _ in 0..4 {
1618 drive_tick().await;
1619 }
1620 let mut ended = take_events(&mut events);
1621 ended.sort();
1622 assert_eq!(ended, [(ACME.to_string(), false), (FEED.to_string(), false)]);
1623
1624 let _third = feed(registry, Tier::default(), "acme/live", true, 1, 25).await;
1626 drive_tick().await;
1627 let mut restarted = take_events(&mut events);
1628 restarted.sort();
1629 assert_eq!(restarted, [(ACME.to_string(), true), (FEED.to_string(), true)]);
1630 let acme = origin.consume().request_broadcast(ACME).await.expect("resolve");
1631 assert_eq!(read_last_frame(&acme, "publisher.json").await["acme/live"].bytes, 25);
1632 }
1633
1634 #[tokio::test(start_paused = true)]
1635 async fn zero_linger_unannounces_once_empty() {
1636 let (producer, _origin, mut events) = grouped_producer(Duration::ZERO);
1637 let first = feed(producer.registry(), Tier::default(), "acme/live", true, 1, 100).await;
1638 drive_tick().await;
1639 assert_eq!(take_events(&mut events).len(), 2);
1640
1641 drop(first);
1643 drive_tick().await;
1644 assert_eq!(take_events(&mut events), []);
1645 drive_tick().await;
1646 let mut ended = take_events(&mut events);
1647 ended.sort();
1648 assert_eq!(ended, [(ACME.to_string(), false), (FEED.to_string(), false)]);
1649 }
1650
1651 #[tokio::test(start_paused = true)]
1652 async fn session_track_surfaces_by_root() {
1653 let (producer, origin) = test_producer(Some("sjc"));
1654 let _a = producer.registry().tier(Tier::default()).session("acme");
1655 let _b = producer.registry().tier(Tier::default()).session("acme");
1656 let _c = producer.registry().tier(Tier::new("region/sjc")).session("peer");
1657
1658 drive_tick().await;
1659
1660 let (_, broadcast) = announced(&origin).await;
1661 let frame = read_session_frame(&broadcast, "sessions.json").await;
1662 let snap = frame.get("acme").expect("root entry");
1663 assert_eq!(snap.sessions_started, 2);
1664 assert_eq!(snap.sessions_ended, 0);
1665 assert!(
1666 !frame.contains_key("peer"),
1667 "regional session must not appear on the default track"
1668 );
1669
1670 let snap = *read_session_frame(&broadcast, "region/sjc/sessions.json")
1671 .await
1672 .get("peer")
1673 .expect("regional entry");
1674 assert_eq!(snap.sessions_started, 1);
1675 }
1676
1677 #[tokio::test(start_paused = true)]
1678 async fn unused_slots_dont_surface() {
1679 let (producer, origin) = test_producer(Some("sjc"));
1683 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 8).await;
1686
1687 drive_tick().await;
1688 drive_tick().await;
1689
1690 let (_, broadcast) = announced(&origin).await;
1691
1692 assert!(
1694 read_last_frame(&broadcast, "publisher.json")
1695 .await
1696 .contains_key("foo/bar"),
1697 "publisher.json must include the active foo/bar entry"
1698 );
1699
1700 let frame = read_frame(&broadcast, "subscriber.json").await;
1703 assert!(frame.is_empty(), "subscriber.json must be empty, got {frame:?}");
1704
1705 for name in ["publisher.json.z", "subscriber.json.z", "sessions.json.z"] {
1707 assert!(broadcast.track(name).is_ok(), "{name} must exist");
1708 }
1709
1710 let subscribing = broadcast
1715 .track("region/sjc/publisher.json")
1716 .expect("logical track")
1717 .subscribe(None);
1718 drive_tick().await;
1719 let mut sub = subscribing.await.expect("an idle tier's track is held open").ordered();
1720 let frame = next_frame(&mut sub).await;
1721 let parsed: BTreeMap<String, Traffic> = serde_json::from_slice(&frame.payload).expect("json");
1722 assert!(parsed.is_empty(), "an idle tier serves zeros, got {parsed:?}");
1723 }
1724
1725 #[test]
1726 fn advertised_path_with_and_without_node() {
1727 let prefix = Path::new(".stats");
1728 let empty = Path::empty();
1729 assert_eq!(
1730 advertised_path(&prefix, &empty, Some("sjc")).as_str(),
1731 ".stats/node/sjc"
1732 );
1733 assert_eq!(
1734 advertised_path(&prefix, &empty, Some("sjc/1")).as_str(),
1735 ".stats/node/sjc/1"
1736 );
1737 assert_eq!(advertised_path(&prefix, &empty, None).as_str(), ".stats/node");
1738 assert_eq!(
1739 advertised_path(&prefix, &Path::new("acme"), Some("sjc")).as_str(),
1740 ".stats/acme/node/sjc"
1741 );
1742
1743 let prefix = Path::new("metrics");
1744 assert_eq!(
1745 advertised_path(&prefix, &Path::new("demo/room"), Some("lon")).as_str(),
1746 "metrics/demo/room/node/lon"
1747 );
1748 }
1749
1750 #[test]
1751 fn group_key_uses_leading_segments() {
1752 assert_eq!(group_key("acme/room/cam", 0), "");
1753 assert_eq!(group_key("acme/room/cam", 1), "acme");
1754 assert_eq!(group_key("acme/room/cam", 2), "acme/room");
1755 assert_eq!(group_key("acme/room", 3), "acme/room");
1756 }
1757
1758 #[test]
1759 fn requested_track_shape_classifies() {
1760 let shape = requested_track_shape("rtmp/publisher.json").expect("valid");
1761 assert_eq!(shape.plain, "rtmp/publisher.json");
1762 assert!(!shape.compressed);
1763 assert!(!shape.sessions);
1764
1765 let shape = requested_track_shape("region/sjc/subscriber.json.z").expect("valid");
1766 assert_eq!(shape.plain, "region/sjc/subscriber.json");
1767 assert!(shape.compressed);
1768 assert!(!shape.sessions);
1769
1770 let shape = requested_track_shape("sessions.json").expect("default tier");
1771 assert_eq!(shape.plain, "sessions.json");
1772 assert!(shape.sessions);
1773
1774 assert!(requested_track_shape("bogus.json").is_none());
1775 assert!(requested_track_shape("xpublisher.json").is_none());
1776 assert!(requested_track_shape("/publisher.json").is_none());
1777 assert!(requested_track_shape("rtmp//publisher.json").is_none());
1778 assert!(requested_track_shape("rtmp/publisher.json.z.z").is_none());
1779 }
1780
1781 #[tokio::test(start_paused = true)]
1785 async fn idle_tier_track_resolves_with_zeros() {
1786 let (producer, origin) = test_producer(Some("sjc"));
1787 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
1789 drive_tick().await;
1790 let (_, broadcast) = announced(&origin).await;
1791
1792 let subscribing = broadcast.track("rtmp/publisher.json").expect("track").subscribe(None);
1794 drive_tick().await;
1795 let mut sub = subscribing.await.expect("held open, not rejected").ordered();
1796 let frame = next_frame(&mut sub).await;
1797 let parsed: BTreeMap<String, Traffic> = serde_json::from_slice(&frame.payload).expect("json");
1798 assert!(parsed.is_empty(), "an idle tier serves zeros");
1799
1800 let _rtmp = feed(producer.registry(), Tier::new("rtmp"), "foo/live", true, 1, 7).await;
1802 drive_tick().await;
1803 let frame = next_frame(&mut sub).await;
1804 let parsed: BTreeMap<String, Traffic> = serde_json::from_slice(&frame.payload).expect("json");
1805 assert_eq!(parsed.get("foo/live").expect("entry").bytes, 7);
1806 }
1807
1808 #[tokio::test(start_paused = true)]
1811 async fn compressed_tier_request_creates_the_pair() {
1812 let (producer, origin) = test_producer(Some("sjc"));
1813 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
1814 drive_tick().await;
1815 let (_, broadcast) = announced(&origin).await;
1816
1817 let subscribing = broadcast.track("srt/subscriber.json.z").expect("track").subscribe(None);
1818 drive_tick().await;
1819 subscribing.await.expect("compressed flavor held open");
1820
1821 subscribe(&broadcast, "srt/subscriber.json").await;
1823 }
1824
1825 #[tokio::test(start_paused = true)]
1827 async fn idle_tier_sessions_track_resolves_with_zeros() {
1828 let (producer, origin) = test_producer(Some("sjc"));
1829 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
1830 drive_tick().await;
1831 let (_, broadcast) = announced(&origin).await;
1832
1833 let subscribing = broadcast.track("webrtc/sessions.json").expect("track").subscribe(None);
1834 drive_tick().await;
1835 let mut sub = subscribing.await.expect("held open, not rejected").ordered();
1836 let frame = next_frame(&mut sub).await;
1837 let parsed: BTreeMap<String, Presence> = serde_json::from_slice(&frame.payload).expect("json");
1838 assert!(parsed.is_empty());
1839 }
1840
1841 #[tokio::test(start_paused = true)]
1843 async fn malformed_track_name_rejected() {
1844 let (producer, origin) = test_producer(Some("sjc"));
1845 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
1846 drive_tick().await;
1847 let (_, broadcast) = announced(&origin).await;
1848
1849 let subscribing = broadcast.track("bogus.json").expect("track").subscribe(None);
1850 drive_tick().await;
1851 assert!(subscribing.await.is_err(), "a non-stats name is rejected");
1852 }
1853
1854 #[tokio::test(start_paused = true)]
1859 async fn request_racing_first_traffic_is_fulfilled() {
1860 let (producer, origin) = test_producer(Some("sjc"));
1861 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
1862 drive_tick().await;
1863 let (_, broadcast) = announced(&origin).await;
1864
1865 let subscribing = broadcast.track("rtmp/publisher.json").expect("track").subscribe(None);
1868 assert!(subscribing.poll_ok(&moq_net::kio::Waiter::noop()).is_pending());
1869 for _ in 0..8 {
1870 tokio::task::yield_now().await;
1871 }
1872
1873 let _rtmp = feed(producer.registry(), Tier::new("rtmp"), "foo/live", true, 1, 7).await;
1875 drive_tick().await;
1876
1877 let mut sub = subscribing
1879 .await
1880 .expect("fulfilled by the tick's own creation")
1881 .ordered();
1882 let frame = next_frame(&mut sub).await;
1883 let parsed: BTreeMap<String, Traffic> = serde_json::from_slice(&frame.payload).expect("json");
1884 assert_eq!(parsed.get("foo/live").expect("entry").bytes, 7);
1885 }
1886
1887 #[tokio::test(start_paused = true)]
1892 async fn requested_quota_recovers_after_disconnect() {
1893 let (producer, origin) = test_producer(Some("sjc"));
1894 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
1895 drive_tick().await;
1896 let (_, broadcast) = announced(&origin).await;
1897
1898 let mut held = Vec::new();
1900 for i in 0..MAX_REQUESTED_TRACKS {
1901 let name = format!("junk{i}/publisher.json");
1902 let subscribing = broadcast.track(&name).expect("track").subscribe(None);
1903 drive_tick().await;
1904 held.push(subscribing.await.expect("within the cap"));
1905 }
1906
1907 let subscribing = broadcast.track("real/publisher.json").expect("track").subscribe(None);
1909 assert!(subscribing.poll_ok(&moq_net::kio::Waiter::noop()).is_pending());
1910 drive_tick().await;
1911 assert!(
1912 subscribing.poll_ok(&moq_net::kio::Waiter::noop()).is_pending(),
1913 "an over-quota request parks instead of being rejected"
1914 );
1915
1916 drop(held);
1922 for _ in 0..4 {
1923 tokio::task::yield_now().await;
1924 }
1925 tokio::time::advance(Duration::from_secs(31)).await;
1926 for _ in 0..3 {
1927 drive_tick().await;
1928 }
1929 subscribing
1930 .await
1931 .expect("the parked request is adopted once the quota frees");
1932 }
1933
1934 #[tokio::test(start_paused = true)]
1939 async fn parked_request_is_adopted_by_first_traffic() {
1940 let (producer, origin) = test_producer(Some("sjc"));
1941 let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
1942 drive_tick().await;
1943 let (_, broadcast) = announced(&origin).await;
1944
1945 let mut held = Vec::new();
1947 for i in 0..MAX_REQUESTED_TRACKS {
1948 let name = format!("junk{i}/publisher.json");
1949 let subscribing = broadcast.track(&name).expect("track").subscribe(None);
1950 drive_tick().await;
1951 held.push(subscribing.await.expect("within the cap"));
1952 }
1953 let subscribing = broadcast.track("rt/publisher.json").expect("track").subscribe(None);
1954 assert!(subscribing.poll_ok(&moq_net::kio::Waiter::noop()).is_pending());
1955 drive_tick().await;
1956 assert!(
1957 subscribing.poll_ok(&moq_net::kio::Waiter::noop()).is_pending(),
1958 "parked"
1959 );
1960
1961 let _rt = feed(producer.registry(), Tier::new("rt"), "foo/live", true, 1, 9).await;
1963 drive_tick().await;
1964 let mut sub = subscribing.await.expect("adopted by the flush").ordered();
1965 let frame = next_frame(&mut sub).await;
1966 let parsed: BTreeMap<String, Traffic> = serde_json::from_slice(&frame.payload).expect("json");
1967 assert_eq!(parsed.get("foo/live").expect("entry").bytes, 9);
1968 }
1969
1970 #[test]
1971 fn frame_serializes_like_a_btreemap() {
1972 let traffic = |bytes| {
1975 let mut traffic = Traffic::default();
1976 traffic.bytes = bytes;
1977 traffic
1978 };
1979 let mut frame = Frame::default();
1980 let mut map = BTreeMap::new();
1981 for (path, bytes) in [("room/b", 2), ("room/a", 1), ("other", 3), ("room/a/cam", 4)] {
1982 frame.entries.push((PathOwned::from(path), traffic(bytes)));
1983 map.insert(path.to_string(), traffic(bytes));
1984 }
1985 frame.entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
1986 assert_eq!(serde_json::to_vec(&frame).unwrap(), serde_json::to_vec(&map).unwrap());
1987 assert_eq!(serde_json::to_vec(&Frame::<Traffic>::default()).unwrap(), b"{}");
1988 }
1989
1990 mod counting {
1993 use std::alloc::{GlobalAlloc, Layout, System};
1994 use std::cell::Cell;
1995
1996 thread_local! {
1997 static ALLOCS: Cell<usize> = const { Cell::new(0) };
1998 }
1999
2000 struct Counting;
2001
2002 unsafe impl GlobalAlloc for Counting {
2003 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
2004 let _ = ALLOCS.try_with(|n| n.set(n.get() + 1));
2005 unsafe { System.alloc(layout) }
2006 }
2007
2008 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
2009 unsafe { System.dealloc(ptr, layout) }
2010 }
2011
2012 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
2013 let _ = ALLOCS.try_with(|n| n.set(n.get() + 1));
2014 unsafe { System.realloc(ptr, layout, new_size) }
2015 }
2016 }
2017
2018 #[global_allocator]
2019 static GLOBAL: Counting = Counting;
2020
2021 pub fn allocs() -> usize {
2022 ALLOCS.with(Cell::get)
2023 }
2024 }
2025
2026 #[tokio::test(start_paused = true)]
2027 async fn steady_drain_collects_without_allocating() {
2028 for depth in [0, 1] {
2032 for (broadcasts, tiers) in [(1, 1), (16, 1), (1, 4), (16, 4)] {
2033 let registry = Registry::new(moq_net::stats::Config::new());
2034 let mut feeds = Vec::new();
2035 for t in 0..tiers {
2036 let tier = match t {
2037 0 => Tier::default(),
2038 t => Tier::new(format!("tier{t}")),
2039 };
2040 for b in 0..broadcasts {
2041 feeds.push(feed(®istry, tier.clone(), &format!("room{b}/cam"), true, 1, 8).await);
2042 }
2043 }
2044
2045 let mut drain = Drain::new(Task {
2046 registry,
2047 origin: produce_origin(),
2048 prefix: PathOwned::from(".stats"),
2049 node: None,
2050 sequence: Arc::new(AtomicU64::new(0)),
2051 depth,
2052 linger: DEFAULT_LINGER,
2053 interval: Duration::from_secs(1),
2054 })
2055 .expect("drain");
2056
2057 for _ in 0..3 {
2059 drain.collect();
2060 drain.publish(Instant::now());
2061 }
2062
2063 let before = counting::allocs();
2064 drain.collect();
2065 let allocs = counting::allocs() - before;
2066 drain.publish(Instant::now());
2067
2068 let pending: usize = drain
2069 .groups
2070 .values()
2071 .map(|group| group.traffic_rows.len() + group.session_rows.len())
2072 .sum();
2073 assert!(pending > 0, "the drain carried entries");
2074 assert_eq!(allocs, 0, "depth {depth}, {broadcasts} broadcasts x {tiers} tiers");
2075 }
2076 }
2077 }
2078}