1use std::{
111 collections::HashMap,
112 fmt,
113 sync::{
114 Arc, Mutex,
115 atomic::{AtomicU64, Ordering},
116 },
117};
118
119use kio::Lock;
120use serde::{Deserialize, Deserializer, Serialize, Serializer};
121
122use crate::{AsPath, PathOwned, Pattern, Patterns};
123
124#[derive(Default, Debug)]
138pub(crate) struct Counters {
139 announces_started: AtomicU64,
140 announces_ended: AtomicU64,
141 announced_bytes: AtomicU64,
146 subscriptions_started: AtomicU64,
147 subscriptions_ended: AtomicU64,
148 fetches: AtomicU64,
152 broadcasts_started: AtomicU64,
153 broadcasts_ended: AtomicU64,
154 bytes: AtomicU64,
155 frames: AtomicU64,
156 groups: AtomicU64,
157 datagrams: AtomicU64,
159 stale: ContentCounters,
162}
163
164#[derive(Default, Debug)]
166struct ContentCounters {
167 bytes: AtomicU64,
168 frames: AtomicU64,
169 groups: AtomicU64,
170 datagrams: AtomicU64,
171}
172
173impl ContentCounters {
174 fn snapshot(&self) -> Content {
175 Content {
176 bytes: self.bytes.load(Ordering::Relaxed),
177 frames: self.frames.load(Ordering::Relaxed),
178 groups: self.groups.load(Ordering::Relaxed),
179 datagrams: self.datagrams.load(Ordering::Relaxed),
180 }
181 }
182
183 fn add(&self, content: Content) {
184 self.bytes.fetch_add(content.bytes, Ordering::Relaxed);
185 self.frames.fetch_add(content.frames, Ordering::Relaxed);
186 self.groups.fetch_add(content.groups, Ordering::Relaxed);
187 self.datagrams.fetch_add(content.datagrams, Ordering::Relaxed);
188 }
189}
190
191impl Counters {
192 fn snapshot(&self) -> Traffic {
200 let announces_ended = self.announces_ended.load(Ordering::Acquire);
201 let subscriptions_ended = self.subscriptions_ended.load(Ordering::Acquire);
202 let broadcasts_ended = self.broadcasts_ended.load(Ordering::Acquire);
203 let announces_started = self.announces_started.load(Ordering::Relaxed);
204 let announced_bytes = self.announced_bytes.load(Ordering::Relaxed);
205 let subscriptions_started = self.subscriptions_started.load(Ordering::Relaxed);
206 let fetches = self.fetches.load(Ordering::Relaxed);
207 let broadcasts_started = self.broadcasts_started.load(Ordering::Relaxed);
208 let bytes = self.bytes.load(Ordering::Relaxed);
209 let frames = self.frames.load(Ordering::Relaxed);
210 let groups = self.groups.load(Ordering::Relaxed);
211 let datagrams = self.datagrams.load(Ordering::Relaxed);
212 let stale = self.stale.snapshot();
213 Traffic {
214 announces_started,
215 announces_ended,
216 announced_bytes,
217 broadcasts_started,
218 broadcasts_ended,
219 subscriptions_started,
220 subscriptions_ended,
221 fetches,
222 bytes,
223 frames,
224 groups,
225 datagrams,
226 stale,
227 }
228 }
229}
230
231#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(default)]
238#[non_exhaustive]
239pub struct Content {
240 pub bytes: u64,
242 pub frames: u64,
244 pub groups: u64,
246 pub datagrams: u64,
248}
249
250impl Content {
251 pub(crate) fn add(&mut self, other: Self) {
253 self.bytes += other.bytes;
254 self.frames += other.frames;
255 self.groups += other.groups;
256 self.datagrams += other.datagrams;
257 }
258}
259
260#[derive(Default, Debug)]
264struct SessionCounters {
265 sessions_started: AtomicU64,
266 sessions_ended: AtomicU64,
267 control: [[AtomicU64; 2]; Control::ALL.len()],
268}
269
270impl SessionCounters {
271 fn snapshot(&self) -> Presence {
275 let sessions_ended = self.sessions_ended.load(Ordering::Acquire);
276 let sessions_started = self.sessions_started.load(Ordering::Relaxed);
277 Presence {
278 sessions_started,
279 sessions_ended,
280 }
281 }
282
283 fn control(&self, totals: &mut [[u64; 2]; Control::ALL.len()]) {
284 for (total, counters) in totals.iter_mut().zip(&self.control) {
285 for (total, counter) in total.iter_mut().zip(counters) {
286 *total += counter.load(Ordering::Relaxed);
287 }
288 }
289 }
290}
291
292#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
304#[non_exhaustive]
305pub struct Traffic {
306 pub announces_started: u64,
308 pub announces_ended: u64,
310 pub announced_bytes: u64,
313 pub broadcasts_started: u64,
316 pub broadcasts_ended: u64,
318 pub subscriptions_started: u64,
320 pub subscriptions_ended: u64,
322 pub fetches: u64,
327 pub bytes: u64,
329 pub frames: u64,
331 pub groups: u64,
333 pub datagrams: u64,
337 pub stale: Content,
342}
343
344#[derive(Default, Clone, Copy)]
349struct Edge(Option<u64>);
350
351impl<'de> Deserialize<'de> for Edge {
352 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
353 u64::deserialize(deserializer).map(|v| Self(Some(v)))
354 }
355}
356
357fn counter_edge(canonical: Edge, legacy: Edge) -> u64 {
360 canonical.0.or(legacy.0).unwrap_or(0)
361}
362
363#[derive(Serialize)]
364struct TrafficSer {
365 announces_started: u64,
366 announced: u64,
367 announces_ended: u64,
368 announced_closed: u64,
369 announced_bytes: u64,
370 broadcasts_started: u64,
371 broadcasts: u64,
372 broadcasts_ended: u64,
373 broadcasts_closed: u64,
374 subscriptions_started: u64,
375 subscriptions: u64,
376 subscriptions_ended: u64,
377 subscriptions_closed: u64,
378 fetches: u64,
379 bytes: u64,
380 frames: u64,
381 groups: u64,
382 datagrams: u64,
383 stale: Content,
384}
385
386impl From<Traffic> for TrafficSer {
387 fn from(t: Traffic) -> Self {
388 Self {
389 announces_started: t.announces_started,
390 announced: t.announces_started,
391 announces_ended: t.announces_ended,
392 announced_closed: t.announces_ended,
393 announced_bytes: t.announced_bytes,
394 broadcasts_started: t.broadcasts_started,
395 broadcasts: t.broadcasts_started,
396 broadcasts_ended: t.broadcasts_ended,
397 broadcasts_closed: t.broadcasts_ended,
398 subscriptions_started: t.subscriptions_started,
399 subscriptions: t.subscriptions_started,
400 subscriptions_ended: t.subscriptions_ended,
401 subscriptions_closed: t.subscriptions_ended,
402 fetches: t.fetches,
403 bytes: t.bytes,
404 frames: t.frames,
405 groups: t.groups,
406 datagrams: t.datagrams,
407 stale: t.stale,
408 }
409 }
410}
411
412#[derive(Default, Deserialize)]
413#[serde(default)]
414struct TrafficDe {
415 announces_started: Edge,
416 announced: Edge,
417 announces_ended: Edge,
418 announced_closed: Edge,
419 announced_bytes: u64,
420 broadcasts_started: Edge,
421 broadcasts: Edge,
422 broadcasts_ended: Edge,
423 broadcasts_closed: Edge,
424 subscriptions_started: Edge,
425 subscriptions: Edge,
426 subscriptions_ended: Edge,
427 subscriptions_closed: Edge,
428 fetches: u64,
429 bytes: u64,
430 frames: u64,
431 groups: u64,
432 datagrams: u64,
433 stale: Content,
434}
435
436impl From<TrafficDe> for Traffic {
437 fn from(d: TrafficDe) -> Self {
438 Self {
439 announces_started: counter_edge(d.announces_started, d.announced),
440 announces_ended: counter_edge(d.announces_ended, d.announced_closed),
441 announced_bytes: d.announced_bytes,
442 broadcasts_started: counter_edge(d.broadcasts_started, d.broadcasts),
443 broadcasts_ended: counter_edge(d.broadcasts_ended, d.broadcasts_closed),
444 subscriptions_started: counter_edge(d.subscriptions_started, d.subscriptions),
445 subscriptions_ended: counter_edge(d.subscriptions_ended, d.subscriptions_closed),
446 fetches: d.fetches,
447 bytes: d.bytes,
448 frames: d.frames,
449 groups: d.groups,
450 datagrams: d.datagrams,
451 stale: d.stale,
452 }
453 }
454}
455
456impl Serialize for Traffic {
457 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
458 TrafficSer::from(*self).serialize(serializer)
459 }
460}
461
462impl<'de> Deserialize<'de> for Traffic {
463 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
464 TrafficDe::deserialize(deserializer).map(Into::into)
465 }
466}
467
468impl Traffic {
469 pub fn add(&mut self, other: Traffic) {
471 self.announces_started += other.announces_started;
472 self.announces_ended += other.announces_ended;
473 self.announced_bytes += other.announced_bytes;
474 self.broadcasts_started += other.broadcasts_started;
475 self.broadcasts_ended += other.broadcasts_ended;
476 self.subscriptions_started += other.subscriptions_started;
477 self.subscriptions_ended += other.subscriptions_ended;
478 self.fetches += other.fetches;
479 self.bytes += other.bytes;
480 self.frames += other.frames;
481 self.groups += other.groups;
482 self.datagrams += other.datagrams;
483 self.stale.add(other.stale);
484 }
485
486 pub fn is_announced(&self) -> bool {
488 self.announces_started > self.announces_ended
489 }
490
491 pub fn active_broadcasts(&self) -> u64 {
493 self.broadcasts_started.saturating_sub(self.broadcasts_ended)
494 }
495
496 pub fn active_subscriptions(&self) -> u64 {
498 self.subscriptions_started.saturating_sub(self.subscriptions_ended)
499 }
500
501 pub fn total_bytes(&self) -> u64 {
505 self.bytes.saturating_add(self.announced_bytes)
506 }
507
508 pub fn is_idle(&self) -> bool {
511 self.announces_started == self.announces_ended
512 && self.subscriptions_started == self.subscriptions_ended
513 && self.broadcasts_started == self.broadcasts_ended
514 }
515}
516
517#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
526#[non_exhaustive]
527pub struct Presence {
528 pub sessions_started: u64,
530 pub sessions_ended: u64,
532}
533
534#[derive(Serialize)]
535struct PresenceSer {
536 sessions_started: u64,
537 sessions: u64,
538 sessions_ended: u64,
539 sessions_closed: u64,
540}
541
542impl From<Presence> for PresenceSer {
543 fn from(p: Presence) -> Self {
544 Self {
545 sessions_started: p.sessions_started,
546 sessions: p.sessions_started,
547 sessions_ended: p.sessions_ended,
548 sessions_closed: p.sessions_ended,
549 }
550 }
551}
552
553#[derive(Default, Deserialize)]
554#[serde(default)]
555struct PresenceDe {
556 sessions_started: Edge,
557 sessions: Edge,
558 sessions_ended: Edge,
559 sessions_closed: Edge,
560}
561
562impl From<PresenceDe> for Presence {
563 fn from(d: PresenceDe) -> Self {
564 Self {
565 sessions_started: counter_edge(d.sessions_started, d.sessions),
566 sessions_ended: counter_edge(d.sessions_ended, d.sessions_closed),
567 }
568 }
569}
570
571impl Serialize for Presence {
572 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
573 PresenceSer::from(*self).serialize(serializer)
574 }
575}
576
577impl<'de> Deserialize<'de> for Presence {
578 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
579 PresenceDe::deserialize(deserializer).map(Into::into)
580 }
581}
582
583impl Presence {
584 pub fn add(&mut self, other: Presence) {
586 self.sessions_started += other.sessions_started;
587 self.sessions_ended += other.sessions_ended;
588 }
589
590 pub fn active(&self) -> u64 {
592 self.sessions_started.saturating_sub(self.sessions_ended)
593 }
594}
595
596#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
607pub struct Tier(PathOwned);
608
609impl Tier {
610 pub fn new(label: impl Into<PathOwned>) -> Self {
612 Self(label.into())
613 }
614
615 pub fn label(&self) -> &PathOwned {
617 &self.0
618 }
619
620 pub fn is_default(&self) -> bool {
622 self.0.is_empty()
623 }
624
625 pub fn track_name(&self, name: &str) -> String {
628 if self.0.is_empty() {
629 name.to_string()
630 } else {
631 format!("{}/{}", self.0.as_str(), name)
632 }
633 }
634
635 pub fn as_str(&self) -> &str {
640 self.0.as_str()
641 }
642}
643
644impl fmt::Display for Tier {
645 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647 fmt::Display::fmt(&self.0, f)
648 }
649}
650
651#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
658pub enum Role {
659 Publisher,
661 Subscriber,
663}
664
665impl Role {
666 fn idx(self) -> usize {
667 match self {
668 Role::Publisher => 0,
669 Role::Subscriber => 1,
670 }
671 }
672
673 pub fn as_str(self) -> &'static str {
675 match self {
676 Role::Publisher => "publisher",
677 Role::Subscriber => "subscriber",
678 }
679 }
680}
681
682#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
684#[non_exhaustive]
685pub struct Bytes {
686 pub tx: u64,
688 pub rx: u64,
690}
691
692#[derive(Clone, Copy, Debug, PartialEq, Eq)]
698#[non_exhaustive]
699pub enum Control {
700 Session,
702 Announce,
704 TrackInfo,
706 Subscribe,
708 Fetch,
710 Probe,
712 Auth,
714 Goaway,
716 Other,
718}
719
720impl Control {
721 const ALL: [Self; 9] = [
722 Self::Session,
723 Self::Announce,
724 Self::TrackInfo,
725 Self::Subscribe,
726 Self::Fetch,
727 Self::Probe,
728 Self::Auth,
729 Self::Goaway,
730 Self::Other,
731 ];
732
733 pub fn as_str(self) -> &'static str {
735 match self {
736 Self::Session => "session",
737 Self::Announce => "announce",
738 Self::TrackInfo => "track_info",
739 Self::Subscribe => "subscribe",
740 Self::Fetch => "fetch",
741 Self::Probe => "probe",
742 Self::Auth => "auth",
743 Self::Goaway => "goaway",
744 Self::Other => "other",
745 }
746 }
747}
748
749#[derive(Debug, Default, Clone, PartialEq, Eq)]
760#[non_exhaustive]
761pub struct Snapshot {
762 traffic: HashMap<Tier, [Traffic; 2]>,
765 sessions: HashMap<Tier, Presence>,
767 control: HashMap<Tier, [[u64; 2]; Control::ALL.len()]>,
768}
769
770impl Snapshot {
771 pub fn control(&self) -> Vec<(Tier, Control, Bytes)> {
775 let mut rows = Vec::new();
776 for (tier, totals) in &self.control {
777 for class in Control::ALL {
778 let [tx, rx] = totals[class as usize];
779 if tx != 0 || rx != 0 {
780 rows.push((tier.clone(), class, Bytes { tx, rx }));
781 }
782 }
783 }
784 rows.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()).then(a.1.as_str().cmp(b.1.as_str())));
785 rows
786 }
787
788 pub fn traffic(&self) -> Vec<(Tier, Role, Traffic)> {
791 let mut rows = Vec::with_capacity(self.traffic.len() * 2);
792 for (tier, roles) in &self.traffic {
793 rows.push((tier.clone(), Role::Publisher, roles[Role::Publisher.idx()]));
794 rows.push((tier.clone(), Role::Subscriber, roles[Role::Subscriber.idx()]));
795 }
796 rows.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()).then(a.1.idx().cmp(&b.1.idx())));
797 rows
798 }
799
800 pub fn sessions(&self) -> Vec<(Tier, Presence)> {
803 let mut rows: Vec<_> = self.sessions.iter().map(|(tier, s)| (tier.clone(), *s)).collect();
804 rows.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
805 rows
806 }
807}
808
809#[derive(Debug, Default, Clone)]
813#[non_exhaustive]
814pub struct Report {
815 pub traffic: Vec<TrafficEntry>,
817 pub sessions: Vec<SessionEntry>,
819}
820
821#[derive(Debug, Clone)]
823#[non_exhaustive]
824pub struct TrafficEntry {
825 pub path: PathOwned,
827 pub tier: Tier,
829 pub publisher: Traffic,
831 pub subscriber: Traffic,
833}
834
835#[derive(Debug, Clone)]
837#[non_exhaustive]
838pub struct SessionEntry {
839 pub tier: Tier,
841 pub root: PathOwned,
843 pub presence: Presence,
845}
846
847#[derive(Clone, Debug, Default)]
853#[non_exhaustive]
854pub struct Config {
855 pub exclude: Patterns,
860}
861
862impl Config {
863 pub fn new() -> Self {
865 Self::default()
866 }
867
868 pub fn with_exclude(mut self, pattern: Pattern) -> Self {
871 self.exclude.insert(pattern);
872 self
873 }
874}
875
876#[derive(Clone)]
881pub struct Registry {
882 exclude: Patterns,
885 shared: Option<Arc<Shared>>,
887}
888
889struct Shared {
891 retired: Lock<Snapshot>,
894 entries: Lock<HashMap<PathOwned, Arc<BroadcastEntry>>>,
895 sessions: Lock<HashMap<Tier, HashMap<PathOwned, Arc<SessionCounters>>>>,
899}
900
901struct BroadcastEntry {
907 tiers: Mutex<HashMap<Tier, Arc<TierCounters>>>,
908}
909
910impl BroadcastEntry {
911 fn new() -> Self {
912 Self {
913 tiers: Mutex::new(HashMap::new()),
914 }
915 }
916
917 fn tier(&self, tier: &Tier) -> Arc<TierCounters> {
919 self.tiers
920 .lock()
921 .expect("stats tiers poisoned")
922 .entry(tier.clone())
923 .or_default()
924 .clone()
925 }
926}
927
928#[derive(Default)]
932struct TierCounters {
933 publisher: Counters,
934 subscriber: Counters,
935}
936
937impl Registry {
938 pub fn new(config: Config) -> Self {
940 let Config { exclude } = config;
941 Self {
942 exclude,
943 shared: Some(Arc::new(Shared {
944 retired: Lock::default(),
945 entries: Lock::default(),
946 sessions: Default::default(),
947 })),
948 }
949 }
950
951 pub fn disabled() -> Self {
953 Self {
954 exclude: Patterns::new(),
955 shared: None,
956 }
957 }
958
959 pub fn exclude(&self) -> &Patterns {
961 &self.exclude
962 }
963
964 #[cfg(test)]
967 fn shared(&self) -> &Arc<Shared> {
968 self.shared.as_ref().expect("enabled stats registry")
969 }
970
971 pub fn tier(&self, tier: Tier) -> Handle {
974 Handle {
975 stats: self.clone(),
976 tier,
977 }
978 }
979
980 fn entry(&self, path: impl AsPath) -> Option<Arc<BroadcastEntry>> {
981 let shared = self.shared.as_ref()?;
983 let path = path.as_path();
984 if self.exclude.matches(path.as_str()) {
988 return None;
989 }
990 let owned = path.to_owned();
991 let mut entries = shared.entries.lock();
992 Some(
993 entries
994 .entry(owned)
995 .or_insert_with(|| Arc::new(BroadcastEntry::new()))
996 .clone(),
997 )
998 }
999
1000 fn session_counters(&self, tier: &Tier, root: impl AsPath) -> Option<Arc<SessionCounters>> {
1004 let shared = self.shared.as_ref()?;
1005 let owned = root.as_path().to_owned();
1006 let mut sessions = shared.sessions.lock();
1007 Some(
1008 sessions
1009 .entry(tier.clone())
1010 .or_default()
1011 .entry(owned)
1012 .or_default()
1013 .clone(),
1014 )
1015 }
1016
1017 pub fn snapshot(&self) -> Snapshot {
1026 let Some(shared) = self.shared.as_ref() else {
1027 return Snapshot::default();
1028 };
1029 let retired = shared.retired.lock();
1030 let mut snap = retired.clone();
1031 {
1032 let entries = shared.entries.lock();
1033 for entry in entries.values() {
1034 let tiers = entry.tiers.lock().expect("stats tiers poisoned");
1035 for (tier, counters) in tiers.iter() {
1036 let totals = snap.traffic.entry(tier.clone()).or_default();
1037 totals[Role::Publisher.idx()].add(counters.publisher.snapshot());
1038 totals[Role::Subscriber.idx()].add(counters.subscriber.snapshot());
1039 }
1040 }
1041 }
1042 {
1043 let sessions = shared.sessions.lock();
1044 for (tier, roots) in sessions.iter() {
1045 let totals = snap.sessions.entry(tier.clone()).or_default();
1046 for counters in roots.values() {
1047 totals.add(counters.snapshot());
1048 counters.control(snap.control.entry(tier.clone()).or_default());
1049 }
1050 }
1051 }
1052 snap
1053 }
1054
1055 pub fn report(&self, report: &mut Report) {
1066 report.traffic.clear();
1067 report.sessions.clear();
1068 let Some(shared) = self.shared.as_ref() else {
1069 return;
1070 };
1071 let mut retired = shared.retired.lock();
1072 {
1073 let mut entries = shared.entries.lock();
1074 for (path, entry) in entries.iter() {
1075 let tiers = entry.tiers.lock().expect("stats tiers poisoned");
1076 for (tier, counters) in tiers.iter() {
1077 report.traffic.push(TrafficEntry {
1078 path: path.clone(),
1079 tier: tier.clone(),
1080 publisher: counters.publisher.snapshot(),
1081 subscriber: counters.subscriber.snapshot(),
1082 });
1083 }
1084 }
1085 entries.retain(|_, entry| {
1090 if Arc::strong_count(entry) > 1 {
1091 return true;
1092 }
1093 let mut tiers = entry.tiers.lock().expect("stats tiers poisoned");
1094 tiers.retain(|tier, counters| {
1095 if Arc::strong_count(counters) > 1 {
1096 return true;
1097 }
1098 let totals = retired.traffic.entry(tier.clone()).or_default();
1099 totals[Role::Publisher.idx()].add(counters.publisher.snapshot());
1100 totals[Role::Subscriber.idx()].add(counters.subscriber.snapshot());
1101 false
1102 });
1103 !tiers.is_empty()
1104 });
1105 }
1106 {
1107 let mut sessions = shared.sessions.lock();
1108 for (tier, roots) in sessions.iter() {
1109 for (root, counters) in roots.iter() {
1110 report.sessions.push(SessionEntry {
1111 tier: tier.clone(),
1112 root: root.clone(),
1113 presence: counters.snapshot(),
1114 });
1115 }
1116 }
1117 for (tier, roots) in sessions.iter_mut() {
1118 roots.retain(|_, counters| {
1119 if Arc::strong_count(counters) > 1 {
1120 return true;
1121 }
1122 retired
1123 .sessions
1124 .entry(tier.clone())
1125 .or_default()
1126 .add(counters.snapshot());
1127 counters.control(retired.control.entry(tier.clone()).or_default());
1128 false
1129 });
1130 }
1131 sessions.retain(|_, roots| !roots.is_empty());
1132 }
1133 }
1134}
1135
1136impl Default for Registry {
1137 fn default() -> Self {
1139 Self::disabled()
1140 }
1141}
1142
1143#[derive(Clone)]
1146pub struct Handle {
1147 stats: Registry,
1148 tier: Tier,
1149}
1150
1151impl Handle {
1152 pub fn parent(&self) -> &Registry {
1154 &self.stats
1155 }
1156
1157 pub fn tier(&self) -> &Tier {
1159 &self.tier
1160 }
1161
1162 pub fn session(&self, root: impl AsPath) -> Session {
1168 Session::new(self.stats.clone(), self.tier.clone(), root)
1169 }
1170}
1171
1172impl Default for Handle {
1173 fn default() -> Self {
1175 Registry::disabled().tier(Tier::default())
1176 }
1177}
1178
1179#[derive(Copy, Clone, Default)]
1183enum Side {
1184 #[default]
1185 Publisher,
1186 Subscriber,
1187}
1188
1189impl Side {
1190 fn counters(self, tier: &TierCounters) -> &Counters {
1191 match self {
1192 Side::Publisher => &tier.publisher,
1193 Side::Subscriber => &tier.subscriber,
1194 }
1195 }
1196}
1197
1198#[derive(Clone, Default)]
1220pub struct Session {
1221 inner: Option<Arc<SessionInner>>,
1223}
1224
1225struct SessionInner {
1228 registry: Registry,
1229 root: PathOwned,
1231 current: Mutex<Current>,
1233 generation: AtomicU64,
1235 viewers: Mutex<HashMap<PathOwned, Viewer>>,
1239}
1240
1241struct Current {
1243 tier: Tier,
1244 presence: Option<Arc<SessionCounters>>,
1246}
1247
1248struct Viewer {
1250 subscriptions: u32,
1251 counters: Arc<TierCounters>,
1253}
1254
1255impl Session {
1256 pub(crate) fn control(&self, class: Control, bytes: usize, received: bool) {
1257 let Some(inner) = &self.inner else { return };
1258 let current = inner.current.lock().expect("stats session poisoned");
1259 if let Some(counters) = ¤t.presence {
1260 counters.control[class as usize][usize::from(received)].fetch_add(bytes as u64, Ordering::Relaxed);
1261 }
1262 }
1263
1264 fn new(registry: Registry, tier: Tier, root: impl AsPath) -> Self {
1265 let root = root.as_path().to_owned();
1266 let presence = registry.session_counters(&tier, &root);
1267 if let Some(presence) = &presence {
1268 presence.sessions_started.fetch_add(1, Ordering::Relaxed);
1269 }
1270 Self {
1271 inner: Some(Arc::new(SessionInner {
1272 registry,
1273 root,
1274 current: Mutex::new(Current { tier, presence }),
1275 generation: AtomicU64::new(0),
1276 viewers: Mutex::new(HashMap::new()),
1277 })),
1278 }
1279 }
1280
1281 pub fn set_tier(&self, tier: Tier) {
1283 let Some(inner) = &self.inner else { return };
1284 let mut current = inner.current.lock().expect("stats session poisoned");
1285 if current.tier == tier {
1286 return;
1287 }
1288 let presence = inner.registry.session_counters(&tier, &inner.root);
1289 if let Some(presence) = &presence {
1290 presence.sessions_started.fetch_add(1, Ordering::Relaxed);
1291 }
1292 if let Some(old) = std::mem::replace(&mut current.presence, presence) {
1293 old.sessions_ended.fetch_add(1, Ordering::Release);
1295 }
1296 current.tier = tier;
1297 inner.generation.fetch_add(1, Ordering::Release);
1298 }
1299
1300 pub(crate) fn egress(&self, path: impl AsPath) -> Scope {
1303 self.scope(path, Side::Publisher)
1304 }
1305
1306 pub(crate) fn ingress(&self, path: impl AsPath) -> Scope {
1308 self.scope(path, Side::Subscriber)
1309 }
1310
1311 fn scope(&self, path: impl AsPath, side: Side) -> Scope {
1312 let Some(inner) = &self.inner else {
1313 return Scope::default();
1314 };
1315 let path = path.as_path().to_owned();
1316 let resolved = inner.resolve(&path);
1317 Scope {
1318 session: self.clone(),
1319 resolved: Some(Box::new(Mutex::new(resolved))),
1320 side,
1321 path,
1322 }
1323 }
1324
1325 fn viewer_open(&self, path: &PathOwned, counters: &Arc<TierCounters>) {
1328 let Some(inner) = &self.inner else { return };
1329 let mut viewers = inner.viewers.lock().expect("stats viewers poisoned");
1330 let viewer = viewers.entry(path.clone()).or_insert_with(|| {
1331 counters.publisher.broadcasts_started.fetch_add(1, Ordering::Relaxed);
1332 Viewer {
1333 subscriptions: 0,
1334 counters: counters.clone(),
1335 }
1336 });
1337 viewer.subscriptions += 1;
1338 }
1339
1340 fn viewer_close(&self, path: &PathOwned) {
1343 let Some(inner) = &self.inner else { return };
1344 let mut viewers = inner.viewers.lock().expect("stats viewers poisoned");
1345 let Some(viewer) = viewers.get_mut(path) else { return };
1346 viewer.subscriptions -= 1;
1347 if viewer.subscriptions == 0
1348 && let Some(viewer) = viewers.remove(path)
1349 {
1350 viewer
1352 .counters
1353 .publisher
1354 .broadcasts_ended
1355 .fetch_add(1, Ordering::Release);
1356 }
1357 }
1358}
1359
1360impl SessionInner {
1361 fn resolve(&self, path: &PathOwned) -> Resolved {
1364 let (generation, tier) = {
1365 let current = self.current.lock().expect("stats session poisoned");
1366 (self.generation.load(Ordering::Relaxed), current.tier.clone())
1367 };
1368 Resolved {
1369 generation,
1370 counters: self.registry.entry(path).map(|entry| entry.tier(&tier)),
1371 }
1372 }
1373}
1374
1375impl Drop for SessionInner {
1376 fn drop(&mut self) {
1377 let current = self.current.get_mut().expect("stats session poisoned");
1378 if let Some(presence) = ¤t.presence {
1379 presence.sessions_ended.fetch_add(1, Ordering::Release);
1382 }
1383 }
1384}
1385
1386#[derive(Clone, Default)]
1401pub(crate) struct Meter {
1402 counters: Option<Arc<TierCounters>>,
1403 side: Side,
1404}
1405
1406impl Meter {
1407 fn counters(&self) -> Option<&Counters> {
1408 self.counters.as_ref().map(|c| self.side.counters(c))
1409 }
1410
1411 pub(crate) fn group(&self) {
1413 if let Some(counters) = self.counters() {
1414 counters.groups.fetch_add(1, Ordering::Relaxed);
1415 }
1416 }
1417
1418 pub(crate) fn frames(&self, n: u64) {
1420 if n == 0 {
1421 return;
1422 }
1423 if let Some(counters) = self.counters() {
1424 counters.frames.fetch_add(n, Ordering::Relaxed);
1425 }
1426 }
1427
1428 pub(crate) fn datagram(&self, n: u64) {
1432 if let Some(counters) = self.counters() {
1433 counters.datagrams.fetch_add(1, Ordering::Relaxed);
1434 counters.groups.fetch_add(1, Ordering::Relaxed);
1435 counters.frames.fetch_add(1, Ordering::Relaxed);
1436 counters.bytes.fetch_add(n, Ordering::Relaxed);
1437 }
1438 }
1439
1440 pub(crate) fn is_tracked(&self) -> bool {
1444 self.counters.is_some()
1445 }
1446
1447 pub(crate) fn stale(&self, content: Content) {
1449 if let Some(counters) = self.counters() {
1450 counters.stale.add(content);
1451 }
1452 }
1453
1454 pub(crate) fn bytes(&self, n: u64) {
1456 if n == 0 {
1457 return;
1458 }
1459 if let Some(counters) = self.counters() {
1460 counters.bytes.fetch_add(n, Ordering::Relaxed);
1461 }
1462 }
1463}
1464
1465#[derive(Default)]
1471pub(crate) struct Scope {
1472 session: Session,
1474 resolved: Option<Box<Mutex<Resolved>>>,
1478 side: Side,
1479 path: PathOwned,
1482}
1483
1484#[derive(Clone)]
1486struct Resolved {
1487 generation: u64,
1488 counters: Option<Arc<TierCounters>>,
1490}
1491
1492impl Clone for Scope {
1493 fn clone(&self) -> Self {
1494 Self {
1495 session: self.session.clone(),
1496 resolved: self
1497 .resolved
1498 .as_ref()
1499 .map(|r| Box::new(Mutex::new(r.lock().expect("stats scope poisoned").clone()))),
1500 side: self.side,
1501 path: self.path.clone(),
1502 }
1503 }
1504}
1505
1506impl Scope {
1507 fn counters(&self) -> Option<Arc<TierCounters>> {
1510 let inner = self.session.inner.as_ref()?;
1511 let generation = inner.generation.load(Ordering::Acquire);
1512 let mut resolved = self.resolved.as_ref()?.lock().expect("stats scope poisoned");
1513 if resolved.generation != generation {
1514 *resolved = inner.resolve(&self.path);
1515 }
1516 resolved.counters.clone()
1517 }
1518
1519 pub(crate) fn meter(&self) -> Meter {
1521 Meter {
1522 counters: self.counters(),
1523 side: self.side,
1524 }
1525 }
1526
1527 pub(crate) fn subscribe(&self) -> Subscription {
1531 let counters = self.counters();
1532 let mut viewer = None;
1533 if let Some(counters) = &counters {
1534 self.side
1535 .counters(counters)
1536 .subscriptions_started
1537 .fetch_add(1, Ordering::Relaxed);
1538 if matches!(self.side, Side::Publisher) {
1541 self.session.viewer_open(&self.path, counters);
1542 viewer = Some((self.session.clone(), self.path.clone()));
1543 }
1544 }
1545 Subscription {
1546 counters,
1547 side: self.side,
1548 viewer,
1549 }
1550 }
1551
1552 pub(crate) fn fetch(&self) {
1554 if let Some(counters) = self.counters() {
1555 self.side.counters(&counters).fetches.fetch_add(1, Ordering::Relaxed);
1556 }
1557 }
1558
1559 pub(crate) fn announce(&self) -> Announce {
1564 let len = self.path.as_str().len() as u64;
1565 let counters = self.counters();
1566 if let Some(counters) = &counters {
1567 let counters = self.side.counters(counters);
1568 counters.announces_started.fetch_add(1, Ordering::Relaxed);
1569 counters.announced_bytes.fetch_add(len, Ordering::Relaxed);
1570 }
1571 Announce {
1572 counters,
1573 side: self.side,
1574 len,
1575 }
1576 }
1577}
1578
1579#[derive(Default)]
1582#[must_use = "drop the guard to record the subscription as closed"]
1583pub(crate) struct Subscription {
1584 counters: Option<Arc<TierCounters>>,
1585 side: Side,
1586 viewer: Option<(Session, PathOwned)>,
1588}
1589
1590impl Drop for Subscription {
1591 fn drop(&mut self) {
1592 if let Some((session, path)) = &self.viewer {
1593 session.viewer_close(path);
1594 }
1595 if let Some(counters) = &self.counters {
1596 self.side
1598 .counters(counters)
1599 .subscriptions_ended
1600 .fetch_add(1, Ordering::Release);
1601 }
1602 }
1603}
1604
1605#[must_use = "drop the guard to record the unannounce"]
1607pub(crate) struct Announce {
1608 counters: Option<Arc<TierCounters>>,
1609 side: Side,
1610 len: u64,
1611}
1612
1613impl Drop for Announce {
1614 fn drop(&mut self) {
1615 if let Some(counters) = &self.counters {
1616 let counters = self.side.counters(counters);
1617 counters.announced_bytes.fetch_add(self.len, Ordering::Relaxed);
1618 counters.announces_ended.fetch_add(1, Ordering::Release);
1620 }
1621 }
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626 use std::sync::{Arc, atomic::Ordering::Relaxed};
1627
1628 use super::*;
1629
1630 #[test]
1631 fn default_tier_has_empty_label() {
1632 let tier = Tier::default();
1633 assert_eq!(tier.as_str(), "");
1634 assert_eq!(tier.to_string(), "");
1635 assert_eq!(tier.track_name("publisher.json"), "publisher.json");
1636 }
1637
1638 fn tier_counters(stats: &Registry, path: &str, tier: &Tier) -> Arc<TierCounters> {
1640 stats
1641 .shared()
1642 .entries
1643 .lock()
1644 .get(&PathOwned::from(path.to_string()))
1645 .expect("entry")
1646 .tier(tier)
1647 }
1648
1649 fn session_snapshot(stats: &Registry, tier: &Tier, root: &str) -> Option<Presence> {
1651 stats
1652 .shared()
1653 .sessions
1654 .lock()
1655 .get(tier)
1656 .and_then(|roots| roots.get(&PathOwned::from(root.to_string())).map(|c| c.snapshot()))
1657 }
1658
1659 fn test_stats() -> Registry {
1660 Registry::new(Config::new().with_exclude(Pattern::subtree(".stats").unwrap()))
1661 }
1662
1663 #[test]
1664 fn default_and_named_tiers_are_independent() {
1665 let stats = test_stats();
1666 let default = stats.tier(Tier::default()).session("root");
1667 let regional = stats.tier(Tier::new("region/sjc")).session("root");
1668
1669 default.egress("demo/bbb").meter().bytes(100);
1670 regional.ingress("demo/bbb").meter().bytes(7);
1671
1672 let default_counters = tier_counters(&stats, "demo/bbb", &Tier::default());
1673 let regional_counters = tier_counters(&stats, "demo/bbb", &Tier::new("region/sjc"));
1674 assert_eq!(default_counters.publisher.bytes.load(Relaxed), 100);
1675 assert_eq!(default_counters.subscriber.bytes.load(Relaxed), 0);
1676 assert_eq!(regional_counters.publisher.bytes.load(Relaxed), 0);
1677 assert_eq!(regional_counters.subscriber.bytes.load(Relaxed), 7);
1678 }
1679
1680 #[test]
1681 fn snapshot_rolls_up_by_tier_role_and_sessions() {
1682 let stats = test_stats();
1683 let default = stats.tier(Tier::default());
1684 let regional = stats.tier(Tier::new("region/sjc"));
1685
1686 let s1 = default.session("acme");
1688 let _s2 = default.session("acme");
1689 let s3 = regional.session("peer");
1690
1691 {
1693 let m = s1.egress("demo/aaa").meter();
1694 m.bytes(100);
1695 m.frames(1);
1696 m.group();
1697 }
1698 s1.egress("demo/bbb").meter().bytes(50);
1699 s3.ingress("demo/aaa").meter().bytes(7);
1701
1702 let snap = stats.snapshot();
1703
1704 let slot = |tier, role| {
1705 snap.traffic()
1706 .into_iter()
1707 .find(|(t, r, _)| *t == tier && *r == role)
1708 .map(|(_, _, c)| c)
1709 .expect("row present")
1710 };
1711
1712 let default_publisher = slot(Tier::default(), Role::Publisher);
1713 assert_eq!(
1714 default_publisher.bytes, 150,
1715 "default egress bytes sum across broadcasts"
1716 );
1717 assert_eq!(default_publisher.frames, 1);
1718 assert_eq!(default_publisher.groups, 1);
1719
1720 let regional_subscriber = slot(Tier::new("region/sjc"), Role::Subscriber);
1721 assert_eq!(regional_subscriber.bytes, 7, "regional ingress isolated by tier/role");
1722 assert_eq!(slot(Tier::default(), Role::Subscriber).bytes, 0);
1723 assert_eq!(slot(Tier::new("region/sjc"), Role::Publisher).bytes, 0);
1724
1725 let sessions = |tier| {
1726 snap.sessions()
1727 .into_iter()
1728 .find(|(t, _)| *t == tier)
1729 .map(|(_, s)| s)
1730 .expect("tier present")
1731 };
1732 let default_sessions = sessions(Tier::default());
1733 assert_eq!(
1734 default_sessions.sessions_started, 2,
1735 "two default-tier sessions under one root"
1736 );
1737 assert_eq!(default_sessions.sessions_ended, 0, "guards still held");
1738 assert_eq!(sessions(Tier::new("region/sjc")).sessions_started, 1);
1739 }
1740
1741 fn drain(stats: &Registry) -> Report {
1742 let mut report = Report::default();
1743 stats.report(&mut report);
1744 report
1745 }
1746
1747 #[test]
1748 fn report_reuses_capacity() {
1749 let stats = test_stats();
1751 let ctx = stats.tier(Tier::default()).session("root");
1752 let _scopes: Vec<_> = (0..8).map(|i| ctx.egress(format!("b/{i}").as_str())).collect();
1753
1754 let mut report = Report::default();
1755 stats.report(&mut report);
1756 assert_eq!(report.traffic.len(), 8);
1757 assert_eq!(report.sessions.len(), 1);
1758 let (traffic, sessions) = (report.traffic.as_ptr(), report.sessions.as_ptr());
1759
1760 stats.report(&mut report);
1761 assert_eq!(report.traffic.len(), 8, "refilled, not appended");
1762 assert_eq!(report.sessions.len(), 1);
1763 assert_eq!(report.traffic.as_ptr(), traffic, "traffic buffer reused");
1764 assert_eq!(report.sessions.as_ptr(), sessions, "sessions buffer reused");
1765 }
1766
1767 #[test]
1768 fn report_returns_detail_and_prunes() {
1769 let stats = test_stats();
1773 let key = PathOwned::from("foo/bar");
1774 let ctx = stats.tier(Tier::default()).session("root");
1775 let scope = ctx.egress("foo/bar");
1776 let sub = scope.subscribe();
1777 scope.meter().bytes(42);
1778
1779 let report = drain(&stats);
1780 let row = report
1781 .traffic
1782 .iter()
1783 .find(|row| row.path == key)
1784 .expect("live entry present");
1785 assert_eq!(row.publisher.bytes, 42);
1786 assert_eq!(row.publisher.subscriptions_started, 1);
1787 assert!(!row.publisher.is_idle(), "subscription guard still open");
1788 assert!(
1789 stats.shared().entries.lock().contains_key(&key),
1790 "live entry kept across drains"
1791 );
1792
1793 drop(sub);
1794 drop(scope);
1795
1796 let report = drain(&stats);
1799 let row = report
1800 .traffic
1801 .iter()
1802 .find(|row| row.path == key)
1803 .expect("closing values still reported once");
1804 assert_eq!(row.publisher.subscriptions_ended, 1);
1805 assert!(row.publisher.is_idle());
1806 assert!(
1807 !stats.shared().entries.lock().contains_key(&key),
1808 "fully-closed entry pruned"
1809 );
1810 assert!(drain(&stats).traffic.is_empty(), "nothing left after the prune");
1811 }
1812
1813 #[test]
1814 fn report_keeps_idle_but_announced_entry() {
1815 let stats = test_stats();
1819 let key = PathOwned::from("foo/bar");
1820 let ctx = stats.tier(Tier::default()).session("root");
1821 let scope = ctx.egress("foo/bar");
1822 let guard = scope.announce();
1823
1824 for _ in 0..3 {
1825 let report = drain(&stats);
1826 assert!(
1827 report.traffic.iter().any(|row| row.path == key),
1828 "announced-but-idle broadcast stays while the guard is held"
1829 );
1830 }
1831
1832 drop(guard);
1833 drop(scope);
1834 let report = drain(&stats);
1835 let row = report.traffic.iter().find(|row| row.path == key).expect("final report");
1836 assert!(row.publisher.is_idle());
1837 assert!(!stats.shared().entries.lock().contains_key(&key));
1838 }
1839
1840 #[test]
1841 fn report_prunes_empty_session_roots() {
1842 let stats = test_stats();
1845 let session = stats.tier(Tier::default()).session("acme");
1846
1847 let report = drain(&stats);
1848 let row = report
1849 .sessions
1850 .iter()
1851 .find(|row| row.root.as_str() == "acme")
1852 .expect("root present");
1853 assert_eq!(row.presence.active(), 1);
1854
1855 drop(session);
1856 let report = drain(&stats);
1857 let row = report
1858 .sessions
1859 .iter()
1860 .find(|row| row.root.as_str() == "acme")
1861 .expect("final gauge reported once");
1862 assert_eq!(row.presence.active(), 0);
1863 assert!(drain(&stats).sessions.is_empty(), "root pruned after the last drain");
1864 assert!(session_snapshot(&stats, &Tier::default(), "acme").is_none());
1865 }
1866
1867 #[test]
1868 fn control_body_totals_follow_retags_and_survive_retirement() {
1869 let registry = Registry::new(Default::default());
1870 let first = Tier::new("internal");
1871 let second = Tier::new("edge");
1872 let context = registry.tier(first.clone()).session("");
1873 context.control(Control::Announce, 17, false);
1874 context.set_tier(second.clone());
1875 context.control(Control::TrackInfo, 23, true);
1876 let expected = vec![
1877 (second, Control::TrackInfo, Bytes { tx: 0, rx: 23 }),
1878 (first, Control::Announce, Bytes { tx: 17, rx: 0 }),
1879 ];
1880 assert_eq!(registry.snapshot().control(), expected);
1881 drop(context);
1882 registry.report(&mut Report::default());
1883 assert_eq!(registry.snapshot().control(), expected);
1884 assert_eq!(registry.snapshot().control(), expected, "scrapes do not drain totals");
1885 }
1886
1887 #[test]
1888 fn snapshot_preserves_retired_counters() {
1889 let stats = test_stats();
1890 let tier = Tier::default();
1891 let live = stats.tier(tier.clone()).session("live");
1892 let scope = live.egress("live/video");
1893 let _live_sub = scope.subscribe();
1894 for _ in 0..2 {
1895 let session = stats.tier(tier.clone()).session("retired");
1896 let scope = session.egress("retired/video");
1897 let sub = scope.subscribe();
1898 scope.meter().bytes(100);
1899 drop(sub);
1900 drop(scope);
1901 drop(session);
1902 let before = stats.snapshot();
1903 drain(&stats);
1904 assert_eq!(stats.snapshot(), before, "pruning must not reset host counters");
1905 }
1906 let snap = stats.snapshot();
1907 let traffic = snap
1908 .traffic()
1909 .into_iter()
1910 .find(|(_, role, _)| *role == Role::Publisher)
1911 .unwrap()
1912 .2;
1913 assert_eq!(traffic.bytes, 200);
1914 assert_eq!(traffic.subscriptions_started, 3);
1915 assert_eq!(traffic.subscriptions_ended, 2);
1916 let sessions = snap.sessions().into_iter().find(|(label, _)| label == &tier).unwrap().1;
1917 assert_eq!(sessions.sessions_started, 3);
1918 assert_eq!(sessions.sessions_ended, 2);
1919 assert_eq!(stats.shared().entries.lock().len(), 1, "retired paths are still pruned");
1920 assert_eq!(
1921 stats.shared().sessions.lock()[&tier].len(),
1922 1,
1923 "retired roots are still pruned"
1924 );
1925 let retired = stats.shared().retired.lock();
1926 assert_eq!(retired.traffic.len(), 1, "retain only a total per tier");
1927 assert_eq!(retired.sessions.len(), 1);
1928 }
1929
1930 #[cfg(not(target_family = "wasm"))]
1931 #[test]
1932 fn snapshot_and_report_transfer_counters_once() {
1933 let stats = test_stats();
1934 let worker_stats = stats.clone();
1935 let worker = std::thread::spawn(move || {
1936 for i in 0..256 {
1937 let path = format!("root/{i}");
1938 let session = worker_stats.tier(Tier::default()).session(path.as_str());
1939 session.ingress(path.as_str()).meter().bytes(1);
1940 drop(session);
1941 drain(&worker_stats);
1942 }
1943 });
1944 let mut previous = 0;
1945 while !worker.is_finished() {
1946 let bytes: u64 = stats
1947 .snapshot()
1948 .traffic()
1949 .iter()
1950 .map(|(_, _, traffic)| traffic.bytes)
1951 .sum();
1952 assert!(
1953 bytes >= previous,
1954 "retiring an entry must not double-count or lose its bytes"
1955 );
1956 assert!(bytes <= 256);
1957 previous = bytes;
1958 }
1959 worker.join().unwrap();
1960 let snap = stats.snapshot();
1961 assert_eq!(
1962 snap.traffic().iter().map(|(_, _, traffic)| traffic.bytes).sum::<u64>(),
1963 256
1964 );
1965 assert_eq!(snap.sessions()[0].1.sessions_started, 256);
1966 assert_eq!(snap.sessions()[0].1.sessions_ended, 256);
1967 assert!(stats.shared().entries.lock().is_empty());
1968 assert!(stats.shared().sessions.lock().is_empty());
1969 let retired = stats.shared().retired.lock();
1970 assert_eq!(retired.traffic.len(), 1);
1971 assert_eq!(retired.sessions.len(), 1);
1972 }
1973
1974 #[test]
1975 fn paths_under_exclude_are_no_op() {
1976 let stats = test_stats();
1979 let ctx = stats.tier(Tier::default()).session("root");
1980 let scope = ctx.egress(".stats/node/sjc");
1981 scope.meter().bytes(100);
1982 let _guard = scope.announce();
1983 let _sub = scope.subscribe();
1984 assert!(stats.shared().entries.lock().is_empty());
1985 }
1986
1987 #[test]
1988 fn disabled_stats_are_noop() {
1989 let stats = Registry::default();
1992 assert!(stats.shared.is_none());
1993 let ctx = stats.tier(Tier::default()).session("root");
1994 let scope = ctx.egress("demo/bbb");
1995 scope.meter().bytes(100);
1996 let _guard = scope.announce();
1997 let _sub = scope.subscribe();
1998 assert!(drain(&stats).traffic.is_empty());
1999 assert!(stats.snapshot().traffic().is_empty());
2000 }
2001
2002 #[test]
2003 fn session_counts_by_root() {
2004 let stats = test_stats();
2007 let ext = stats.tier(Tier::default());
2008
2009 let snap = |root: &str| {
2010 session_snapshot(&stats, &Tier::default(), root).map(|p| (p.sessions_started, p.sessions_ended))
2011 };
2012
2013 let a1 = ext.session("acme");
2014 let a2 = ext.session("acme");
2015 let b1 = ext.session("globex");
2016 assert_eq!(snap("acme"), Some((2, 0)), "two sessions under one root");
2017 assert_eq!(snap("globex"), Some((1, 0)), "a distinct root is counted separately");
2018
2019 drop(a1);
2020 assert_eq!(snap("acme"), Some((2, 1)));
2021 drop(a2);
2022 drop(b1);
2023 assert_eq!(snap("acme"), Some((2, 2)));
2024 assert_eq!(snap("globex"), Some((1, 1)));
2025 }
2026
2027 #[test]
2028 fn traffic_parses_with_missing_and_unknown_fields() {
2029 let old: Traffic = serde_json::from_str(r#"{"announced":1,"bytes":5}"#).expect("older shape parses");
2032 assert_eq!(old.announces_started, 1);
2033 assert_eq!(old.bytes, 5);
2034 assert_eq!(old.announced_bytes, 0, "missing fields default to zero");
2035
2036 let new: Traffic = serde_json::from_str(r#"{"announces_started":1,"announces_ended":1,"future_counter":9}"#)
2037 .expect("newer shape parses");
2038 assert!(new.is_idle());
2039 }
2040
2041 #[test]
2042 fn snapshot_reads_ended_before_started() {
2043 let src = include_str!("stats.rs");
2048 let body_start = src.find("fn snapshot(&self) -> Traffic").expect("snapshot fn present");
2051 let body = &src[body_start..];
2052 let ended_pos = body.find("self.announces_ended.load").expect("announces_ended load");
2053 let started_pos = body
2054 .find("self.announces_started.load")
2055 .expect("announces_started load");
2056 assert!(
2057 ended_pos < started_pos,
2058 "announces_ended must be loaded before announces_started; reversing breaks the started>=ended invariant",
2059 );
2060 let subs_ended_pos = body
2061 .find("self.subscriptions_ended.load")
2062 .expect("subscriptions_ended load");
2063 let subs_pos = body
2064 .find("self.subscriptions_started.load")
2065 .expect("subscriptions_started load");
2066 assert!(
2067 subs_ended_pos < subs_pos,
2068 "subscriptions_ended must be loaded before subscriptions_started",
2069 );
2070 let bcast_ended_pos = body.find("self.broadcasts_ended.load").expect("broadcasts_ended load");
2071 let bcast_pos = body
2072 .find("self.broadcasts_started.load")
2073 .expect("broadcasts_started load");
2074 assert!(
2075 bcast_ended_pos < bcast_pos,
2076 "broadcasts_ended must be loaded before broadcasts_started",
2077 );
2078 }
2079
2080 #[test]
2081 fn context_presence_closes_on_last_clone() {
2082 let stats = test_stats();
2085 let snap = |root: &str| {
2086 session_snapshot(&stats, &Tier::default(), root).map(|p| (p.sessions_started, p.sessions_ended))
2087 };
2088
2089 let ctx = stats.tier(Tier::default()).session("acme");
2090 assert_eq!(snap("acme"), Some((1, 0)));
2091
2092 let clone = ctx.clone();
2093 assert_eq!(snap("acme"), Some((1, 0)));
2095 drop(ctx);
2096 assert_eq!(snap("acme"), Some((1, 0)));
2097 drop(clone);
2098 assert_eq!(snap("acme"), Some((1, 1)));
2099 }
2100
2101 #[test]
2102 fn set_tier_moves_presence() {
2103 let stats = test_stats();
2104 let gold = Tier::new("gold");
2105 let snap = |tier: &Tier| session_snapshot(&stats, tier, "acme").map(|p| (p.sessions_started, p.sessions_ended));
2106
2107 let ctx = stats.tier(Tier::default()).session("acme");
2108 ctx.set_tier(Tier::default());
2109 assert_eq!(snap(&Tier::default()), Some((1, 0)), "the same tier is a no-op");
2110
2111 ctx.set_tier(gold.clone());
2112 assert_eq!(snap(&Tier::default()), Some((1, 1)));
2113 assert_eq!(snap(&gold), Some((1, 0)));
2114
2115 drop(ctx);
2116 assert_eq!(snap(&gold), Some((1, 1)), "the session closes on its current tier");
2117 }
2118
2119 #[test]
2120 fn set_tier_moves_subsequent_traffic() {
2121 let stats = test_stats();
2122 let gold = Tier::new("gold");
2123 let ctx = stats.tier(Tier::default()).session("acme");
2124 let scope = ctx.egress("demo/bbb");
2125 let clone = scope.clone();
2126
2127 let before = scope.meter();
2128 let sub = scope.subscribe();
2129 let announce = scope.announce();
2130 before.bytes(10);
2131
2132 ctx.set_tier(gold.clone());
2133 before.bytes(1);
2135 scope.meter().bytes(5);
2136 clone.meter().bytes(7);
2137 let sub2 = scope.subscribe();
2138 scope.fetch();
2139
2140 drop(sub);
2141 drop(sub2);
2142 drop(announce);
2143
2144 let old = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2145 let new = tier_counters(&stats, "demo/bbb", &gold).publisher.snapshot();
2146 assert_eq!(old.bytes, 11);
2147 assert_eq!(new.bytes, 12);
2148 assert_eq!((old.fetches, new.fetches), (0, 1));
2149
2150 assert_eq!((old.subscriptions_started, old.subscriptions_ended), (1, 1));
2152 assert_eq!((new.subscriptions_started, new.subscriptions_ended), (1, 1));
2153 assert_eq!((old.announces_started, old.announces_ended), (1, 1));
2154 assert_eq!((old.broadcasts_started, old.broadcasts_ended), (1, 1));
2157 assert_eq!((new.broadcasts_started, new.broadcasts_ended), (0, 0));
2158 assert!(old.is_idle() && new.is_idle());
2159 }
2160
2161 #[test]
2162 fn meter_bumps_the_right_side() {
2163 let stats = test_stats();
2165 let ctx = stats.tier(Tier::default()).session("root");
2166
2167 let egress = ctx.egress("demo/bbb").meter();
2168 egress.group();
2169 egress.frames(3);
2170 egress.bytes(100);
2171
2172 let ingress = ctx.ingress("demo/bbb").meter();
2173 ingress.group();
2174 ingress.frames(1);
2175 ingress.bytes(7);
2176
2177 let counters = tier_counters(&stats, "demo/bbb", &Tier::default());
2178 let pub_ = counters.publisher.snapshot();
2179 let sub = counters.subscriber.snapshot();
2180 assert_eq!((pub_.groups, pub_.frames, pub_.bytes), (1, 3, 100));
2181 assert_eq!((sub.groups, sub.frames, sub.bytes), (1, 1, 7));
2182 }
2183
2184 #[test]
2185 fn egress_subscribe_drives_subscriptions_and_viewers() {
2186 let stats = test_stats();
2189 let ctx = stats.tier(Tier::default()).session("root");
2190 let raw = || tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2191
2192 let scope = ctx.egress("demo/bbb");
2193 let s1 = scope.subscribe();
2194 let s2 = scope.subscribe();
2195 let r = raw();
2196 assert_eq!(r.subscriptions_started, 2, "two track subs");
2197 assert_eq!(r.broadcasts_started, 1, "one context => one viewer");
2198 assert_eq!(r.broadcasts_ended, 0);
2199
2200 drop(s1);
2201 assert_eq!(raw().broadcasts_ended, 0, "context still has a sub open");
2202 drop(s2);
2203 let r = raw();
2204 assert_eq!(r.subscriptions_ended, 2);
2205 assert_eq!(r.broadcasts_ended, 1, "last sub closed => one broadcasts_ended");
2206 }
2207
2208 #[test]
2209 fn distinct_contexts_are_distinct_viewers() {
2210 let stats = test_stats();
2212 let raw = || tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2213
2214 let v1 = stats.tier(Tier::default()).session("a").egress("demo/bbb").subscribe();
2215 assert_eq!(raw().broadcasts_started, 1);
2216 let v2 = stats.tier(Tier::default()).session("b").egress("demo/bbb").subscribe();
2217 assert_eq!(raw().broadcasts_started, 2, "two distinct contexts => two viewers");
2218
2219 drop(v1);
2220 assert_eq!(raw().active_broadcasts(), 1);
2221 drop(v2);
2222 assert_eq!(raw().broadcasts_ended, 2);
2223 }
2224
2225 #[test]
2226 fn ingress_subscription_has_no_viewer() {
2227 let stats = test_stats();
2230 let ctx = stats.tier(Tier::default()).session("root");
2231 let guard = ctx.ingress("demo/bbb").subscribe();
2232 let sub = tier_counters(&stats, "demo/bbb", &Tier::default())
2233 .subscriber
2234 .snapshot();
2235 assert_eq!(sub.subscriptions_started, 1);
2236 assert_eq!(sub.broadcasts_started, 0, "ingress has no viewer refcount");
2237 drop(guard);
2238 assert_eq!(
2239 tier_counters(&stats, "demo/bbb", &Tier::default())
2240 .subscriber
2241 .snapshot()
2242 .subscriptions_ended,
2243 1
2244 );
2245 }
2246
2247 #[test]
2248 fn fetch_counts_separately_from_subscriptions() {
2249 let stats = test_stats();
2251 let ctx = stats.tier(Tier::default()).session("root");
2252 let scope = ctx.egress("demo/bbb");
2253 scope.fetch();
2254 scope.fetch();
2255 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2256 assert_eq!(r.fetches, 2);
2257 assert_eq!(r.subscriptions_started, 0);
2258 assert_eq!(r.broadcasts_started, 0);
2259 }
2260
2261 #[test]
2262 fn announce_guard_records_bytes_on_open_and_close() {
2263 let stats = test_stats();
2266 let ctx = stats.tier(Tier::default()).session("root");
2267 let path_len = "demo/bbb".len() as u64;
2268
2269 let guard = ctx.egress("demo/bbb").announce();
2270 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2271 assert_eq!(r.announces_started, 1);
2272 assert_eq!(r.announces_ended, 0);
2273 assert_eq!(r.announced_bytes, path_len);
2274
2275 drop(guard);
2276 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2277 assert_eq!(r.announces_ended, 1);
2278 assert_eq!(
2279 r.announced_bytes,
2280 path_len * 2,
2281 "path length recorded on open and close"
2282 );
2283 }
2284
2285 #[test]
2286 fn disabled_context_is_noop() {
2287 let ctx = Session::default();
2289 let scope = ctx.egress("demo/bbb");
2290 scope.meter().bytes(100);
2291 let _guard = scope.announce();
2292 let _sub = scope.subscribe();
2293 scope.fetch();
2294 assert!(ctx.inner.is_none());
2296 }
2297
2298 #[test]
2299 fn fetches_serde_roundtrips() {
2300 let old: Traffic = serde_json::from_str(r#"{"bytes":5}"#).expect("older shape parses");
2303 assert_eq!(old.fetches, 0);
2304
2305 let t = Traffic {
2306 fetches: 9,
2307 ..Default::default()
2308 };
2309 let json = serde_json::to_string(&t).unwrap();
2310 let back: Traffic = serde_json::from_str(&json).unwrap();
2311 assert_eq!(back.fetches, 9);
2312 }
2313
2314 #[test]
2315 fn session_snapshot_reads_ended_before_started() {
2316 let src = include_str!("stats.rs");
2320 let body_start = src
2321 .find("fn snapshot(&self) -> Presence")
2322 .expect("SessionCounters::snapshot fn present");
2323 let body = &src[body_start..];
2324 let ended_pos = body.find("self.sessions_ended.load").expect("sessions_ended load");
2325 let started_pos = body.find("self.sessions_started.load").expect("sessions_started load");
2326 assert!(
2327 ended_pos < started_pos,
2328 "sessions_ended must be loaded before sessions_started",
2329 );
2330 }
2331
2332 fn expected_traffic() -> Traffic {
2333 Traffic {
2334 announces_started: 2,
2335 announces_ended: 1,
2336 broadcasts_started: 4,
2337 broadcasts_ended: 3,
2338 subscriptions_started: 6,
2339 subscriptions_ended: 5,
2340 bytes: 9,
2341 ..Default::default()
2342 }
2343 }
2344
2345 fn expected_presence() -> Presence {
2346 Presence {
2347 sessions_started: 3,
2348 sessions_ended: 1,
2349 }
2350 }
2351
2352 #[test]
2353 fn traffic_decodes_old_new_and_both_spellings() {
2354 let expected = expected_traffic();
2357 let old = r#"{"announced":2,"announced_closed":1,"broadcasts":4,"broadcasts_closed":3,"subscriptions":6,"subscriptions_closed":5,"bytes":9}"#;
2358 let new = r#"{"announces_started":2,"announces_ended":1,"broadcasts_started":4,"broadcasts_ended":3,"subscriptions_started":6,"subscriptions_ended":5,"bytes":9}"#;
2359 assert_eq!(serde_json::from_str::<Traffic>(old).unwrap(), expected);
2360 assert_eq!(serde_json::from_str::<Traffic>(new).unwrap(), expected);
2361 let both = serde_json::to_string(&expected).unwrap();
2362 assert!(both.contains("\"announces_started\":2"), "{both}");
2363 assert!(both.contains("\"announced\":2"), "{both}");
2364 assert!(both.contains("\"announces_ended\":1"), "{both}");
2365 assert!(both.contains("\"announced_closed\":1"), "{both}");
2366 assert!(both.contains("\"broadcasts_started\":4"), "{both}");
2367 assert!(both.contains("\"broadcasts\":4"), "{both}");
2368 assert!(both.contains("\"subscriptions_started\":6"), "{both}");
2369 assert!(both.contains("\"subscriptions\":6"), "{both}");
2370 assert_eq!(serde_json::from_str::<Traffic>(&both).unwrap(), expected);
2371 }
2372
2373 #[test]
2374 fn presence_decodes_old_new_and_both_spellings() {
2375 let expected = expected_presence();
2376 assert_eq!(
2377 serde_json::from_str::<Presence>(r#"{"sessions":3,"sessions_closed":1}"#).unwrap(),
2378 expected
2379 );
2380 assert_eq!(
2381 serde_json::from_str::<Presence>(r#"{"sessions_started":3,"sessions_ended":1}"#).unwrap(),
2382 expected
2383 );
2384 let both = serde_json::to_string(&expected).unwrap();
2385 assert!(both.contains("\"sessions_started\":3"), "{both}");
2386 assert!(both.contains("\"sessions\":3"), "{both}");
2387 assert!(both.contains("\"sessions_ended\":1"), "{both}");
2388 assert!(both.contains("\"sessions_closed\":1"), "{both}");
2389 assert_eq!(serde_json::from_str::<Presence>(&both).unwrap(), expected);
2390 }
2391
2392 #[test]
2393 fn counter_edge_canonical_wins_when_spellings_disagree() {
2394 let traffic: Traffic =
2395 serde_json::from_str(r#"{"announces_started":9,"announced":1,"announces_ended":8,"announced_closed":0}"#)
2396 .unwrap();
2397 assert_eq!(traffic.announces_started, 9);
2398 assert_eq!(traffic.announces_ended, 8);
2399
2400 let presence: Presence =
2401 serde_json::from_str(r#"{"sessions_started":4,"sessions":0,"sessions_ended":2,"sessions_closed":9}"#)
2402 .unwrap();
2403 assert_eq!(presence.sessions_started, 4);
2404 assert_eq!(presence.sessions_ended, 2);
2405 }
2406
2407 #[test]
2408 fn counter_edge_refuses_null() {
2409 assert!(serde_json::from_str::<Traffic>(r#"{"announces_started":null,"announced":7}"#).is_err());
2411 assert!(serde_json::from_str::<Traffic>(r#"{"subscriptions_closed":null}"#).is_err());
2412 assert!(serde_json::from_str::<Presence>(r#"{"sessions_started":null}"#).is_err());
2413 assert!(serde_json::from_str::<Presence>(r#"{"sessions":null,"sessions_closed":1}"#).is_err());
2414 }
2415}