Skip to main content

moq_stats/
produce.rs

1//! The publishing half: drain a [`Registry`] on an interval into stats tracks.
2
3use 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/// Settings for a [`Producer`]. Construct with [`Config::new`] and chain
19/// the `with_*` setters (e.g.
20/// `Config::new().with_origin(origin).with_prefix(".foo")`), then hand it
21/// to [`Producer::new`].
22///
23/// With no origin set the resulting producer is a no-op: its registry is
24/// disabled (bumps are dropped) and no task spawns. Call
25/// [`Config::with_origin`] to publish.
26#[derive(Clone)]
27#[non_exhaustive]
28pub struct Config {
29	/// Origin the stats broadcasts are created on.
30	/// When `None`, [`Producer::new`] spawns no task and publishes nothing.
31	pub origin: Option<origin::Producer>,
32	/// Top-level path stats are published under (default `.stats`). The full
33	/// advertised path is `<prefix>/node/<node>` (or `<prefix>/node` when
34	/// `node` is unset). Also the registry's exclude prefix, so serving a
35	/// stats broadcast doesn't generate more stats.
36	pub prefix: PathOwned,
37	/// Node suffix that disambiguates broadcasts from different relays sharing a
38	/// cluster origin. Set this on every node in multi-relay deployments. May be
39	/// multi-segment (e.g. `sjc/1`, `sjc/2`) so a region with multiple hosts can
40	/// nest under a shared region key. An empty path is treated as unset.
41	/// Default none.
42	pub node: Option<PathOwned>,
43	/// How long the publish task waits between drains. Default 1s.
44	pub interval: Duration,
45	/// How many leading broadcast-path segments to use as a grouping key.
46	///
47	/// Default `0` publishes one `<prefix>/node/<node>` broadcast carrying every
48	/// path. `1` publishes one broadcast per first segment at
49	/// `<prefix>/<group>/node/<node>`, and larger values include more leading
50	/// segments. Group broadcasts are announced while their group has live traffic,
51	/// plus the [`Self::linger`]; at depth `0`, the single broadcast stays
52	/// announced for the producer's life.
53	pub depth: usize,
54	/// How long a group broadcast stays announced after its group's last entry
55	/// leaves. Default 5 minutes.
56	///
57	/// A group that returns within the linger keeps its broadcast, so a viewer
58	/// leaving and another arriving causes no unannounce and re-announce across
59	/// the mesh. While it lingers empty, every track reads `{}`. Zero
60	/// unannounces on the first drain the group is empty. Unused at depth `0`.
61	pub linger: Duration,
62}
63
64impl Config {
65	/// A config with default settings: no origin (no-op), `.stats` prefix, 1s
66	/// interval, and no node suffix. Call [`Self::with_origin`] to actually
67	/// publish.
68	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	/// Set the origin to publish the stats broadcast on. Without this the
80	/// producer is a no-op.
81	pub fn with_origin(mut self, origin: impl Into<Option<origin::Producer>>) -> Self {
82		self.origin = origin.into();
83		self
84	}
85
86	/// Override the top-level prefix (default `.stats`).
87	pub fn with_prefix(mut self, prefix: impl Into<PathOwned>) -> Self {
88		self.prefix = prefix.into();
89		self
90	}
91
92	/// Override the publish interval (default 1s).
93	pub fn with_interval(mut self, interval: Duration) -> Self {
94		self.interval = interval;
95		self
96	}
97
98	/// Set the node suffix (default none). An empty path is treated as unset.
99	pub fn with_node(mut self, node: impl Into<Option<PathOwned>>) -> Self {
100		self.node = node.into();
101		self
102	}
103
104	/// Set the grouping depth (default 0, a single broadcast). See [`Self::depth`].
105	pub fn with_depth(mut self, depth: usize) -> Self {
106		self.depth = depth;
107		self
108	}
109
110	/// Set how long an empty group broadcast stays announced (default 5
111	/// minutes). See [`Self::linger`].
112	pub fn with_linger(mut self, linger: Duration) -> Self {
113		self.linger = linger;
114		self
115	}
116}
117
118/// The default [`Config::linger`]. Sized from a live fleet, where an idle
119/// group's viewers returned 46 to 112 seconds after the last one left.
120const DEFAULT_LINGER: Duration = Duration::from_secs(300);
121
122impl Default for Config {
123	fn default() -> Self {
124		Self::new()
125	}
126}
127
128/// Cap on concurrently-held consumer-requested (vs traffic-created) track
129/// pairs per group broadcast. Requests mint real tracks, so a connected
130/// subscriber probing arbitrary tier names must hit a bound - but only while
131/// its subscriptions are actually held: a requested pair that loses its last
132/// consumer before its tier ever records is reclaimed on the next drain,
133/// refunding the cap, so a disconnected prober cannot deny a later collector.
134/// A valid-shaped request over the cap parks rather than being rejected (see
135/// [`MAX_PARKED_REQUESTS`]). Sized far above any real tier set (a deployment
136/// has on the order of ten tiers, three track kinds each).
137const MAX_REQUESTED_TRACKS: usize = 64;
138
139/// Cap on parked (valid-shaped, awaiting quota) consumer requests per group
140/// broadcast, beyond which new names are rejected outright. Parking instead of
141/// rejecting is what keeps a quota-full window from terminally stranding a
142/// collector - a consumer that treats one rejection as final would otherwise
143/// lose the tier until the broadcast unannounces - so this bound exists only
144/// to stop the parked buffer itself growing without limit.
145const MAX_PARKED_REQUESTS: usize = 256;
146
147/// Keeps the publish task alive: the task holds only a `Weak` to this, so it
148/// exits once the last [`Producer`] clone drops.
149struct Keepalive;
150
151/// Publishes a [`Registry`]'s counters as stats broadcasts. Cheap to clone.
152///
153/// [`Producer::new`] builds the registry itself (wiring the config's prefix as
154/// its exclude prefix) and spawns the publish task; hand sessions tier-scoped
155/// handles via [`Registry::tier`] on [`Producer::registry`]. The task drains
156/// the registry every interval and writes a frame per changed track, running
157/// until the last [`Producer`] clone is dropped.
158#[derive(Clone)]
159pub struct Producer {
160	registry: Registry,
161	/// `None` for a no-op producer (config had no origin): no task was spawned
162	/// and the registry is disabled.
163	_keepalive: Option<Arc<Keepalive>>,
164}
165
166impl Producer {
167	/// Build a producer from `config`.
168	///
169	/// When `config` has an origin, this spawns the publish task immediately
170	/// and announces the stats broadcast; the task runs until the last
171	/// [`Producer`] clone is dropped. With no origin the producer is a no-op
172	/// (its registry is disabled, nothing is published) and no task spawns, so
173	/// it's safe to build outside an async runtime.
174	pub fn new(config: Config) -> Self {
175		let Config {
176			origin,
177			prefix,
178			node,
179			interval,
180			depth,
181			linger,
182		} = config;
183		// An empty path after normalization is indistinguishable from "no node
184		// set"; collapse it so downstream code only sees a single representation.
185		// We do this here (not in `with_node`) so a directly-assigned
186		// `config.node` is normalized too.
187		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		// The prefix is a literal path, so its subtree claim cannot fail.
197		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	/// The registry this producer drains. Hand sessions tier-scoped handles via
219	/// [`Registry::tier`]; read node totals back with [`Registry::snapshot`].
220	/// Disabled (all bumps no-op) for a no-op producer.
221	pub fn registry(&self) -> &Registry {
222		&self.registry
223	}
224}
225
226/// Everything the publish task owns.
227/// A producer's first group number: wall-clock microseconds, so a restarted
228/// relay resumes past the groups its previous run left in a subscriber's cache.
229/// The allocator takes far fewer than one group per microsecond, so it stays
230/// below the next run's start. A clock stepped back across a restart still
231/// parks subscribers until the new run passes their floor.
232///
233/// Temporary, on `release` only: `main` replaces this with stats epochs, a
234/// fresh broadcast per restart whose totals the aggregator sums.
235fn 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	/// Publishes stats broadcasts and writes a frame per drain. Runs until
253	/// every [`Producer`] clone is dropped (`weak.upgrade()` returns `None`).
254	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
280/// The publish task's state, kept across drains so a steady-state drain
281/// reuses its buffers instead of allocating per entry.
282struct Drain {
283	task: Task,
284	/// Keyed by the group's path; `""` at depth 0.
285	groups: HashMap<String, GroupPublisher>,
286	/// Refilled by every drain.
287	report: Report,
288	/// Groups whose broadcast the origin refused this drain, so a refusal is
289	/// logged once per drain rather than once per entry.
290	refused: Vec<String>,
291	/// Drain counter, stamped on the change-detection state an entry touches
292	/// so state the report no longer carries can be dropped.
293	tick: u64,
294}
295
296impl Drain {
297	/// Build the drain state. At depth 0 the single broadcast is announced
298	/// up front and lives for the producer's life; `None` if the origin
299	/// refused it.
300	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	/// Drain the registry and sort each entry into its group's pending frames,
316	/// creating group broadcasts and tracks on first sight.
317	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	/// Get or create the group publisher for `key`, `None` if the origin
346	/// refused its broadcast.
347	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	/// Write every group's pending frames, serve consumer track requests, and
371	/// unpublish groups that have lingered empty.
372	fn publish(&mut self, now: Instant) {
373		// At depth 0 the single broadcast stays for the producer's life; a
374		// group broadcast lives while its group has entries, plus the linger.
375		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			// Deliberate unpublish: finish (tracks included) rather than drop,
381			// so there is no dropped-without-finish warning.
382			group.finish();
383		}
384
385		for group in self.groups.values_mut() {
386			group.flush(self.tick);
387
388			// Serve consumer requests for tracks no drain has created yet: a
389			// tier's tracks appear lazily on its first traffic, so a subscriber
390			// arriving first would otherwise be rejected and forced into a
391			// retry loop (fleet-wide, that rejection churn is a log and CPU
392			// storm). Held open with zeros instead; see `serve_requests`.
393			group.serve_requests();
394		}
395	}
396
397	fn finish(self) {
398		for (_, group) in self.groups {
399			group.finish();
400		}
401	}
402}
403
404/// One track's frame, rebuilt every drain in a buffer kept across drains.
405/// Serializes as a JSON object keyed by path, byte-identical to
406/// [`TrafficFrame`](crate::TrafficFrame) / [`SessionsFrame`](crate::SessionsFrame) once sorted.
407struct 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
423/// Writes encoded snapshots with group numbers from the producer-wide
424/// allocator. The allocator outlives on-demand tracks and group broadcasts, so
425/// a recreated track resumes past any group a subscriber has cached, without
426/// retaining a tombstone per requested name.
427struct 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
485/// A plain track and its `.z` sibling, kept in lockstep. The plain side runs
486/// moq-json with deltas and compression off, which is wire-identical to
487/// writing each frame as its own single-frame group; the compressed side uses
488/// merge-patch deltas inside a shared DEFLATE window.
489struct TrackPair<V> {
490	plain: Snapshot<V>,
491	compressed: Snapshot<V>,
492	/// This drain's entries, published and cleared by [`Self::publish`].
493	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	/// Whether any consumer exists on either flavor.
516	fn is_used(&self) -> bool {
517		self.plain.is_used() || self.compressed.is_used()
518	}
519
520	/// Publish this drain's entries on both flavors (`{}` when there are none)
521	/// and clear them for the next drain; moq-json skips unchanged values.
522	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	/// Finish both flavors, so dropping the pair is a deliberate end instead of
534	/// a dropped-without-finish warning. An error means the track already
535	/// ended; there is nothing left to close.
536	fn finish(&mut self) {
537		let _ = self.plain.finish();
538		let _ = self.compressed.finish();
539	}
540}
541
542/// Both flavors' pending requests for one plain track name, collected before
543/// serving so each is answered through its own request.
544#[derive(Default)]
545struct PendingPair {
546	plain: Option<track::Request>,
547	compressed: Option<track::Request>,
548}
549
550impl PendingPair {
551	/// Build a pair from consumer requests, creating whichever flavor was not
552	/// requested. A popped request is no longer queued, so `create_track`'s
553	/// queued-request fulfillment cannot reach it; the caller collects both
554	/// flavors' popped requests and this serves each through its actual
555	/// request where one exists.
556	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	/// Whether any present flavor still has a live requester. Through a relay
584	/// origin the serving task holds the request while its info is pending, so
585	/// this can read used for a while after the end subscriber left; that only
586	/// delays reclamation, it never strands anyone.
587	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
595/// One frame type's live pairs and the requests parked for them; the traffic
596/// tracks and the sessions tracks each form one family.
597struct TrackFamily<V> {
598	sequence: Arc<AtomicU64>,
599	tracks: HashMap<String, TrackPair<V>>,
600	/// Valid-shaped requests awaiting quota, keyed by plain name and bounded by
601	/// [`MAX_PARKED_REQUESTS`] across both families. Adopted as the quota
602	/// frees, or dropped once every requester leaves.
603	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	/// Add one entry to track `name`'s pending frame, creating the pair on the
616	/// track's first entry.
617	///
618	/// A pair created here serves any parked requests for its name: a parked
619	/// request was already popped off the broadcast queue, so `create_track`'s
620	/// queued-request fulfillment cannot reach it, and creating the pair blind
621	/// would strand its requesters on a name that now exists. A requested pair
622	/// whose tier records becomes an ordinary tier pair: kept for the
623	/// broadcast's life, no longer counting against the requested quota.
624	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	/// Publish every pair's pending frame, an empty one when the drain had
655	/// nothing for it, so a track whose last entry closed transitions to `{}`
656	/// exactly once.
657	fn flush(&mut self) {
658		for (name, pair) in self.tracks.iter_mut() {
659			pair.publish(name);
660		}
661	}
662
663	/// Reclaim requested pairs whose last consumer left before their tier ever
664	/// recorded: cached state nobody is watching. The pair is finished (a
665	/// deliberate end, not a warning) and dropped, so a returning subscriber
666	/// re-requests and is re-adopted; the quota refund means a disconnected
667	/// prober can never deny a later drain's legitimate requests.
668	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	/// Park one popped request, merging the two flavors of a plain name. Only a
680	/// NEW name while the parked buffer is `full` is rejected.
681	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				// Keep the first requester for a flavor. A duplicate means
689				// the original was already popped off the broadcast queue;
690				// dropping the newcomer aborts it into a retry, which joins
691				// the live track once the parked pair is adopted.
692				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	/// Adopt parked requests as the quota allows; the rest stay parked for a
709	/// later drain, so a valid-shaped request is never terminally rejected
710	/// merely for arriving while the quota was full. Entries whose every
711	/// requester left are dropped instead of adopted.
712	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	/// Adopt one plain name's pending requests into a live [`TrackPair`],
729	/// publishing a zero frame so the subscription resolves immediately. The
730	/// caller owns the quota decision; this only mints the pair.
731	fn adopt_pair(
732		&mut self,
733		broadcast: &broadcast::Producer,
734		requested: &mut HashSet<String>,
735		plain: String,
736		pending: PendingPair,
737	) {
738		// Defensive only: a request racing the pair's creation is fulfilled by
739		// `create_track` (queued) or adopted by [`Self::flush`] (parked), so it
740		// never reaches this with the pair already live. Rejecting is still
741		// safe there - the requester's retry resolves against the live track.
742		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				// Hold the subscription open with zeros until the tier records.
749				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	/// Finish every pair, making teardown a deliberate end.
758	fn finish(&mut self) {
759		for pair in self.tracks.values_mut() {
760			pair.finish();
761		}
762	}
763}
764
765/// One group stats broadcast and its change-detection state.
766struct GroupPublisher {
767	broadcast: broadcast::Producer,
768	/// Holds the broadcast's request queue open, so a subscriber asking for a
769	/// tier track no drain has created yet parks (served next tick) instead of
770	/// being rejected `NotFound` on the spot.
771	dynamic: broadcast::Dynamic,
772	/// Names of consumer-requested pairs whose tier has not recorded yet. Its
773	/// size is the [`MAX_REQUESTED_TRACKS`] quota; a name leaves the set by
774	/// recording real traffic (now an ordinary tier pair, kept forever) or by
775	/// losing its last consumer (reclaimed, quota refunded).
776	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	/// Track names per tier, built once so a drain never formats a name.
782	names: HashMap<Tier, TierNames>,
783	/// This drain's entries for the group, as indices into the report.
784	traffic_rows: Vec<usize>,
785	session_rows: Vec<usize>,
786	/// When the group's last entry left; `None` while it has entries.
787	empty_since: Option<Instant>,
788}
789
790/// The plain track names one tier's entries land on.
791struct 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		// The default tier's tracks always exist, even while idle.
823		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	/// Whether this drain left the group empty for at least `linger`. An entry
865	/// returning within the linger re-arms it.
866	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	/// Run this drain's rows through change detection into the pending frames.
876	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	/// Publish the pending frames, then drop change-detection state for
927	/// entries this drain did not carry (the registry pruned them).
928	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	/// Serve consumer requests for tracks no drain has created yet.
943	///
944	/// A tier's tracks are created lazily, on the tier's first recorded byte, so
945	/// a subscriber can legitimately ask before they exist (an idle protocol a
946	/// collector watches on every node). Rejecting such a request forces every
947	/// one of those subscribers into a resubscribe loop; instead any
948	/// stats-shaped name is accepted immediately and held open with a zero
949	/// frame, and the tier's real data rides the same tracks once it records
950	/// ([`flush_dynamic`] finds the pair already created). Names that do not
951	/// match the stats track shape are rejected as before, and valid names over
952	/// the quota park (bounded) until it frees rather than being rejected.
953	fn serve_requests(&mut self) {
954		// Reclaim before parking and adopting, so a freed quota slot is usable
955		// by this very drain.
956		self.traffic.reclaim(&mut self.requested);
957		self.sessions.reclaim(&mut self.requested);
958
959		// Pop everything queued into the parked maps, grouping the two flavors
960		// of one plain name so the pair is built from the actual requests where
961		// present. Only names past the parked bound are rejected.
962		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	/// Deliberately end the broadcast: finish every pair, then close the broadcast.
980	fn finish(mut self) {
981		self.traffic.finish();
982		self.sessions.finish();
983		self.broadcast.close();
984	}
985}
986
987/// The parsed shape of a consumer-requested stats track name.
988struct RequestedShape {
989	/// The plain (uncompressed) track name, the pair maps' key.
990	plain: String,
991	/// Whether the requested flavor was the [`COMPRESSED_SUFFIX`] one.
992	compressed: bool,
993	/// Sessions track vs traffic track, picking the frame type.
994	sessions: bool,
995}
996
997/// Classify a consumer-requested track name against the stats track shape
998/// `[<tier>/]{publisher|subscriber|sessions}.json[.z]`, or `None` for a name no
999/// tier could ever produce.
1000fn 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	// The tier label is an arbitrary path; require a clean one so a malformed
1015	// name can't mint a track a real tier could never produce.
1016	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/// Emission state for one `(path, tier, side)` slot, owned by the publish
1029/// task. The task is single-threaded so this needs no atomics.
1030#[derive(Default)]
1031struct SlotState {
1032	/// Whether any counter on this side has moved since the registry created
1033	/// the entry.
1034	moved: bool,
1035}
1036
1037/// Emission state for one `(path, tier)`: a [`SlotState`] per side.
1038#[derive(Default)]
1039struct SideSlots {
1040	publisher: SlotState,
1041	subscriber: SlotState,
1042	/// The last drain that reported this `(path, tier)`.
1043	seen: u64,
1044}
1045
1046/// Change-detection state for one session-track root.
1047#[derive(Default)]
1048struct SessionSlotState {
1049	prev_emitted: Option<Presence>,
1050	/// The last drain that reported this root.
1051	seen: u64,
1052}
1053
1054/// Per-drain work for a single `(side, tier)` slot: hand `snap` to `emit` once
1055/// the side has moved, on every drain until the registry drops the entry.
1056fn process_slot(snap: Traffic, slot_state: &mut SlotState, emit: impl FnOnce(Traffic)) {
1057	// A side can go idle while its entry lives on: the last viewer leaves but
1058	// the publisher still holds the path, so the egress counters are kept and
1059	// resume where they stopped. Omitting it would make its return look like a
1060	// fresh entry to a reader diffing frames, and the old total would count
1061	// twice. So once moved, a side stays in every frame until `flush` drops its
1062	// state with the entry, and a path missing from a frame really restarted.
1063	//
1064	// A side that never moved stays out, so an entry with traffic on one side
1065	// does not surface on the other track as zeros. A live side has a started
1066	// counter above zero, so it has always moved.
1067	slot_state.moved |= snap != Traffic::default();
1068	if slot_state.moved {
1069		emit(snap);
1070	}
1071}
1072
1073/// Per-drain work for one session-track root: emit it while a session is
1074/// connected, and on the drain its counters change. A root's counters are
1075/// dropped with its last session, so it never idles in the registry.
1076fn 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
1088/// The leading `depth` segments of `path`, the group it publishes under.
1089fn 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	// `<prefix>/<group>/node/<node>`. The group segment is empty at depth 0.
1101	// The fixed `node` category leaves room for sibling categories (e.g.
1102	// `<top-prefix>/<group>/cluster` for relay-mesh stats) under the same prefix.
1103	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	/// Serve a track from `before`, reclaim it, then recreate it on `after` for
1135	/// a subscriber resuming past the last group it saw.
1136	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	/// Build an origin producer, spawning its driver on the ambient runtime.
1182	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			// A sync test: nothing polls the driver, and dropping it would tear
1188			// the origin down, so leak it and rely on the synchronous half.
1189			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	/// Kept-alive handles from [`feed`]: dropping them closes the subscription and the
1212	/// announce (bumping the `_closed` counters).
1213	#[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	/// Drive a tagged egress broadcast so `registry` records publisher-side traffic on
1222	/// `path` under `tier`. The local publisher (ingress) is left untagged, so only the
1223	/// egress (publisher) counters move, matching how a relay bills read-out traffic.
1224	///
1225	/// Announces the broadcast; if `subscribe`, opens one subscription and reads a group
1226	/// of `frames` frames of `frame_size` bytes each (so `bytes`/`frames`/`groups` move).
1227	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		// Egress (publisher side) is tagged; the local publisher stays untagged.
1238		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	/// Awaits the stats announce and returns its broadcast.
1283	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	/// Advance past one publish interval so the task drains and writes frames.
1297	async fn drive_tick() {
1298		tokio::time::advance(Duration::from_millis(1100)).await;
1299		// Yield several times to let the task wake, drain the registry, write
1300		// the frames, and re-await the next tick.
1301		for _ in 0..4 {
1302			tokio::task::yield_now().await;
1303		}
1304	}
1305
1306	/// Reads the first frame off a plain track as raw JSON, pinning the plain
1307	/// wire format (a full JSON object per frame, no compression).
1308	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	/// Read the latest buffered traffic frame off a track. The producer emits an
1315	/// immediate first (often empty) frame at time zero, so a test that records
1316	/// traffic asynchronously reads the accumulated state rather than that stale one.
1317	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	/// The next group's first frame. Stats tracks are one frame per group, so this is
1355	/// one published sample.
1356	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	/// The same, without blocking: `None` once nothing more is buffered.
1362	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	/// The advertised path normalizes a messy node suffix and drops an
1369	/// all-empty one. Observed through the announced path, since the task
1370	/// announces at construction.
1371	#[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		// No matter how many broadcasts get bumped, exactly one stats
1383		// broadcast is announced (the per-node aggregate).
1384		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		// One announced broadcast, one subscription, one 42-byte frame read out.
1403		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		// Announce only: the guard records the broadcast-name length once on open.
1424		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		// An announce with no subscription should bump announces_started but NOT broadcasts_started
1442		// (which only counts sessions with an active sub).
1443		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		// A subscription that opens AND closes within a single drain window
1459		// must still surface as a complete broadcasts start/end cycle. The
1460		// cumulative counters retain broadcasts_started=1/broadcasts_ended=1, and the
1461		// entry surfaces because it moved, even though it's net-idle by drain time.
1462		let (producer, origin) = test_producer(Some("sjc"));
1463		{
1464			// Subscribe, read one 123-byte frame, then drop everything within the
1465			// first interval so the open and close both land before the drain.
1466			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		// One session opened then closed a subscription within the drain.
1475		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		// The publisher holds `foo/bar` throughout while viewers come and go, so
1486		// the registry keeps its egress counters between them. A reader diffing
1487		// frames must see the path the whole time: if it vanished, its return
1488		// (carrying the first viewer's bytes) would look like a fresh entry.
1489		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		// Once nothing holds the path the registry drops it, and so does the frame.
1533		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	/// A depth-1 producer with `linger`, its origin, and a hidden announce
1544	/// listener started before anything publishes.
1545	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	/// The announce events buffered so far, as `(path, active)`.
1559	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		// `feed` records traffic on `acme/live` (group `acme`) and holds a
1577		// session rooted at `feed` (group `feed`).
1578		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		// The viewer leaves: both groups empty but stay announced, reading zero.
1588		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		// Another viewer arrives within the linger: same broadcast, no announce.
1599		// The path restarted, so a reader diffing frames counts 100 + 50, the
1600		// same as it would across an unannounce and re-announce.
1601		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		// The return re-armed the linger: it unannounces only once it elapses
1611		// again with the group still empty.
1612		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		// A return after the linger is a new broadcast counted from zero.
1625		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		// The closing readout drain still carries the entry; the next is empty.
1642		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		// A broadcast that only sees default-tier publisher traffic must NOT
1680		// surface on its sibling default-tier subscriber track, and a tier
1681		// with no traffic gets no tracks at all.
1682		let (producer, origin) = test_producer(Some("sjc"));
1683		// Only the egress (publisher) side is tagged, so `foo/bar` gets publisher
1684		// traffic and no subscriber traffic.
1685		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		// Default-tier publisher slot SHOULD include foo/bar.
1693		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		// The default-tier subscriber slot had zero activity; its first frame
1701		// must be `{}`, not `{"foo/bar": {all zeros}}`.
1702		let frame = read_frame(&broadcast, "subscriber.json").await;
1703		assert!(frame.is_empty(), "subscriber.json must be empty, got {frame:?}");
1704
1705		// The compressed siblings of the default tracks always exist.
1706		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		// The regional tier never saw traffic, so no drain created its tracks;
1711		// a subscribe is held open and served zeros instead of being rejected
1712		// (see `serve_requests`), and its slot still never surfaces in the
1713		// frames above.
1714		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	/// A subscribe for a tier that has never recorded resolves with a zero
1782	/// frame instead of being rejected, and the tier's real data later rides
1783	/// the SAME subscription (the retry storm this held open replaces).
1784	#[tokio::test(start_paused = true)]
1785	async fn idle_tier_track_resolves_with_zeros() {
1786		let (producer, origin) = test_producer(Some("sjc"));
1787		// Some default-tier traffic so the group broadcast exists at all.
1788		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		// Nothing has recorded on the rtmp tier: the track does not exist yet.
1793		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		// The tier records: the same subscription carries the data.
1801		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	/// The compressed flavor is adoptable too, and adopting either flavor
1809	/// creates its sibling, so the pair stays in lockstep.
1810	#[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		// The plain sibling was created alongside, so it resolves immediately.
1822		subscribe(&broadcast, "srt/subscriber.json").await;
1823	}
1824
1825	/// A sessions-shaped request is held open with zeros like the traffic ones.
1826	#[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	/// A name no tier could produce is still rejected.
1842	#[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	/// A request queued just before its tier's first traffic must not be
1855	/// stranded: the tick's own `create_track` fulfills the queued request, so
1856	/// the subscriber and the traffic-created pair are one track and the first
1857	/// real frame reaches the waiting subscription.
1858	#[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		// Queue the request and drive it far enough to reach the stats
1866		// broadcast's request queue (the serve chain runs on yields)...
1867		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		// ...then the tier records its first traffic before the next tick.
1874		let _rtmp = feed(producer.registry(), Tier::new("rtmp"), "foo/live", true, 1, 7).await;
1875		drive_tick().await;
1876
1877		// The queued subscription resolves and carries the tier's first data.
1878		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	/// The requested-pair quota binds only while its subscriptions are held,
1888	/// and never terminally rejects a valid collector: an over-quota request
1889	/// parks until the quota frees (here, a prober disconnecting), then the
1890	/// SAME subscription resolves.
1891	#[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		// Fill the whole quota and HOLD it.
1899		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		// While held, the next request parks: pending, not rejected.
1908		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		// Disconnecting frees the quota: once the origin releases its idle
1917		// copies (the track linger) the next drains reclaim the junk pairs and
1918		// adopt the parked request, resolving the SAME subscription. Yield
1919		// first so the serve tasks observe the demand edge and ARM the linger,
1920		// then advance past it, then let a few drains observe the releases.
1921		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	/// A parked request whose tier records while parked is adopted by the
1935	/// flush itself (quota-exempt, it is traffic-backed now), so the waiting
1936	/// subscription resolves with the tier's first data instead of being
1937	/// stranded on a name that meanwhile exists.
1938	#[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		// Fill the whole quota and HOLD it, so the next request parks.
1946		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		// The tier records while the request is parked: the flush adopts it.
1962		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		// The producer's reused frame must stay byte-identical to the
1973		// `TrafficFrame` consumers parse, including key order.
1974		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	/// Counts this thread's allocations, so the test below measures only its
1991	/// own drain while other tests run in parallel.
1992	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		// Once every group, track, and buffer exists, draining the registry
2029		// into the pending frames allocates nothing, however many broadcasts
2030		// and tiers there are. Encoding the frames (moq-json) is out of scope.
2031		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(&registry, 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				// Warm up: create the groups and tracks and grow every buffer.
2058				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}