Skip to main content

prns_runtime/runtime/
health.rs

1use core::time::Duration;
2
3use crate::interfaces::{ConnectionState, InterfaceKind, InterfaceSnapshot};
4
5/// A compact, host-facing health summary for a running runtime.
6///
7/// The source of truth is the runtime's live [`InterfaceSnapshot`] list. Hosts can expose this over
8/// Android binders, daemon JSON, CLIs, or logs without each one re-learning how to fold interface
9/// state into the same operational counters.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub struct RuntimeHealth {
12    pub uptime_millis: u64,
13    pub interface_count: u32,
14    pub online_interface_count: u32,
15    pub local_client_count: u32,
16    pub route_count: u32,
17    pub link_count: u32,
18    pub transported_link_count: u32,
19    pub rx_bytes: u64,
20    pub tx_bytes: u64,
21    pub rx_bps: u64,
22    pub tx_bps: u64,
23}
24
25impl RuntimeHealth {
26    /// Fold a runtime snapshot list into the health shape hosts expose externally.
27    #[must_use]
28    pub fn from_snapshots(uptime: Duration, snapshots: &[InterfaceSnapshot]) -> Self {
29        let mut health = Self {
30            uptime_millis: millis_u64(uptime),
31            interface_count: snapshots.len() as u32,
32            ..Self::default()
33        };
34        for snapshot in snapshots {
35            if matches!(
36                snapshot.connection,
37                ConnectionState::Connected | ConnectionState::Degraded
38            ) {
39                health.online_interface_count = health.online_interface_count.saturating_add(1);
40            }
41            if snapshot.id.kind() == Some(InterfaceKind::LocalClient) {
42                health.local_client_count = health.local_client_count.saturating_add(1);
43            }
44            health.route_count = health.route_count.saturating_add(snapshot.destinations);
45            health.link_count = health.link_count.saturating_add(snapshot.links);
46            health.transported_link_count = health
47                .transported_link_count
48                .saturating_add(snapshot.transported_links);
49            health.rx_bytes = health.rx_bytes.saturating_add(snapshot.rx_bytes);
50            health.tx_bytes = health.tx_bytes.saturating_add(snapshot.tx_bytes);
51            if let Some(rates) = snapshot.transfer_rates {
52                health.rx_bps = health.rx_bps.saturating_add(u64::from(rates.rx_bps));
53                health.tx_bps = health.tx_bps.saturating_add(u64::from(rates.tx_bps));
54            }
55        }
56        health
57    }
58}
59
60fn millis_u64(duration: Duration) -> u64 {
61    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::interfaces::{InterfaceId, Membership, TransferRates};
68
69    #[test]
70    fn runtime_health_aggregates_interface_snapshots() {
71        let local_client = InterfaceSnapshot {
72            id: InterfaceId::from_channel_tag(InterfaceKind::LocalClient, b"app"),
73            mode: crate::interfaces::InterfaceMode::Full,
74            gravity: crate::interfaces::InterfaceGravity::ZERO,
75            connection: ConnectionState::Connected,
76            failure_reason: None,
77            rx_bytes: 10,
78            tx_bytes: 20,
79            transfer_rates: Some(TransferRates {
80                rx_bps: 3,
81                tx_bps: 4,
82            }),
83            destinations: 2,
84            links: 1,
85            transported_links: 0,
86            membership: Membership::Independent,
87        };
88        let wifi_peer = InterfaceSnapshot {
89            id: InterfaceId::from_channel_tag(InterfaceKind::WifiPeer, b"peer"),
90            mode: crate::interfaces::InterfaceMode::Full,
91            gravity: crate::interfaces::InterfaceGravity::ZERO,
92            connection: ConnectionState::Reconnecting,
93            failure_reason: None,
94            rx_bytes: 5,
95            tx_bytes: 7,
96            transfer_rates: None,
97            destinations: 1,
98            links: 0,
99            transported_links: 2,
100            membership: Membership::FleetMember {
101                supervisor_id: InterfaceId::from_channel_tag(InterfaceKind::AutoWifi, b"wifi"),
102            },
103        };
104
105        let health =
106            RuntimeHealth::from_snapshots(Duration::from_millis(123), &[local_client, wifi_peer]);
107
108        assert_eq!(health.uptime_millis, 123);
109        assert_eq!(health.interface_count, 2);
110        assert_eq!(health.online_interface_count, 1);
111        assert_eq!(health.local_client_count, 1);
112        assert_eq!(health.route_count, 3);
113        assert_eq!(health.link_count, 1);
114        assert_eq!(health.transported_link_count, 2);
115        assert_eq!(health.rx_bytes, 15);
116        assert_eq!(health.tx_bytes, 27);
117        assert_eq!(health.rx_bps, 3);
118        assert_eq!(health.tx_bps, 4);
119    }
120}