use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Weak};
use std::time::Duration;
use moq_net::stats::{Presence, Registry, Role, Tier, Traffic};
use moq_net::{Path, PathOwned, broadcast, origin};
use serde::Serialize;
use web_async::spawn;
use crate::{COMPRESSED_SUFFIX, SessionsFrame, TrafficFrame, sessions_track, traffic_track};
#[derive(Clone)]
#[non_exhaustive]
pub struct ProducerConfig {
pub origin: Option<origin::Producer>,
pub prefix: PathOwned,
pub node: Option<PathOwned>,
pub interval: Duration,
pub depth: usize,
}
impl ProducerConfig {
pub fn new() -> Self {
Self {
origin: None,
prefix: PathOwned::from(".stats"),
node: None,
interval: Duration::from_secs(1),
depth: 0,
}
}
pub fn with_origin(mut self, origin: impl Into<Option<origin::Producer>>) -> Self {
self.origin = origin.into();
self
}
pub fn with_prefix(mut self, prefix: impl Into<PathOwned>) -> Self {
self.prefix = prefix.into();
self
}
pub fn with_interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
pub fn with_node(mut self, node: impl Into<Option<PathOwned>>) -> Self {
self.node = node.into();
self
}
pub fn with_depth(mut self, depth: usize) -> Self {
self.depth = depth;
self
}
}
impl Default for ProducerConfig {
fn default() -> Self {
Self::new()
}
}
struct Keepalive;
#[derive(Clone)]
pub struct Producer {
registry: Registry,
_keepalive: Option<Arc<Keepalive>>,
}
impl Producer {
pub fn new(config: ProducerConfig) -> Self {
let ProducerConfig {
origin,
prefix,
node,
interval,
depth,
} = config;
let node = node.filter(|p| !p.is_empty());
let Some(origin) = origin else {
return Self {
registry: Registry::disabled(),
_keepalive: None,
};
};
let registry = Registry::new(moq_net::stats::Config::new().with_exclude(prefix.clone()));
let keepalive = Arc::new(Keepalive);
let task = Task {
registry: registry.clone(),
origin,
prefix,
node,
depth,
interval,
};
spawn(task.run(Arc::downgrade(&keepalive)));
Self {
registry,
_keepalive: Some(keepalive),
}
}
pub fn registry(&self) -> &Registry {
&self.registry
}
}
struct Task {
registry: Registry,
origin: origin::Producer,
prefix: PathOwned,
node: Option<PathOwned>,
depth: usize,
interval: Duration,
}
impl Task {
async fn run(self, weak: Weak<Keepalive>) {
let node = self.node.as_ref().map(|p| p.as_str());
let mut groups: HashMap<PathOwned, GroupPublisher> = HashMap::new();
if self.depth == 0 {
let Some(group) = GroupPublisher::create(&self.origin, &self.prefix, &Path::empty(), node) else {
return;
};
groups.insert(Path::empty().to_owned(), group);
}
let mut ticker = web_async::time::interval(self.interval);
ticker.set_missed_tick_behavior(web_async::time::MissedTickBehavior::Delay);
loop {
ticker.tick().await;
if weak.upgrade().is_none() {
for (_, mut publisher) in groups.drain() {
publisher.broadcast.finish();
}
return;
}
let report = self.registry.report();
let mut entries_by_group: HashMap<PathOwned, Vec<&moq_net::stats::TrafficEntry>> = HashMap::new();
for entry in &report.traffic {
entries_by_group
.entry(group_key(entry.path.as_str(), self.depth))
.or_default()
.push(entry);
}
let mut sessions_by_group: HashMap<PathOwned, Vec<&moq_net::stats::SessionEntry>> = HashMap::new();
for entry in &report.sessions {
sessions_by_group
.entry(group_key(entry.root.as_str(), self.depth))
.or_default()
.push(entry);
}
let mut active: HashSet<PathOwned> = HashSet::new();
active.extend(entries_by_group.keys().cloned());
active.extend(sessions_by_group.keys().cloned());
if self.depth == 0 {
active.insert(Path::empty().to_owned());
}
for group in &active {
if !groups.contains_key(group) {
let Some(publisher) = GroupPublisher::create(&self.origin, &self.prefix, group, node) else {
continue;
};
groups.insert(group.clone(), publisher);
}
let publisher = groups.get_mut(group).expect("just inserted");
let mut frames: HashMap<String, TrafficFrame> = HashMap::new();
if let Some(group_entries) = entries_by_group.get(group) {
for entry in group_entries {
let slots = publisher
.local
.entry(entry.path.clone())
.or_default()
.entry(entry.tier.clone())
.or_default();
process_slot(entry.publisher, &mut slots.publisher, |snap| {
frames
.entry(traffic_track(&entry.tier, Role::Publisher, false))
.or_default()
.insert(entry.path.as_str().to_string(), snap);
});
process_slot(entry.subscriber, &mut slots.subscriber, |snap| {
frames
.entry(traffic_track(&entry.tier, Role::Subscriber, false))
.or_default()
.insert(entry.path.as_str().to_string(), snap);
});
}
}
let mut session_frames: HashMap<String, SessionsFrame> = HashMap::new();
if let Some(group_sessions) = sessions_by_group.get(group) {
for entry in group_sessions {
let state = publisher
.session_local
.entry(entry.tier.clone())
.or_default()
.entry(entry.root.clone())
.or_default();
process_session_slot(entry.presence, state, |snap| {
session_frames
.entry(sessions_track(&entry.tier, false))
.or_default()
.insert(entry.root.as_str().to_string(), snap);
});
}
}
flush_dynamic(&mut publisher.broadcast, &mut publisher.traffic_tracks, &frames);
flush_dynamic(&mut publisher.broadcast, &mut publisher.session_tracks, &session_frames);
}
let reported: HashSet<(&PathOwned, &Tier)> =
report.traffic.iter().map(|entry| (&entry.path, &entry.tier)).collect();
let reported_sessions: HashSet<(&Tier, &PathOwned)> =
report.sessions.iter().map(|entry| (&entry.tier, &entry.root)).collect();
for publisher in groups.values_mut() {
publisher.local.retain(|path, tiers| {
tiers.retain(|tier, _| reported.contains(&(path, tier)));
!tiers.is_empty()
});
publisher.session_local.retain(|tier, roots| {
roots.retain(|root, _| reported_sessions.contains(&(tier, root)));
!roots.is_empty()
});
}
let evicted: Vec<PathOwned> = groups
.keys()
.filter(|group| !active.contains(*group))
.cloned()
.collect();
for group in evicted {
if let Some(mut publisher) = groups.remove(&group) {
publisher.broadcast.finish();
}
}
}
}
}
struct TrackPair<T> {
plain: moq_json::snapshot::Producer<T>,
compressed: moq_json::snapshot::Producer<T>,
}
impl<T: Serialize> TrackPair<T> {
fn create(broadcast: &mut broadcast::Producer, name: &str) -> Result<Self, moq_net::Error> {
let plain_track = broadcast.create_track(name, None)?;
let compressed_track = broadcast.create_track(format!("{name}{COMPRESSED_SUFFIX}").as_str(), None)?;
let plain_config = moq_json::snapshot::ProducerConfig::default().with_delta_ratio(0);
let compressed_config = moq_json::snapshot::ProducerConfig::default().with_compression(true);
Ok(Self {
plain: moq_json::snapshot::Producer::new(plain_track, plain_config),
compressed: moq_json::snapshot::Producer::new(compressed_track, compressed_config),
})
}
fn update(&mut self, name: &str, frame: &T) {
if let Err(err) = self.plain.update(frame) {
tracing::debug!(?err, name, "stats: failed to write frame");
}
if let Err(err) = self.compressed.update(frame) {
tracing::debug!(?err, name, "stats: failed to write compressed frame");
}
}
}
fn flush_dynamic<T: Serialize + Default>(
broadcast: &mut broadcast::Producer,
tracks: &mut HashMap<String, TrackPair<T>>,
frames: &HashMap<String, T>,
) {
for name in frames.keys() {
if !tracks.contains_key(name) {
match TrackPair::create(broadcast, name) {
Ok(pair) => {
tracks.insert(name.clone(), pair);
}
Err(err) => tracing::warn!(?err, name, "stats: failed to create track"),
}
}
}
let empty = T::default();
for (name, pair) in tracks.iter_mut() {
pair.update(name, frames.get(name).unwrap_or(&empty));
}
}
struct GroupPublisher {
broadcast: broadcast::Producer,
traffic_tracks: HashMap<String, TrackPair<TrafficFrame>>,
session_tracks: HashMap<String, TrackPair<SessionsFrame>>,
local: HashMap<PathOwned, HashMap<Tier, SideSlots>>,
session_local: HashMap<Tier, HashMap<PathOwned, SessionSlotState>>,
}
impl GroupPublisher {
fn create(origin: &origin::Producer, prefix: &Path, group: &Path, node: Option<&str>) -> Option<Self> {
let advertised = advertised_path(prefix, group, node);
let mut broadcast = match origin.create_broadcast(&advertised, broadcast::Route::new().with_announce(true)) {
Ok(broadcast) => broadcast,
Err(err) => {
tracing::warn!(advertised = %advertised, ?err, "stats: origin rejected stats broadcast");
return None;
}
};
tracing::debug!(advertised = %advertised, "stats: publishing broadcast");
let mut traffic_tracks = HashMap::new();
let mut session_tracks = HashMap::new();
let tier = Tier::default();
for role in [Role::Publisher, Role::Subscriber] {
let name = traffic_track(&tier, role, false);
match TrackPair::create(&mut broadcast, &name) {
Ok(pair) => {
traffic_tracks.insert(name, pair);
}
Err(err) => {
tracing::warn!(?err, name, "stats: failed to create track");
return None;
}
}
}
let name = sessions_track(&tier, false);
match TrackPair::create(&mut broadcast, &name) {
Ok(pair) => {
session_tracks.insert(name, pair);
}
Err(err) => {
tracing::warn!(?err, name, "stats: failed to create track");
return None;
}
}
Some(Self {
broadcast,
traffic_tracks,
session_tracks,
local: HashMap::new(),
session_local: HashMap::new(),
})
}
}
#[derive(Default)]
struct SlotState {
prev_emitted: Option<Traffic>,
}
#[derive(Default)]
struct SideSlots {
publisher: SlotState,
subscriber: SlotState,
}
#[derive(Default)]
struct SessionSlotState {
prev_emitted: Option<Presence>,
}
fn process_slot(snap: Traffic, slot_state: &mut SlotState, emit: impl FnOnce(Traffic)) {
let live = !snap.is_idle();
let prev_snap = slot_state.prev_emitted.unwrap_or_default();
let changed = snap != prev_snap;
if changed {
slot_state.prev_emitted = Some(snap);
}
if live || changed {
emit(snap);
}
}
fn process_session_slot(snap: Presence, slot_state: &mut SessionSlotState, emit: impl FnOnce(Presence)) {
let live = snap.active() > 0;
let prev_snap = slot_state.prev_emitted.unwrap_or_default();
let changed = snap != prev_snap;
if changed {
slot_state.prev_emitted = Some(snap);
}
if live || changed {
emit(snap);
}
}
fn group_key(path: &str, depth: usize) -> PathOwned {
if depth == 0 {
return Path::empty().to_owned();
}
let mut seen = 0;
let mut end = path.len();
for (i, b) in path.bytes().enumerate() {
if b == b'/' {
seen += 1;
if seen == depth {
end = i;
break;
}
}
}
Path::new(&path[..end]).to_owned()
}
fn advertised_path(prefix: &Path, group: &Path, node: Option<&str>) -> PathOwned {
let mut out = prefix.as_str().to_string();
if !group.is_empty() {
out.push('/');
out.push_str(group.as_str());
}
out.push_str("/node");
if let Some(node) = node {
out.push('/');
out.push_str(node);
}
PathOwned::from(out)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use moq_net::stats::{Registry, Tier};
use moq_net::{Origin, Timestamp, announce, broadcast, track};
use super::*;
fn test_producer(node: Option<&str>) -> (Producer, origin::Producer) {
let origin = Origin::random().produce();
let producer = Producer::new(
ProducerConfig::new()
.with_origin(origin.clone())
.with_node(node.map(|s| PathOwned::from(s.to_string()))),
);
(producer, origin)
}
#[allow(dead_code)]
struct Feed {
announced: announce::Consumer,
source: broadcast::Producer,
consumer: broadcast::Consumer,
sub: Option<track::Subscriber>,
}
async fn feed(
registry: &Registry,
tier: Tier,
path: &str,
subscribe: bool,
frames: usize,
frame_size: usize,
) -> Feed {
let ctx = registry.tier(tier).session("feed");
let origin = Origin::random().produce();
let egress = origin.consume().with_stats(ctx);
let mut announced = egress.announced();
let mut source = origin
.create_broadcast(path, broadcast::Route::announced())
.expect("create_broadcast");
let mut producer = 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 sub = if subscribe {
let mut sub = consumer
.track("video")
.expect("track")
.subscribe(None)
.await
.expect("subscribe");
if frames > 0 {
let mut group = producer.append_group().expect("group");
for _ in 0..frames {
group
.write_frame(Timestamp::ZERO, vec![0u8; frame_size])
.expect("write");
}
group.finish().expect("finish");
let mut group = sub.recv_group().await.expect("recv").expect("group");
while group.read_frame().await.expect("read").is_some() {}
}
Some(sub)
} else {
None
};
Feed {
announced,
source,
consumer,
sub,
}
}
async fn announced(origin: &origin::Producer) -> (String, moq_net::broadcast::Consumer) {
let mut consumer = origin.consume().announced();
tokio::time::advance(Duration::from_millis(1)).await;
let announce::Update { path, broadcast } = consumer.next().await.expect("expected announce");
(path.as_str().to_string(), broadcast.expect("active"))
}
async fn drive_tick() {
tokio::time::advance(Duration::from_millis(1100)).await;
for _ in 0..4 {
tokio::task::yield_now().await;
}
}
async fn read_frame(broadcast: &moq_net::broadcast::Consumer, name: &str) -> BTreeMap<String, Traffic> {
let mut track = subscribe(broadcast, name).await;
let frame = track.read_frame().await.expect("ok").expect("frame");
serde_json::from_slice(&frame.payload).expect("json parse")
}
async fn read_last_frame(broadcast: &moq_net::broadcast::Consumer, name: &str) -> BTreeMap<String, Traffic> {
use futures::FutureExt;
let mut track = subscribe(broadcast, name).await;
let mut last = track.read_frame().await.expect("ok").expect("frame");
while let Some(Ok(Some(frame))) = track.read_frame().now_or_never() {
last = frame;
}
serde_json::from_slice(&last.payload).expect("json parse")
}
async fn read_session_frame(broadcast: &moq_net::broadcast::Consumer, name: &str) -> BTreeMap<String, Presence> {
let mut track = subscribe(broadcast, name).await;
let frame = track.read_frame().await.expect("ok").expect("frame");
serde_json::from_slice(&frame.payload).expect("json parse")
}
async fn subscribe(broadcast: &moq_net::broadcast::Consumer, name: &str) -> track::Subscriber {
broadcast
.track(name)
.expect("track")
.subscribe(None)
.await
.expect("subscribe")
}
#[tokio::test(start_paused = true)]
async fn new_normalizes_and_drops_empty_node() {
let (_producer, origin) = test_producer(Some("/sjc//1/"));
assert_eq!(announced(&origin).await.0, ".stats/node/sjc/1");
let (_producer, origin) = test_producer(Some("///"));
assert_eq!(announced(&origin).await.0, ".stats/node");
}
#[tokio::test(start_paused = true)]
async fn single_broadcast_path_announced() {
let (producer, origin) = test_producer(Some("sjc/1"));
let _f1 = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 8).await;
let _f2 = feed(producer.registry(), Tier::default(), "baz/qux", true, 1, 8).await;
assert_eq!(announced(&origin).await.0, ".stats/node/sjc/1");
}
#[tokio::test(start_paused = true)]
async fn task_announces_without_node_suffix() {
let (producer, origin) = test_producer(None);
let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 8).await;
assert_eq!(announced(&origin).await.0, ".stats/node");
}
#[tokio::test(start_paused = true)]
async fn frame_emits_expected_counters() {
let (producer, origin) = test_producer(Some("sjc"));
let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 42).await;
drive_tick().await;
let (_, broadcast) = announced(&origin).await;
let frame = read_last_frame(&broadcast, "publisher.json").await;
let snap = frame.get("foo/bar").expect("foo/bar entry");
assert_eq!(snap.announced, 1, "egress announce stream bumps announced");
assert_eq!(snap.broadcasts, 1, "one session subscribed");
assert_eq!(snap.subscriptions, 1);
assert_eq!(snap.bytes, 42);
assert_eq!(snap.frames, 1);
}
#[tokio::test(start_paused = true)]
async fn announced_bytes_surfaces_in_frame() {
let (producer, origin) = test_producer(Some("sjc"));
let _f = feed(producer.registry(), Tier::default(), "foo/bar", false, 0, 0).await;
drive_tick().await;
let (_, broadcast) = announced(&origin).await;
let frame = read_last_frame(&broadcast, "publisher.json").await;
let snap = frame.get("foo/bar").expect("foo/bar entry");
assert_eq!(snap.announced, 1);
assert_eq!(
snap.announced_bytes,
"foo/bar".len() as u64,
"name length recorded on announce"
);
}
#[tokio::test(start_paused = true)]
async fn announced_decouples_from_broadcasts() {
let (producer, origin) = test_producer(Some("sjc"));
let _f = feed(producer.registry(), Tier::default(), "foo/bar", false, 0, 0).await;
drive_tick().await;
let (_, broadcast) = announced(&origin).await;
let frame = read_last_frame(&broadcast, "publisher.json").await;
let snap = frame.get("foo/bar").expect("foo/bar entry");
assert_eq!(snap.announced, 1);
assert_eq!(snap.broadcasts, 0, "no subscription, no broadcasts sentinel");
assert_eq!(snap.subscriptions, 0);
}
#[tokio::test(start_paused = true)]
async fn short_lived_sub_is_surfaced() {
let (producer, origin) = test_producer(Some("sjc"));
{
let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 123).await;
}
drive_tick().await;
let (_, broadcast) = announced(&origin).await;
let frame = read_last_frame(&broadcast, "publisher.json").await;
let snap = frame.get("foo/bar").expect("foo/bar entry");
assert_eq!(snap.subscriptions, 1);
assert_eq!(snap.subscriptions_closed, 1);
assert_eq!(snap.broadcasts, 1, "one session subscribed");
assert_eq!(snap.broadcasts_closed, 1);
assert_eq!(snap.bytes, 123);
assert_eq!(snap.frames, 1);
}
#[tokio::test(start_paused = true)]
async fn session_track_surfaces_by_root() {
let (producer, origin) = test_producer(Some("sjc"));
let _a = producer.registry().tier(Tier::default()).session("acme");
let _b = producer.registry().tier(Tier::default()).session("acme");
let _c = producer.registry().tier(Tier::new("region/sjc")).session("peer");
drive_tick().await;
let (_, broadcast) = announced(&origin).await;
let frame = read_session_frame(&broadcast, "sessions.json").await;
let snap = frame.get("acme").expect("root entry");
assert_eq!(snap.sessions, 2);
assert_eq!(snap.sessions_closed, 0);
assert!(
!frame.contains_key("peer"),
"regional session must not appear on the default track"
);
let snap = *read_session_frame(&broadcast, "region/sjc/sessions.json")
.await
.get("peer")
.expect("regional entry");
assert_eq!(snap.sessions, 1);
}
#[tokio::test(start_paused = true)]
async fn unused_slots_dont_surface() {
let (producer, origin) = test_producer(Some("sjc"));
let _f = feed(producer.registry(), Tier::default(), "foo/bar", true, 1, 8).await;
drive_tick().await;
drive_tick().await;
let (_, broadcast) = announced(&origin).await;
assert!(
read_last_frame(&broadcast, "publisher.json")
.await
.contains_key("foo/bar"),
"publisher.json must include the active foo/bar entry"
);
let frame = read_frame(&broadcast, "subscriber.json").await;
assert!(frame.is_empty(), "subscriber.json must be empty, got {frame:?}");
for name in ["publisher.json.z", "subscriber.json.z", "sessions.json.z"] {
assert!(broadcast.track(name).is_ok(), "{name} must exist");
}
for name in ["region/sjc/publisher.json", "region/sjc/publisher.json.z"] {
let track = broadcast.track(name).expect("logical track");
assert!(
track.subscribe(None).await.is_err(),
"{name} must not exist for a tier with no traffic",
);
}
}
#[test]
fn advertised_path_with_and_without_node() {
let prefix = Path::new(".stats");
let empty = Path::empty();
assert_eq!(
advertised_path(&prefix, &empty, Some("sjc")).as_str(),
".stats/node/sjc"
);
assert_eq!(
advertised_path(&prefix, &empty, Some("sjc/1")).as_str(),
".stats/node/sjc/1"
);
assert_eq!(advertised_path(&prefix, &empty, None).as_str(), ".stats/node");
assert_eq!(
advertised_path(&prefix, &Path::new("acme"), Some("sjc")).as_str(),
".stats/acme/node/sjc"
);
let prefix = Path::new("metrics");
assert_eq!(
advertised_path(&prefix, &Path::new("demo/room"), Some("lon")).as_str(),
"metrics/demo/room/node/lon"
);
}
#[test]
fn group_key_uses_leading_segments() {
assert_eq!(group_key("acme/room/cam", 0), Path::empty().to_owned());
assert_eq!(group_key("acme/room/cam", 1), Path::new("acme").to_owned());
assert_eq!(group_key("acme/room/cam", 2), Path::new("acme/room").to_owned());
assert_eq!(group_key("acme/room", 3), Path::new("acme/room").to_owned());
}
}