Skip to main content

moq_net/model/
bandwidth.rs

1//! Rate estimation, split into a [Producer] and [Consumer] handle.
2//!
3//! A [Producer] is used to set the current estimated bitrate, notifying consumers.
4//! A [Consumer] can read the current estimate and wait for changes.
5//!
6//! One estimate covers a whole connection, so senders sharing one divide it with
7//! an [Allocator] rather than each targeting the whole thing. How a sender then
8//! *follows* its share is shared media policy: see `moq_mux::rate`.
9
10use std::task::Poll;
11
12use crate::{Error, Result, track};
13
14/// A rate, in bits per second.
15///
16/// A newtype rather than a bare integer because everything that meets in an
17/// [`Allocator`] is the same quantity measured the same way: a congestion
18/// controller's estimate, a track's reservation, an encoder's ceiling. One of them
19/// reading bytes per second, or kilobits, is off by a factor of eight or a thousand
20/// and still typechecks, which is the kind of wrong that reaches production.
21///
22/// Named for what it measures rather than for the module: `Session::stats` has called
23/// this quantity a rate all along (`estimated_send_rate`).
24#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub struct Rate(u64);
26
27impl Rate {
28	/// No bandwidth at all.
29	pub const ZERO: Self = Self(0);
30
31	/// A rate in bits per second, the unit every wire and codec API here uses.
32	pub const fn from_bps(bps: u64) -> Self {
33		Self(bps)
34	}
35
36	/// A rate in kilobits (1000 bits) per second, saturating.
37	pub const fn from_kbps(kbps: u64) -> Self {
38		Self(kbps.saturating_mul(1_000))
39	}
40
41	/// A rate in megabits (1000 kilobits) per second, saturating.
42	pub const fn from_mbps(mbps: u64) -> Self {
43		Self(mbps.saturating_mul(1_000_000))
44	}
45
46	/// This rate in bits per second, for handing to a codec or FFI that wants a plain integer.
47	pub const fn as_bps(self) -> u64 {
48		self.0
49	}
50
51	/// This rate scaled by `factor`, saturating at both ends.
52	///
53	/// For the fractional arithmetic rate control does (a ramp allowance, a hysteresis
54	/// band) without spreading `as` casts across the callers.
55	pub fn scaled(self, factor: f64) -> Self {
56		let scaled = self.0 as f64 * factor.max(0.0);
57		if scaled >= u64::MAX as f64 {
58			Self(u64::MAX)
59		} else {
60			Self(scaled as u64)
61		}
62	}
63
64	/// The absolute difference between two rates.
65	pub const fn abs_diff(self, other: Self) -> Self {
66		Self(self.0.abs_diff(other.0))
67	}
68}
69
70#[derive(Default)]
71struct State {
72	bitrate: Option<Rate>,
73	abort: Option<Error>,
74}
75
76/// Produces bandwidth estimates, notifying consumers when the value changes.
77#[derive(Clone)]
78pub struct Producer {
79	state: kio::Producer<State>,
80}
81
82impl Producer {
83	/// Create a fresh producer with no current estimate.
84	pub fn new() -> Self {
85		Self {
86			state: kio::Producer::default(),
87		}
88	}
89
90	/// Set the current bandwidth estimate, or `None` while the backend has none.
91	pub fn set(&self, bitrate: Option<Rate>) -> Result<()> {
92		let mut state = self.modify()?;
93		if state.bitrate != bitrate {
94			state.bitrate = bitrate;
95		}
96		Ok(())
97	}
98
99	/// Create a new consumer for the bandwidth estimate.
100	pub fn consume(&self) -> Consumer {
101		Consumer {
102			inner: Inner::Whole(self.state.consume()),
103			last: None,
104		}
105	}
106
107	/// A handle that mints consumers without being one, so holding it is not demand.
108	pub(crate) fn weak(&self) -> Weak {
109		Weak(self.state.weak())
110	}
111
112	/// Close the producer with an error, notifying all consumers.
113	pub fn abort(&self, err: Error) -> Result<()> {
114		let mut state = self.modify()?;
115		state.abort = Some(err);
116		state.close();
117		Ok(())
118	}
119
120	/// Block until the channel is closed, returning the cause.
121	pub async fn closed(&self) -> Error {
122		self.state.closed().await;
123		self.close_error()
124	}
125
126	/// Block until there are no active consumers.
127	pub async fn unused(&self) -> Result<()> {
128		kio::wait(|waiter| self.poll_unused(waiter)).await
129	}
130
131	/// Poll until there are no active consumers. Errors if the channel closes first.
132	pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<Result<()>> {
133		self.state.poll_unused(waiter).map(|used| match used {
134			Some(()) => Ok(()),
135			None => Err(self.close_error()),
136		})
137	}
138
139	/// Whether at least one active consumer exists right now.
140	pub fn is_used(&self) -> bool {
141		self.state.is_used()
142	}
143
144	/// Block until there is at least one active consumer.
145	pub async fn used(&self) -> Result<()> {
146		kio::wait(|waiter| self.poll_used(waiter)).await
147	}
148
149	/// Poll until at least one active consumer exists. Errors if the channel closes first.
150	pub fn poll_used(&self, waiter: &kio::Waiter) -> Poll<Result<()>> {
151		self.state.poll_used(waiter).map(|used| match used {
152			Some(()) => Ok(()),
153			None => Err(self.close_error()),
154		})
155	}
156
157	fn modify(&self) -> Result<kio::Mut<'_, State>> {
158		self.state
159			.write()
160			.map_err(|r| r.abort.clone().unwrap_or(Error::Dropped))
161	}
162
163	/// The close error, once the channel is closed.
164	fn close_error(&self) -> Error {
165		self.state.read().abort.clone().unwrap_or(Error::Dropped)
166	}
167}
168
169impl Default for Producer {
170	fn default() -> Self {
171		Self::new()
172	}
173}
174
175/// Mints [`Consumer`]s on demand without counting as one, nor keeping the channel open.
176#[derive(Clone)]
177pub(crate) struct Weak(kio::ProducerWeak<State>);
178
179impl Weak {
180	/// A new consumer, which reports the closure once the producer is gone.
181	pub(crate) fn consume(&self) -> Consumer {
182		Consumer {
183			inner: Inner::Whole(self.0.consume()),
184			last: None,
185		}
186	}
187}
188
189/// Divides one connection's bandwidth estimate among the tracks sharing it.
190///
191/// Every sender on a connection reads the same estimate, so N senders each
192/// targeting all of it oversubscribe the uplink N times over. Register a track
193/// here and it gets a [`Consumer`] reporting only its own slice, so the slices
194/// sum to the estimate instead of each matching it.
195///
196/// Advisory, not enforced. A track that ignores its slice, or can't follow it at
197/// all (PCM audio has a fixed bitrate), still sends what it sends; the transport
198/// sheds the excess by dropping groups. Rate estimation isn't exact enough
199/// for the difference to be worth policing.
200///
201/// Clones share one registry, so hand a clone to each sender.
202#[derive(Clone)]
203pub struct Allocator {
204	estimate: Consumer,
205	registry: kio::Producer<Registry>,
206}
207
208impl Allocator {
209	/// Divide `estimate`, normally a connection's
210	/// [`Session::send_bandwidth`](crate::Session::send_bandwidth).
211	pub fn new(estimate: Consumer) -> Self {
212		Self {
213			estimate,
214			registry: kio::Producer::default(),
215		}
216	}
217
218	/// An allocator with nothing to divide, so every reservation reports `None`.
219	///
220	/// `None` already means "no opinion, hold your rate" to a sender, so this is what
221	/// a transport with no congestion estimate, a local file, or a test harness wants,
222	/// and it saves every config struct on the way down from being an `Option`. It is
223	/// also [`Default`], so a config that never sets one encodes at its configured rate.
224	pub fn unlimited() -> Self {
225		Self {
226			estimate: Consumer {
227				inner: Inner::Unavailable,
228				last: None,
229			},
230			registry: kio::Producer::default(),
231		}
232	}
233
234	/// Reserve up to `max` for `track`, returning the reservation.
235	///
236	/// `max` is a ceiling, not a measurement: reserve the most the track can ever
237	/// send, not what it happens to be sending. A VBR encoder sitting on a black
238	/// screen at 1 Mbps can jump to 6 Mbps between one frame and the next, and a
239	/// reservation that had followed it down would have already handed that room
240	/// to somebody else.
241	///
242	/// Priority comes from the track ([`track::Info::priority`], higher served
243	/// first). A tier is filled to its reservations before the next one sees a
244	/// bit; within a tier the split is max-min fair, so a share asking for less
245	/// than an even cut takes all of it and leaves the difference to the others.
246	///
247	/// That is the *publisher's* priority, which is not what orders the local send
248	/// queue: that ranks by each subscription's own priority, so a subscriber
249	/// asking for video ahead of audio is served that way whatever this decides.
250	/// The publisher's is still the right one to divide by, since allocation is a
251	/// decision about what to *produce*, and there is no single subscriber
252	/// priority to read when several are watching one track.
253	///
254	/// That last part carries a publisher that leaves `priority` at its default:
255	/// one tier of audio and video still serves audio's small reservation in full
256	/// before video takes the remainder.
257	///
258	/// The reservation lasts as long as the returned [`Reservation`]: hold it for as
259	/// long as the sender is publishing, change the ceiling with
260	/// [`update`](Reservation::update), and drop it to hand the room back. It is
261	/// released when the track closes either way, since a [`track::Demand`] is a weak
262	/// handle and reserving never keeps a track alive.
263	///
264	/// Read the current slice through [`Reservation::consumer`], which reports `None`
265	/// while nothing is subscribed to the track or the connection has no estimate.
266	/// That tells a sender to hold its current rate rather than encode at zero.
267	pub fn reserve(&self, track: &track::Demand, max: Rate) -> Reservation {
268		// Read the track before taking the registry lock, so the two are never held
269		// at once and there's no order to get wrong.
270		let priority = track.priority();
271		let demand = track.clone();
272
273		let id = {
274			// Nothing ever closes this channel: the allocator holds the only producer
275			// and never aborts it, so it's open for as long as `self` is.
276			let Ok(mut registry) = self.registry.write() else {
277				unreachable!("the allocator holds its own registry producer")
278			};
279			// Closed tracks can't be demanded again; drop them rather than walking
280			// them on every poll for the rest of the connection.
281			registry.entries.retain(|entry| !entry.demand.is_closed());
282
283			let id = registry.next_id;
284			registry.next_id += 1;
285			registry.entries.push(Entry {
286				id,
287				demand,
288				priority,
289				max,
290			});
291			id
292		};
293
294		Reservation {
295			share: Share {
296				estimate: self.estimate.clone(),
297				registry: self.registry.consume(),
298				id,
299			},
300			registry: self.registry.downgrade(),
301		}
302	}
303}
304
305/// One track's standing claim on an [`Allocator`], held for as long as the sender
306/// that took it is publishing.
307///
308/// Separate from the [`Consumer`] that reads the slice, because the two have opposite
309/// lifetimes: read handles are cloned around and dropped freely, while the claim itself
310/// has to outlive every one of them or the sender silently stops claiming anything.
311#[must_use = "a dropped Reservation is released, so the sender claims nothing and its siblings take the room"]
312pub struct Reservation {
313	share: Share,
314	/// Weak so a reservation can't keep the registry alive: one outliving every
315	/// [`Allocator`] reports the estimate as gone rather than holding the channel open.
316	registry: kio::Weak<Registry>,
317}
318
319impl Reservation {
320	/// This reservation's slice right now.
321	///
322	/// Stateless, unlike [`Consumer::changed`], which carries a cursor over what it last
323	/// reported and so needs a handle of its own.
324	pub fn peek(&self) -> Option<Rate> {
325		self.share.grant()
326	}
327
328	/// A handle reading this reservation's current slice of the estimate.
329	///
330	/// Cloneable and independent of the reservation: once the reservation is dropped
331	/// these report `None`, the same "hold your rate" a track nobody is watching reports.
332	pub fn consumer(&self) -> Consumer {
333		Consumer {
334			inner: Inner::Share(Box::new(self.share.clone())),
335			last: None,
336		}
337	}
338
339	/// Change the ceiling, keeping the same claim.
340	///
341	/// For a sender whose ceiling genuinely moved: an encoder reopening at a resolution
342	/// it negotiated with the device, not an encoder observing its own output. A
343	/// reservation that followed the rate a VBR source happens to be sending would hand
344	/// the room away every time the picture went still, and not have it back when the
345	/// picture moved again.
346	///
347	/// Does nothing once every [`Allocator`] is gone, since there is then nothing left
348	/// dividing anything; a reader learns that from its [`consumer`](Self::consumer).
349	pub fn update(&self, max: Rate) {
350		let Some(registry) = self.registry.upgrade() else {
351			return;
352		};
353		let Ok(mut registry) = registry.write() else {
354			return;
355		};
356		if let Some(entry) = registry.entries.iter_mut().find(|entry| entry.id == self.share.id) {
357			entry.max = max;
358		}
359	}
360}
361
362impl Drop for Reservation {
363	fn drop(&mut self) {
364		let Some(registry) = self.registry.upgrade() else {
365			return;
366		};
367		let Ok(mut registry) = registry.write() else {
368			return;
369		};
370		registry.entries.retain(|entry| entry.id != self.share.id);
371	}
372}
373
374/// An allocator with nothing to divide, so a config that never sets one leaves its
375/// senders at their configured rates. See [`Allocator::unlimited`].
376impl Default for Allocator {
377	fn default() -> Self {
378		Self::unlimited()
379	}
380}
381
382// Hand-written so the config structs that carry an allocator can still derive
383// `Debug`. The registry is the only part worth printing.
384impl std::fmt::Debug for Allocator {
385	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386		f.debug_struct("Allocator")
387			.field("registered", &self.registry.read().entries.len())
388			.finish()
389	}
390}
391
392/// Every track registered with one [`Allocator`].
393#[derive(Default)]
394struct Registry {
395	entries: Vec<Entry>,
396	next_id: u64,
397}
398
399/// One registered track's standing reservation.
400struct Entry {
401	id: u64,
402	demand: track::Demand,
403	priority: u8,
404	max: Rate,
405}
406
407/// A share's view of the estimate it divides.
408#[derive(Clone)]
409struct Share {
410	/// What's being divided, which may itself be a share.
411	estimate: Consumer,
412	registry: kio::Consumer<Registry>,
413	id: u64,
414}
415
416/// One demanded track's claim, snapshotted out of the [`Registry`] so no lock is
417/// held while the tracks themselves are read.
418#[derive(Copy, Clone, Debug, Eq, PartialEq)]
419struct Want {
420	id: u64,
421	priority: u8,
422	max: Rate,
423}
424
425impl Share {
426	/// This share's slice right now.
427	fn grant(&self) -> Option<Rate> {
428		let estimate = self.estimate.peek()?;
429		let wants: Vec<Want> = self
430			.claims()
431			.into_iter()
432			.filter(|(_, demand)| demand.is_used())
433			.map(|(want, _)| want)
434			.collect();
435		allocate(estimate, &wants, self.id)
436	}
437
438	/// This share's slice, arming `waiter` for everything that could move it: the
439	/// estimate, the set of registered tracks, and each track's demand.
440	fn poll_grant(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Rate>>> {
441		// Drain the estimate rather than polling it once. A poll that returns Ready
442		// registers no waker, and this share may still conclude its own slice didn't
443		// move (its reservation caps it, so most estimate changes don't reach it) and
444		// park. Without the drain that park would never be woken by the next move.
445		// The value itself is read below, since an unchanged estimate still needs
446		// re-dividing when the tracks sharing it change.
447		loop {
448			match self.estimate.poll_changed(waiter) {
449				// Moved: go round again, which either finds it settled and arms the
450				// waker, or finds it moved again and makes progress toward the latest.
451				Poll::Ready(Ok(_)) => continue,
452				Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
453				Poll::Pending => break,
454			}
455		}
456
457		// Wake on any registration change. The closure never completes, so the only
458		// ready outcome is the registry being gone, i.e. every allocator dropped.
459		if let Poll::Ready(Err(_)) = self.registry.poll(waiter, |_| Poll::<()>::Pending) {
460			return Poll::Ready(Err(Error::Dropped));
461		}
462
463		let wants: Vec<Want> = self
464			.claims()
465			.into_iter()
466			.filter(|(_, demand)| match demand.poll_state(waiter) {
467				track::DemandState::Active => true,
468				// An idle track claims nothing, so an unwatched encoder's reservation
469				// goes to whoever is actually sending.
470				track::DemandState::Idle => false,
471				// Gone for good, and pruned by the next `register`.
472				track::DemandState::Closed => false,
473			})
474			.map(|(want, _)| want)
475			.collect();
476
477		let grant = self
478			.estimate
479			.peek()
480			.and_then(|estimate| allocate(estimate, &wants, self.id));
481		Poll::Ready(Ok(grant))
482	}
483
484	/// Snapshot the registry so the tracks can be read without holding its lock.
485	fn claims(&self) -> Vec<(Want, track::Demand)> {
486		self.registry
487			.read()
488			.entries
489			.iter()
490			.map(|entry| {
491				(
492					Want {
493						id: entry.id,
494						priority: entry.priority,
495						max: entry.max,
496					},
497					entry.demand.clone(),
498				)
499			})
500			.collect()
501	}
502}
503
504/// Divide `estimate` among `wants`, returning the slice for `id`.
505///
506/// Strict priority: a tier is filled to its reservations before the next tier
507/// sees a bit. Within a tier the split is max-min fair, so a share asking for
508/// less than an even split takes all of it and leaves the rest to the others.
509///
510/// Surplus above the total reserved is left unclaimed rather than spread around.
511/// A reservation is what a sender can use, so handing it more is not a reason to
512/// send more than it was configured for.
513///
514/// `None` when `id` isn't among the wants, which is how an idle or closed track
515/// reports "hold your rate" instead of a grant of zero.
516fn allocate(estimate: Rate, wants: &[Want], id: u64) -> Option<Rate> {
517	// Plain bits per second inside: this is where the division actually happens, and
518	// wrapping every intermediate would need arithmetic on `Rate` that no caller wants.
519	let mut budget = estimate.as_bps();
520	let mut tier = wants.iter().map(|want| want.priority).max();
521
522	while let Some(priority) = tier {
523		// Ascending by reservation: each share takes an even cut of what's left, or
524		// all it asked for if that's less, which frees the difference for the rest.
525		let mut members: Vec<&Want> = wants.iter().filter(|want| want.priority == priority).collect();
526		members.sort_by_key(|want| want.max);
527
528		let mut remaining = members.len() as u64;
529		for want in members {
530			let even = budget / remaining;
531			let grant = want.max.as_bps().min(even);
532			if want.id == id {
533				return Some(Rate::from_bps(grant));
534			}
535			budget -= grant;
536			remaining -= 1;
537		}
538
539		tier = wants
540			.iter()
541			.map(|want| want.priority)
542			.filter(|other| *other < priority)
543			.max();
544	}
545
546	None
547}
548
549/// Consumes bandwidth estimates, allowing reads and async change notifications.
550#[derive(Clone)]
551pub struct Consumer {
552	inner: Inner,
553	last: Option<Rate>,
554}
555
556/// What a [`Consumer`] is reading: the whole estimate, or one track's slice of it.
557#[derive(Clone)]
558enum Inner {
559	Whole(kio::Consumer<State>),
560	Share(Box<Share>),
561	/// [`Allocator::unlimited`]'s: no estimate, and never will be.
562	Unavailable,
563}
564
565impl Consumer {
566	/// Get the current bandwidth estimate synchronously.
567	pub fn peek(&self) -> Option<Rate> {
568		match &self.inner {
569			Inner::Whole(state) => state.read().bitrate,
570			Inner::Share(share) => share.grant(),
571			Inner::Unavailable => None,
572		}
573	}
574
575	/// Poll for a bandwidth change without blocking.
576	///
577	/// `Ok(None)` means the estimate is unavailable *for now*: the backend
578	/// stopped reporting one, or the handle spans reconnects and is between
579	/// sessions. `Err` means the producer is gone and no further change will ever
580	/// arrive. They're distinct because a caller holds its current rate for the
581	/// first and stops watching for the second.
582	///
583	/// A backend with no bandwidth estimation at all yields no [Consumer] in the
584	/// first place, so that case never reaches here.
585	pub fn poll_changed(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Rate>>> {
586		let last = self.last;
587
588		let bitrate = match &mut self.inner {
589			Inner::Whole(state) => match state.poll(waiter, |state| {
590				if state.bitrate != last {
591					Poll::Ready(state.bitrate)
592				} else {
593					Poll::Pending
594				}
595			}) {
596				Poll::Ready(Ok(bitrate)) => bitrate,
597				// Closed, and the value hasn't moved since the last read: report it as
598				// terminal. Collapsing this into `Ok(None)` would be indistinguishable
599				// from a live-but-unavailable estimate, and since a closed channel is
600				// always immediately ready, a `select!` over it would spin forever.
601				Poll::Ready(Err(state)) => return Poll::Ready(Err(state.abort.clone().unwrap_or(Error::Dropped))),
602				Poll::Pending => return Poll::Pending,
603			},
604			// A share recomputes its slice on every wakeup, so it filters the
605			// unchanged case here rather than inside the poll. Every waker that could
606			// move the slice was armed by `poll_grant` either way.
607			// Nothing to report and nothing that could ever change it, so park without
608			// arming anything rather than reporting a `None` the caller would re-read forever.
609			Inner::Unavailable => return Poll::Pending,
610			Inner::Share(share) => match share.poll_grant(waiter) {
611				Poll::Ready(Ok(grant)) if grant == last => return Poll::Pending,
612				Poll::Ready(Ok(grant)) => grant,
613				Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
614				Poll::Pending => return Poll::Pending,
615			},
616		};
617
618		self.last = bitrate;
619		Poll::Ready(Ok(bitrate))
620	}
621
622	/// Block until the bandwidth estimate changes, returning the new value, or
623	/// `None` when the estimate has become unavailable.
624	///
625	/// # Errors
626	///
627	/// Returns an error once the producer is closed or dropped, so a caller can
628	/// stop watching. See [`poll_changed`](Self::poll_changed).
629	pub async fn changed(&mut self) -> Result<Option<Rate>> {
630		kio::wait(|waiter| self.poll_changed(waiter)).await
631	}
632}
633
634#[cfg(test)]
635mod tests {
636	use super::*;
637	use crate::broadcast;
638
639	/// Priorities matching `hang`'s, which is what the allocator sees in practice.
640	const AUDIO: u8 = 80;
641	const VIDEO: u8 = 60;
642
643	/// Bits per second, so the tables below stay readable.
644	fn bps(bps: u64) -> Rate {
645		Rate::from_bps(bps)
646	}
647
648	fn want(id: u64, priority: u8, max: u64) -> Want {
649		Want {
650			id,
651			priority,
652			max: bps(max),
653		}
654	}
655
656	/// A standalone track at `priority`, plus the broadcast keeping it alive.
657	fn track(priority: u8) -> (broadcast::Producer, track::Producer) {
658		let broadcast = broadcast::Info::default().produce();
659		let track = broadcast
660			.create_track("t", track::Info::default().with_priority(priority))
661			.unwrap();
662		(broadcast, track)
663	}
664
665	#[test]
666	fn strict_priority_fills_the_top_tier_first() {
667		let wants = [want(0, AUDIO, 128_000), want(1, VIDEO, 4_000_000)];
668
669		// Audio takes its reservation off the top; video gets what's left. This is
670		// the job `rate::Policy::headroom` used to approximate with a flat 10%.
671		assert_eq!(allocate(bps(2_000_000), &wants, 0), Some(bps(128_000)));
672		assert_eq!(allocate(bps(2_000_000), &wants, 1), Some(bps(1_872_000)));
673	}
674
675	#[test]
676	fn a_starved_tier_gets_nothing() {
677		let wants = [want(0, AUDIO, 2_000_000), want(1, VIDEO, 4_000_000)];
678
679		assert_eq!(allocate(bps(1_000_000), &wants, 0), Some(bps(1_000_000)));
680		// Strict, not weighted: the lower tier is not owed a floor. Its encoder
681		// clamps to `rate::Policy::min` and the transport sheds what won't fit.
682		assert_eq!(allocate(bps(1_000_000), &wants, 1), Some(bps(0)));
683	}
684
685	/// A publisher that doesn't set [`track::Info::priority`] puts audio and video
686	/// in one tier. That has to come out right anyway, and it does: max-min fair
687	/// satisfies the small claim first, so audio still gets its full reservation
688	/// and video takes the rest. Priority only changes the answer once a tier's
689	/// smaller claims outgrow an even split.
690	#[test]
691	fn one_tier_still_serves_audio_before_video() {
692		let flat = [want(0, 0, 128_000), want(1, 0, 4_000_000)];
693		let tiered = [want(0, AUDIO, 128_000), want(1, VIDEO, 4_000_000)];
694
695		for wants in [flat, tiered] {
696			assert_eq!(allocate(bps(2_000_000), &wants, 0), Some(bps(128_000)));
697			assert_eq!(allocate(bps(2_000_000), &wants, 1), Some(bps(1_872_000)));
698		}
699	}
700
701	#[test]
702	fn an_even_tier_splits_evenly() {
703		let wants = [want(0, VIDEO, 4_000_000), want(1, VIDEO, 4_000_000)];
704
705		assert_eq!(allocate(bps(6_000_000), &wants, 0), Some(bps(3_000_000)));
706		assert_eq!(allocate(bps(6_000_000), &wants, 1), Some(bps(3_000_000)));
707	}
708
709	/// Max-min fair, not an even split: a 360p rung sharing with a 1080p one takes
710	/// only what it asked for and leaves the rest, instead of both being held to half.
711	#[test]
712	fn a_small_share_frees_what_it_does_not_want() {
713		let wants = [want(0, VIDEO, 1_000_000), want(1, VIDEO, 8_000_000)];
714
715		assert_eq!(allocate(bps(6_000_000), &wants, 0), Some(bps(1_000_000)));
716		assert_eq!(allocate(bps(6_000_000), &wants, 1), Some(bps(5_000_000)));
717	}
718
719	/// Capping at the reservation is what keeps an uncongested link encoding at
720	/// exactly the configured rate. Spreading the surplus instead would only matter
721	/// if senders scaled a fraction of their grant, which is what headroom did.
722	#[test]
723	fn surplus_is_left_unclaimed() {
724		assert_eq!(
725			allocate(bps(10_000_000), &[want(0, VIDEO, 4_000_000)], 0),
726			Some(bps(4_000_000))
727		);
728	}
729
730	#[test]
731	fn an_unregistered_share_has_no_grant() {
732		assert_eq!(allocate(bps(1_000_000), &[], 0), None);
733		assert_eq!(allocate(bps(1_000_000), &[want(0, VIDEO, 1_000)], 7), None);
734	}
735
736	/// The headline case: two encoders on one connection must not each target the
737	/// whole estimate.
738	#[tokio::test]
739	async fn concurrent_tracks_split_the_estimate() {
740		let estimate = Producer::new();
741		let allocator = Allocator::new(estimate.consume());
742
743		let (_first_broadcast, first) = track(VIDEO);
744		let _first_sub = first.consume();
745		let first = allocator.reserve(&first.demand(), bps(4_000_000));
746		let mut first_share = first.consumer();
747
748		estimate.set(Some(bps(2_000_000))).unwrap();
749		// Alone, it gets everything it asked for that the link can carry.
750		assert_eq!(first_share.changed().await.unwrap(), Some(bps(2_000_000)));
751
752		// A second encoder starts. The first must give half back without anyone
753		// telling it to, or the two together target 200% of the uplink.
754		let (_second_broadcast, second) = track(VIDEO);
755		let _second_sub = second.consume();
756		let second_share = allocator.reserve(&second.demand(), bps(4_000_000));
757
758		assert_eq!(first_share.changed().await.unwrap(), Some(bps(1_000_000)));
759		assert_eq!(second_share.peek(), Some(bps(1_000_000)));
760	}
761
762	/// An unwatched track releases its reservation, since `publish_capture` stops
763	/// encoding entirely while nothing is subscribed.
764	#[tokio::test]
765	async fn an_idle_track_claims_nothing() {
766		let estimate = Producer::new();
767		let allocator = Allocator::new(estimate.consume());
768		estimate.set(Some(bps(2_000_000))).unwrap();
769
770		let (_watched_broadcast, watched) = track(VIDEO);
771		let _watched_sub = watched.consume();
772		let watched_share = allocator.reserve(&watched.demand(), bps(4_000_000));
773
774		let (_idle_broadcast, idle) = track(VIDEO);
775		let idle_share = allocator.reserve(&idle.demand(), bps(4_000_000));
776
777		assert_eq!(watched_share.peek(), Some(bps(2_000_000)));
778		// Not zero: an idle share reports "no opinion" so a sender that is mid-shutdown
779		// holds its rate instead of retuning to the floor on the way out.
780		assert_eq!(idle_share.peek(), None);
781
782		// It joins, and the split happens.
783		let _idle_sub = idle.consume();
784		assert_eq!(watched_share.peek(), Some(bps(1_000_000)));
785		assert_eq!(idle_share.peek(), Some(bps(1_000_000)));
786	}
787
788	/// Demand transitions have to wake a parked share, not just change what a later
789	/// `peek` would see; the encoder is sitting in a `select!` on `changed`.
790	#[tokio::test]
791	async fn a_share_wakes_when_a_sibling_goes_idle() {
792		let estimate = Producer::new();
793		let allocator = Allocator::new(estimate.consume());
794		estimate.set(Some(bps(2_000_000))).unwrap();
795
796		let (_mine_broadcast, mine) = track(VIDEO);
797		let _mine_sub = mine.consume();
798		let mine_share_reserved = allocator.reserve(&mine.demand(), bps(4_000_000));
799		let mut mine_share = mine_share_reserved.consumer();
800
801		let (_sibling_broadcast, sibling) = track(VIDEO);
802		let sibling_sub = sibling.consume();
803		let _sibling_share = allocator.reserve(&sibling.demand(), bps(4_000_000));
804
805		assert_eq!(mine_share.changed().await.unwrap(), Some(bps(1_000_000)));
806
807		// The sibling's last viewer leaves, so its half comes back to us.
808		drop(sibling_sub);
809		assert_eq!(mine_share.changed().await.unwrap(), Some(bps(2_000_000)));
810	}
811
812	/// Records whether a parked poll was actually woken, which re-reading state on a
813	/// fresh `poll` can't tell you: a lost wakeup still looks correct on the next poll
814	/// and only shows up as a task that never runs again.
815	#[derive(Default)]
816	struct Woken(std::sync::atomic::AtomicBool);
817
818	impl std::task::Wake for Woken {
819		fn wake(self: std::sync::Arc<Self>) {
820			self.wake_by_ref();
821		}
822
823		fn wake_by_ref(self: &std::sync::Arc<Self>) {
824			self.0.store(true, std::sync::atomic::Ordering::SeqCst);
825		}
826	}
827
828	impl Woken {
829		/// A flag and its waiter. Both are returned because a [`kio::WaiterList`]
830		/// holds only a `Weak`, so a waiter dropped at the end of the calling
831		/// statement takes its own registration with it and never fires.
832		fn new() -> (std::sync::Arc<Self>, kio::Waiter) {
833			let flag = std::sync::Arc::new(Self::default());
834			let waiter = kio::Waiter::new(std::task::Waker::from(flag.clone()));
835			(flag, waiter)
836		}
837
838		fn woken(&self) -> bool {
839			self.0.load(std::sync::atomic::Ordering::SeqCst)
840		}
841	}
842
843	/// Regression: a share whose slice didn't move still has to leave the estimate's
844	/// waker armed. Reservations cap the slice, so most estimate changes don't reach
845	/// it, and a poll that returned the value without arming anything would park the
846	/// encoder with nothing left to wake it.
847	#[tokio::test]
848	async fn an_unchanged_slice_keeps_watching_the_estimate() {
849		let estimate = Producer::new();
850		let allocator = Allocator::new(estimate.consume());
851
852		let (_broadcast, track) = track(VIDEO);
853		let _sub = track.consume();
854		let share_reserved = allocator.reserve(&track.demand(), bps(4_000_000));
855		let mut share = share_reserved.consumer();
856
857		estimate.set(Some(bps(10_000_000))).unwrap();
858		assert_eq!(share.changed().await.unwrap(), Some(bps(4_000_000)));
859
860		// Still miles above the reservation, so the slice holds at 4 Mbps: the share
861		// observes the change, decides it doesn't move, and parks.
862		let (woken, waiter) = Woken::new();
863		estimate.set(Some(bps(9_000_000))).unwrap();
864		assert!(share.poll_changed(&waiter).is_pending());
865
866		// The estimate finally drops past the reservation: this has to reach it.
867		estimate.set(Some(bps(1_000_000))).unwrap();
868		assert!(woken.woken(), "a parked share must be woken by the next estimate");
869		assert_eq!(share.changed().await.unwrap(), Some(bps(1_000_000)));
870	}
871
872	/// The same, for the other input: a sibling's demand.
873	#[tokio::test]
874	async fn a_parked_share_is_woken_by_sibling_demand() {
875		let estimate = Producer::new();
876		let allocator = Allocator::new(estimate.consume());
877		estimate.set(Some(bps(2_000_000))).unwrap();
878
879		let (_mine_broadcast, mine) = track(VIDEO);
880		let _mine_sub = mine.consume();
881		let share_reserved = allocator.reserve(&mine.demand(), bps(4_000_000));
882		let mut share = share_reserved.consumer();
883
884		let (_sibling_broadcast, sibling) = track(VIDEO);
885		let sibling_sub = sibling.consume();
886		let _sibling_share = allocator.reserve(&sibling.demand(), bps(4_000_000));
887
888		assert_eq!(share.changed().await.unwrap(), Some(bps(1_000_000)));
889
890		let (woken, waiter) = Woken::new();
891		assert!(share.poll_changed(&waiter).is_pending());
892		drop(sibling_sub);
893		assert!(woken.woken(), "a sibling going idle must wake a parked share");
894	}
895
896	/// A share follows the estimate's own lifecycle: unavailable while disconnected
897	/// (hold the current rate), terminal once the session is gone for good.
898	#[tokio::test]
899	async fn a_share_follows_the_estimate_lifecycle() {
900		let estimate = Producer::new();
901		let allocator = Allocator::new(estimate.consume());
902
903		let (_broadcast, track) = track(VIDEO);
904		let _sub = track.consume();
905		let share_reserved = allocator.reserve(&track.demand(), bps(4_000_000));
906		let mut share = share_reserved.consumer();
907
908		estimate.set(Some(bps(2_000_000))).unwrap();
909		assert_eq!(share.changed().await.unwrap(), Some(bps(2_000_000)));
910
911		estimate.set(None).unwrap();
912		assert_eq!(share.changed().await.unwrap(), None);
913
914		estimate.abort(Error::Cancel).unwrap();
915		assert!(share.changed().await.is_err());
916		assert!(share.changed().await.is_err());
917	}
918
919	/// A closed track's entry can't linger: it would be walked on every poll for the
920	/// rest of the connection, and a long-lived publisher churns tracks.
921	#[tokio::test]
922	async fn a_closed_track_is_pruned() {
923		let estimate = Producer::new();
924		let allocator = Allocator::new(estimate.consume());
925
926		// Both shares are held: dropping one releases its reservation on its own,
927		// which would prove nothing about pruning the track that closed.
928		let (_first_broadcast, first) = track(VIDEO);
929		let _first_share = allocator.reserve(&first.demand(), bps(4_000_000));
930		first.abort(Error::Cancel).unwrap();
931
932		let (_second_broadcast, second) = track(VIDEO);
933		let _second_share = allocator.reserve(&second.demand(), bps(4_000_000));
934
935		assert_eq!(allocator.registry.read().entries.len(), 1);
936	}
937
938	/// Dropping the reservation hands the room back. The registry only ever prunes
939	/// tracks that have *closed*, so a claim left behind by a track that is still
940	/// publishing would stand for the rest of the connection.
941	#[tokio::test]
942	async fn dropping_a_reservation_releases_it() {
943		let estimate = Producer::new();
944		let allocator = Allocator::new(estimate.consume());
945		estimate.set(Some(bps(2_000_000))).unwrap();
946
947		let (_first_broadcast, first) = track(VIDEO);
948		let _first_sub = first.consume();
949		let first_reserved = allocator.reserve(&first.demand(), bps(4_000_000));
950
951		let (_second_broadcast, second) = track(VIDEO);
952		let _second_sub = second.consume();
953		let second_reserved = allocator.reserve(&second.demand(), bps(4_000_000));
954		assert_eq!(second_reserved.peek(), Some(bps(1_000_000)));
955
956		// A read handle is not the claim: the reservation outliving it is what keeps the
957		// room, and the reservation going away is what returns it.
958		let mut orphan = first_reserved.consumer();
959		assert_eq!(orphan.changed().await.unwrap(), Some(bps(1_000_000)));
960
961		drop(first_reserved);
962		assert_eq!(allocator.registry.read().entries.len(), 1);
963		assert_eq!(second_reserved.peek(), Some(bps(2_000_000)));
964
965		// The orphaned reader is woken and told, rather than being left parked on a slice
966		// that will never move again. It reports the same "hold your rate" as an unwatched
967		// track, not a grant of zero that would tell an encoder to stop.
968		assert_eq!(orphan.changed().await.unwrap(), None);
969		assert_eq!(orphan.peek(), None);
970	}
971
972	/// A sender whose ceiling moved (a capture reopening at a resolution it negotiated
973	/// with the device) changes the claim in place. Re-reserving instead would claim
974	/// twice, since nothing releases the first entry while the track is still alive.
975	#[tokio::test]
976	async fn update_changes_the_claim_in_place() {
977		let estimate = Producer::new();
978		let allocator = Allocator::new(estimate.consume());
979		estimate.set(Some(bps(6_000_000))).unwrap();
980
981		let (_small_broadcast, small) = track(VIDEO);
982		let _small_sub = small.consume();
983		let small_reserved = allocator.reserve(&small.demand(), bps(1_000_000));
984
985		let (_large_broadcast, large) = track(VIDEO);
986		let _large_sub = large.consume();
987		let large_reserved = allocator.reserve(&large.demand(), bps(8_000_000));
988
989		// Max-min fair: the small claim is satisfied in full, the rest goes to the other.
990		assert_eq!(small_reserved.peek(), Some(bps(1_000_000)));
991		assert_eq!(large_reserved.peek(), Some(bps(5_000_000)));
992
993		// It reopens at a mode that can use much more, and the split follows without a
994		// second entry appearing.
995		small_reserved.update(bps(4_000_000));
996		assert_eq!(allocator.registry.read().entries.len(), 2);
997		assert_eq!(small_reserved.peek(), Some(bps(3_000_000)));
998		assert_eq!(large_reserved.peek(), Some(bps(3_000_000)));
999
1000		// And back down, which has to release the difference rather than hold it.
1001		small_reserved.update(bps(1_000_000));
1002		assert_eq!(large_reserved.peek(), Some(bps(5_000_000)));
1003	}
1004
1005	/// A reader parked on `changed` has to be woken by its own reservation moving, not
1006	/// just by the estimate or a sibling: the encoder is sitting in a `select!` on it.
1007	#[tokio::test]
1008	async fn update_wakes_a_parked_reader() {
1009		let estimate = Producer::new();
1010		let allocator = Allocator::new(estimate.consume());
1011		estimate.set(Some(bps(6_000_000))).unwrap();
1012
1013		let (_broadcast, producer) = track(VIDEO);
1014		let _sub = producer.consume();
1015		let reserved = allocator.reserve(&producer.demand(), bps(1_000_000));
1016		let mut share = reserved.consumer();
1017		assert_eq!(share.changed().await.unwrap(), Some(bps(1_000_000)));
1018
1019		let (woken, waiter) = Woken::new();
1020		assert!(share.poll_changed(&waiter).is_pending());
1021
1022		reserved.update(bps(4_000_000));
1023		assert!(woken.woken(), "raising the ceiling must wake the reader");
1024		assert_eq!(share.changed().await.unwrap(), Some(bps(4_000_000)));
1025	}
1026
1027	/// The registry is the allocator's, not a share's: a share that outlives every
1028	/// allocator reports the estimate as gone rather than keeping the channel open.
1029	#[tokio::test]
1030	async fn a_share_outliving_the_allocator_reports_closed() {
1031		let estimate = Producer::new();
1032		let allocator = Allocator::new(estimate.consume());
1033
1034		let (_broadcast, producer) = track(VIDEO);
1035		let _sub = producer.consume();
1036		let share_reserved = allocator.reserve(&producer.demand(), bps(4_000_000));
1037		let mut share = share_reserved.consumer();
1038
1039		drop(allocator);
1040		drop(estimate);
1041		assert!(share.changed().await.is_err());
1042	}
1043
1044	/// An unavailable estimate and a dead producer must not look alike: a caller
1045	/// holds its rate for the former and stops watching for the latter.
1046	/// Reporting closure as `Ok(None)` would spin any `select!` over `changed()`,
1047	/// because a closed channel is always immediately ready.
1048	#[tokio::test]
1049	async fn closed_is_distinct_from_unavailable() {
1050		let producer = Producer::new();
1051		let mut consumer = producer.consume();
1052
1053		producer.set(Some(bps(1_000_000))).unwrap();
1054		assert_eq!(consumer.changed().await.unwrap(), Some(bps(1_000_000)));
1055
1056		// Live, but the estimate went away (e.g. disconnected): still watchable.
1057		producer.set(None).unwrap();
1058		assert_eq!(consumer.changed().await.unwrap(), None);
1059
1060		// Gone for good.
1061		producer.abort(Error::Cancel).unwrap();
1062		assert!(consumer.changed().await.is_err());
1063		// And it stays terminal rather than flapping back to a value.
1064		assert!(consumer.changed().await.is_err());
1065	}
1066}