use std::collections::{BTreeMap, HashMap};
use std::task::Poll;
use moq_net::kio::{self, Pending, Waiter};
use moq_net::stats::{Presence, Role, Tier, Traffic};
use moq_net::track::Subscribing;
use moq_net::{PathOwned, origin};
use crate::{Result, SessionsFrame, TrafficFrame, parse_node_path, sessions_track, traffic_track};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Config {
pub prefix: PathOwned,
pub depth: usize,
pub compression: bool,
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn with_prefix(mut self, prefix: impl Into<PathOwned>) -> Self {
self.prefix = prefix.into();
self
}
pub fn with_depth(mut self, depth: usize) -> Self {
self.depth = depth;
self
}
pub fn with_compression(mut self, compression: bool) -> Self {
self.compression = compression;
self
}
}
impl Default for Config {
fn default() -> Self {
Self {
prefix: PathOwned::from(".stats"),
depth: 0,
compression: false,
}
}
}
pub struct Consumer {
origin: origin::Consumer,
config: Config,
}
impl Consumer {
pub fn new(origin: origin::Consumer, config: Config) -> Self {
Self { origin, config }
}
pub fn traffic(&self, tier: &Tier, role: Role) -> TrafficConsumer {
let name = traffic_track(tier, role, self.config.compression);
TrafficConsumer {
inner: Merged::new(self.origin.announced(), &self.config, name),
}
}
pub fn sessions(&self, tier: &Tier) -> SessionsConsumer {
let name = sessions_track(tier, self.config.compression);
SessionsConsumer {
inner: Merged::new(self.origin.announced(), &self.config, name),
}
}
}
pub struct TrafficConsumer {
inner: Merged<Traffic>,
}
impl TrafficConsumer {
pub async fn next(&mut self) -> Result<Option<TrafficFrame>> {
kio::wait(|waiter| self.inner.poll_next(waiter)).await
}
}
pub struct SessionsConsumer {
inner: Merged<Presence>,
}
impl SessionsConsumer {
pub async fn next(&mut self) -> Result<Option<SessionsFrame>> {
kio::wait(|waiter| self.inner.poll_next(waiter)).await
}
}
trait Mergeable: serde::de::DeserializeOwned + Default + Copy + 'static {
fn merge(acc: &mut Self, other: Self);
}
impl Mergeable for Traffic {
fn merge(acc: &mut Self, other: Self) {
acc.add(other);
}
}
impl Mergeable for Presence {
fn merge(acc: &mut Self, other: Self) {
acc.add(other);
}
}
enum Reader<V: Mergeable> {
Subscribing(Pending<Subscribing>),
Active(Box<moq_json::snapshot::Consumer<BTreeMap<String, V>>>),
Ended,
}
struct Node<V: Mergeable> {
reader: Reader<V>,
last: Option<BTreeMap<String, V>>,
}
struct Merged<V: Mergeable> {
announce: moq_net::announce::Consumer,
prefix: PathOwned,
depth: usize,
name: String,
config: moq_json::snapshot::ConsumerConfig,
nodes: HashMap<PathOwned, Node<V>>,
}
impl<V: Mergeable> Merged<V> {
fn new(announce: moq_net::announce::Consumer, config: &Config, name: String) -> Self {
Self {
announce,
prefix: config.prefix.clone(),
depth: config.depth,
name,
config: moq_json::snapshot::ConsumerConfig::default().with_compression(config.compression),
nodes: HashMap::new(),
}
}
fn poll_next(&mut self, waiter: &Waiter) -> Poll<Result<Option<BTreeMap<String, V>>>> {
let mut changed = false;
loop {
match self.announce.poll_next(waiter) {
Poll::Ready(Some(update)) => changed |= self.apply_announce(update),
Poll::Ready(None) => return Poll::Ready(Ok(None)),
Poll::Pending => break,
}
}
let config = &self.config;
let name = self.name.as_str();
for node in self.nodes.values_mut() {
changed |= advance(node, config, name, waiter);
}
if changed {
Poll::Ready(Ok(Some(self.merged())))
} else {
Poll::Pending
}
}
fn apply_announce(&mut self, update: moq_net::announce::Update) -> bool {
let moq_net::announce::Update { path, broadcast } = update;
let absolute = self.announce.absolute(&path).to_owned();
if parse_node_path(&self.prefix, self.depth, &absolute).is_none() {
return false;
}
match broadcast {
Some(broadcast) => match broadcast.track(&self.name) {
Ok(track) => {
let node = Node {
reader: Reader::Subscribing(track.subscribe(None)),
last: None,
};
self.nodes.insert(absolute, node).is_some_and(|old| old.last.is_some())
}
Err(err) => {
tracing::debug!(?err, node = %absolute, name = %self.name, "stats: node missing track");
self.nodes.remove(&absolute).is_some_and(|old| old.last.is_some())
}
},
None => self.nodes.remove(&absolute).is_some_and(|old| old.last.is_some()),
}
}
fn merged(&self) -> BTreeMap<String, V> {
let mut acc: BTreeMap<String, V> = BTreeMap::new();
for node in self.nodes.values() {
if let Some(last) = &node.last {
for (key, value) in last {
V::merge(acc.entry(key.clone()).or_default(), *value);
}
}
}
acc
}
}
fn advance<V: Mergeable>(
node: &mut Node<V>,
config: &moq_json::snapshot::ConsumerConfig,
name: &str,
waiter: &Waiter,
) -> bool {
let mut changed = false;
loop {
match &mut node.reader {
Reader::Subscribing(pending) => match pending.poll_ok(waiter) {
Poll::Ready(Ok(subscriber)) => {
node.reader =
Reader::Active(Box::new(moq_json::snapshot::Consumer::new(subscriber, config.clone())));
}
Poll::Ready(Err(err)) => {
tracing::debug!(?err, name, "stats: node subscribe failed");
node.reader = Reader::Ended;
return changed;
}
Poll::Pending => return changed,
},
Reader::Active(reader) => match reader.poll_next(waiter) {
Poll::Ready(Ok(Some(frame))) => {
node.last = Some(frame);
changed = true;
}
Poll::Ready(Ok(None)) => return terminate(node, changed),
Poll::Ready(Err(err)) => {
tracing::debug!(?err, name, "stats: node read error");
return terminate(node, changed);
}
Poll::Pending => return changed,
},
Reader::Ended => return changed,
}
}
}
fn terminate<V: Mergeable>(node: &mut Node<V>, changed: bool) -> bool {
let had_value = node.last.take().is_some();
node.reader = Reader::Ended;
changed || had_value
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use moq_net::{Origin, PathOwned, Timestamp, announce, broadcast, origin, track};
use crate::{Producer, ProducerConfig};
use super::*;
fn node_producer(origin: &origin::Producer, node: &str) -> Producer {
Producer::new(
ProducerConfig::new()
.with_origin(origin.clone())
.with_node(PathOwned::from(node.to_string()))
.with_depth(1),
)
}
#[allow(dead_code)]
struct Feed {
announced: announce::Consumer,
source: broadcast::Producer,
consumer: broadcast::Consumer,
sub: track::Subscriber,
ctx: moq_net::stats::Session,
}
async fn feed(producer: &Producer, tier: Tier, root: &str, path: &str, bytes: usize) -> Feed {
let ctx = producer.registry().tier(tier).session(root);
let feed_origin = Origin::random().produce();
let egress = feed_origin.consume().with_stats(ctx.clone());
let mut announced = egress.announced();
let mut source = feed_origin
.create_broadcast(path, broadcast::Route::announced())
.expect("create_broadcast");
let mut track = source.create_track("video", None).expect("create_track");
tokio::time::sleep(Duration::from_millis(1)).await;
tokio::time::sleep(Duration::from_millis(1)).await;
let announce::Update { broadcast, .. } = announced.next().await.expect("announce");
let consumer = broadcast.expect("active");
let mut sub = consumer.track("video").unwrap().subscribe(None).await.unwrap();
let mut group = track.append_group().unwrap();
group.write_frame(Timestamp::ZERO, vec![0u8; bytes]).unwrap();
group.finish().unwrap();
let mut group = sub.recv_group().await.unwrap().unwrap();
while group.read_frame().await.unwrap().is_some() {}
Feed {
announced,
source,
consumer,
sub,
ctx,
}
}
async fn drive_tick() {
tokio::time::advance(Duration::from_millis(1100)).await;
for _ in 0..8 {
tokio::task::yield_now().await;
}
}
async fn read_until_bytes(consumer: &mut TrafficConsumer, path: &str, want: u64) -> TrafficFrame {
loop {
let frame = consumer.next().await.expect("read").expect("frame");
if frame.get(path).map(|t| t.bytes).unwrap_or(0) >= want {
return frame;
}
}
}
#[tokio::test(start_paused = true)]
async fn merges_traffic_across_nodes() {
let origin = Origin::random().produce();
let node_a = node_producer(&origin, "a");
let node_b = node_producer(&origin, "b");
let _fa = feed(&node_a, Tier::default(), "acme", "acme/room", 100).await;
let _fb = feed(&node_b, Tier::default(), "acme", "acme/room", 40).await;
drive_tick().await;
let agg = Consumer::new(origin.consume(), Config::new().with_depth(1));
let mut traffic = agg.traffic(&Tier::default(), Role::Publisher);
let frame = read_until_bytes(&mut traffic, "acme/room", 140).await;
let snap = frame.get("acme/room").expect("entry");
assert_eq!(snap.bytes, 140, "bytes sum across both nodes");
assert_eq!(snap.subscriptions, 2, "one subscription per node");
assert_eq!(snap.broadcasts, 2, "one viewer per node");
}
#[tokio::test(start_paused = true)]
async fn node_drop_regresses_the_merged_view() {
let origin = Origin::random().produce();
let node_a = node_producer(&origin, "a");
let node_b = node_producer(&origin, "b");
let _fa = feed(&node_a, Tier::default(), "acme", "acme/room", 100).await;
let fb = feed(&node_b, Tier::default(), "acme", "acme/room", 40).await;
drive_tick().await;
let agg = Consumer::new(origin.consume(), Config::new().with_depth(1));
let mut traffic = agg.traffic(&Tier::default(), Role::Publisher);
read_until_bytes(&mut traffic, "acme/room", 140).await;
drop(fb);
drop(node_b);
drive_tick().await;
loop {
let frame = traffic.next().await.expect("read").expect("frame");
if frame.get("acme/room").map(|t| t.bytes) == Some(100) {
break;
}
}
}
#[tokio::test(start_paused = true)]
async fn merges_sessions_across_nodes() {
let origin = Origin::random().produce();
let node_a = node_producer(&origin, "a");
let node_b = node_producer(&origin, "b");
let _fa = feed(&node_a, Tier::default(), "acme", "acme/room", 8).await;
let _fb = feed(&node_b, Tier::default(), "acme", "acme/room", 8).await;
let _sa = node_a.registry().tier(Tier::default()).session("acme");
let _sb = node_b.registry().tier(Tier::default()).session("acme");
drive_tick().await;
let agg = Consumer::new(origin.consume(), Config::new().with_depth(1));
let mut sessions = agg.sessions(&Tier::default());
loop {
let frame = sessions.next().await.expect("read").expect("frame");
if frame.get("acme").map(|p| p.active()) >= Some(4) {
break;
}
}
}
}