use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll};
use std::sync::Arc;
use std::time::Duration;
use portable_atomic::{AtomicU64, Ordering};
use crate::sync_marker::MaybeSendSync;
#[derive(Debug, Default)]
pub struct SessionStats {
bytes_sent: AtomicU64,
bytes_received: AtomicU64,
frames_sent: AtomicU64,
frames_received: AtomicU64,
messages_sent: AtomicU64,
messages_received: AtomicU64,
events_dropped: AtomicU64,
reconnects: AtomicU64,
last_data_received_ms: AtomicU64,
first_send_since_recv_ms: AtomicU64,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default)]
pub struct StatsSnapshot {
pub bytes_sent: u64,
pub bytes_received: u64,
pub frames_sent: u64,
pub frames_received: u64,
pub messages_sent: u64,
pub messages_received: u64,
pub events_dropped: u64,
pub reconnects: u64,
pub reconnect_errors: u32,
pub resends_throttled: u64,
pub last_data_received_ms: u64,
}
impl SessionStats {
pub fn new() -> Self {
Self::default()
}
fn now_ms() -> u64 {
crate::time::now_millis().max(0) as u64
}
#[inline]
pub fn record_frame_sent(&self, wire_bytes: usize) {
self.bytes_sent
.fetch_add(wire_bytes as u64, Ordering::Relaxed);
self.frames_sent.fetch_add(1, Ordering::Relaxed);
let last_recv = self.last_data_received_ms.load(Ordering::Relaxed);
let anchor = self.first_send_since_recv_ms.load(Ordering::Relaxed);
if anchor == 0 || anchor <= last_recv {
let now = Self::now_ms();
let _ = self.first_send_since_recv_ms.fetch_update(
Ordering::Relaxed,
Ordering::Relaxed,
|current| (current == 0 || current <= last_recv).then_some(now),
);
}
}
#[inline]
pub fn record_recv_batch(&self, wire_bytes: usize, frames: u32) {
self.bytes_received
.fetch_add(wire_bytes as u64, Ordering::Relaxed);
self.frames_received
.fetch_add(frames as u64, Ordering::Relaxed);
if frames > 1 {
self.last_data_received_ms
.store(Self::now_ms(), Ordering::Relaxed);
self.first_send_since_recv_ms.store(0, Ordering::Relaxed);
}
}
#[inline]
pub fn mark_recv_activity(&self) {
self.last_data_received_ms
.store(Self::now_ms(), Ordering::Relaxed);
self.first_send_since_recv_ms.store(0, Ordering::Relaxed);
}
#[inline]
pub fn record_message_sent(&self) {
self.messages_sent.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_message_received(&self) {
self.messages_received.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_reconnect(&self) {
self.reconnects.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_event_dropped(&self) {
self.events_dropped.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn events_dropped(&self) -> u64 {
self.events_dropped.load(Ordering::Relaxed)
}
pub fn reset_connection_activity(&self) {
self.last_data_received_ms.store(0, Ordering::Relaxed);
self.first_send_since_recv_ms.store(0, Ordering::Relaxed);
}
#[inline]
pub fn first_send_since_recv_ms(&self) -> u64 {
self.first_send_since_recv_ms.load(Ordering::Relaxed)
}
#[inline]
pub fn last_data_received_ms(&self) -> u64 {
self.last_data_received_ms.load(Ordering::Relaxed)
}
pub fn snapshot(&self) -> StatsSnapshot {
StatsSnapshot {
bytes_sent: self.bytes_sent.load(Ordering::Relaxed),
bytes_received: self.bytes_received.load(Ordering::Relaxed),
frames_sent: self.frames_sent.load(Ordering::Relaxed),
frames_received: self.frames_received.load(Ordering::Relaxed),
messages_sent: self.messages_sent.load(Ordering::Relaxed),
messages_received: self.messages_received.load(Ordering::Relaxed),
events_dropped: self.events_dropped.load(Ordering::Relaxed),
reconnects: self.reconnects.load(Ordering::Relaxed),
reconnect_errors: 0,
resends_throttled: 0,
last_data_received_ms: self.last_data_received_ms.load(Ordering::Relaxed),
}
}
}
pub trait HeapSize {
fn heap_bytes(&self) -> usize;
}
impl<T: HeapSize> HeapSize for Arc<T> {
fn heap_bytes(&self) -> usize {
size_of::<T>() + T::heap_bytes(self)
}
}
impl HeapSize for Vec<u8> {
fn heap_bytes(&self) -> usize {
self.capacity()
}
}
impl HeapSize for String {
fn heap_bytes(&self) -> usize {
self.capacity()
}
}
impl HeapSize for str {
fn heap_bytes(&self) -> usize {
self.len()
}
}
impl HeapSize for wacore_binary::CompactString {
fn heap_bytes(&self) -> usize {
if self.is_heap_allocated() {
self.len()
} else {
0
}
}
}
impl HeapSize for wacore_binary::Jid {
fn heap_bytes(&self) -> usize {
self.user.heap_bytes()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CollectionStats {
pub entries: u64,
pub bytes: u64,
}
impl CollectionStats {
pub fn new(entries: u64, bytes: u64) -> Self {
Self { entries, bytes }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct StorageResourceReport {
pub memory_bytes: Option<u64>,
pub pages: Option<u64>,
pub io_read_bytes: Option<u64>,
pub io_write_bytes: Option<u64>,
}
impl StorageResourceReport {
pub fn total_bytes(&self) -> u64 {
self.memory_bytes.unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TransportResourceReport {
pub read_buffer_bytes: Option<u64>,
pub write_buffer_bytes: Option<u64>,
pub tls_state_bytes: Option<u64>,
}
impl TransportResourceReport {
pub fn total_bytes(&self) -> u64 {
self.read_buffer_bytes
.unwrap_or(0)
.saturating_add(self.write_buffer_bytes.unwrap_or(0))
.saturating_add(self.tls_state_bytes.unwrap_or(0))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct HttpResourceReport {
pub pool_connections: Option<u64>,
pub pool_buffer_bytes: Option<u64>,
pub inflight_bytes: Option<u64>,
}
impl HttpResourceReport {
pub fn total_bytes(&self) -> u64 {
self.pool_buffer_bytes
.unwrap_or(0)
.saturating_add(self.inflight_bytes.unwrap_or(0))
}
}
pub trait TaskInstrument: MaybeSendSync {
fn on_poll_start(&self);
fn on_poll_end(&self);
}
pub struct MeteredFuture<F> {
inner: F,
instrument: Arc<dyn TaskInstrument>,
}
impl<F> MeteredFuture<F> {
pub fn new(inner: F, instrument: Arc<dyn TaskInstrument>) -> Self {
Self { inner, instrument }
}
}
struct PollGuard<'a>(&'a dyn TaskInstrument);
impl Drop for PollGuard<'_> {
fn drop(&mut self) {
self.0.on_poll_end();
}
}
impl<F: Future + Unpin> Future for MeteredFuture<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
this.instrument.on_poll_start();
let _guard = PollGuard(&*this.instrument);
Pin::new(&mut this.inner).poll(cx)
}
}
#[derive(Debug, Default)]
pub struct CpuMeter {
busy_nanos: AtomicU64,
polls: AtomicU64,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default)]
pub struct CpuSnapshot {
pub busy: Duration,
pub polls: u64,
}
impl CpuMeter {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> CpuSnapshot {
CpuSnapshot {
busy: Duration::from_nanos(self.busy_nanos.load(Ordering::Relaxed)),
polls: self.polls.load(Ordering::Relaxed),
}
}
}
std::thread_local! {
static POLL_START: core::cell::RefCell<Vec<crate::time::Instant>> =
const { core::cell::RefCell::new(Vec::new()) };
}
impl TaskInstrument for CpuMeter {
fn on_poll_start(&self) {
POLL_START.with(|s| s.borrow_mut().push(crate::time::Instant::now()));
}
fn on_poll_end(&self) {
if let Some(start) = POLL_START.with(|s| s.borrow_mut().pop()) {
self.busy_nanos
.fetch_add(start.elapsed().as_nanos() as u64, Ordering::Relaxed);
self.polls.fetch_add(1, Ordering::Relaxed);
}
}
}
std::thread_local! {
static ACTIVE_ALLOC_METER: core::cell::RefCell<Vec<Arc<AllocMeterInner>>> =
const { core::cell::RefCell::new(Vec::new()) };
}
#[derive(Debug, Default)]
struct AllocMeterInner {
allocated: AtomicU64,
freed: AtomicU64,
allocations: AtomicU64,
}
#[derive(Debug, Default, Clone)]
pub struct AllocMeter {
inner: Arc<AllocMeterInner>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default)]
pub struct AllocSnapshot {
pub allocated_bytes: u64,
pub freed_bytes: u64,
pub allocations: u64,
}
impl AllocSnapshot {
pub fn net_bytes(&self) -> u64 {
self.allocated_bytes.saturating_sub(self.freed_bytes)
}
}
impl AllocMeter {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn on_alloc(bytes: usize) {
Self::with_active(|inner| {
inner.allocated.fetch_add(bytes as u64, Ordering::Relaxed);
inner.allocations.fetch_add(1, Ordering::Relaxed);
});
}
#[inline]
pub fn on_dealloc(bytes: usize) {
Self::with_active(|inner| {
inner.freed.fetch_add(bytes as u64, Ordering::Relaxed);
});
}
#[inline]
fn with_active(f: impl FnOnce(&AllocMeterInner)) {
let _ = ACTIVE_ALLOC_METER.try_with(|cell| {
if let Ok(stack) = cell.try_borrow()
&& let Some(inner) = stack.last()
{
f(inner);
}
});
}
pub fn snapshot(&self) -> AllocSnapshot {
AllocSnapshot {
allocated_bytes: self.inner.allocated.load(Ordering::Relaxed),
freed_bytes: self.inner.freed.load(Ordering::Relaxed),
allocations: self.inner.allocations.load(Ordering::Relaxed),
}
}
}
impl TaskInstrument for AllocMeter {
fn on_poll_start(&self) {
let _ = ACTIVE_ALLOC_METER.try_with(|cell| cell.borrow_mut().push(self.inner.clone()));
}
fn on_poll_end(&self) {
let popped = ACTIVE_ALLOC_METER
.try_with(|cell| cell.borrow_mut().pop())
.ok()
.flatten();
drop(popped);
}
}
use crate::runtime::{AbortHandle, Runtime};
pub struct InstrumentedRuntime {
inner: Arc<dyn Runtime>,
instrument: Arc<dyn TaskInstrument>,
}
impl InstrumentedRuntime {
pub fn new(inner: Arc<dyn Runtime>, instrument: Arc<dyn TaskInstrument>) -> Self {
Self { inner, instrument }
}
}
#[cfg(target_arch = "wasm32")]
unsafe impl Send for InstrumentedRuntime {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for InstrumentedRuntime {}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl Runtime for InstrumentedRuntime {
fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> AbortHandle {
self.inner.spawn(Box::pin(MeteredFuture::new(
future,
self.instrument.clone(),
)))
}
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> {
self.inner.sleep(duration)
}
fn spawn_blocking(
&self,
f: Box<dyn FnOnce() + Send + 'static>,
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
let instrument = self.instrument.clone();
self.inner.spawn_blocking(Box::new(move || {
instrument.on_poll_start();
let _guard = PollGuard(&*instrument);
f();
}))
}
fn yield_now(&self) -> Option<Pin<Box<dyn Future<Output = ()> + Send>>> {
self.inner.yield_now()
}
fn yield_frequency(&self) -> u32 {
self.inner.yield_frequency()
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait::async_trait(?Send)]
impl Runtime for InstrumentedRuntime {
fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + 'static>>) -> AbortHandle {
self.inner.spawn(Box::pin(MeteredFuture::new(
future,
self.instrument.clone(),
)))
}
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()>>> {
self.inner.sleep(duration)
}
fn spawn_blocking(&self, f: Box<dyn FnOnce() + 'static>) -> Pin<Box<dyn Future<Output = ()>>> {
let instrument = self.instrument.clone();
self.inner.spawn_blocking(Box::new(move || {
instrument.on_poll_start();
let _guard = PollGuard(&*instrument);
f();
}))
}
fn yield_now(&self) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
self.inner.yield_now()
}
fn yield_frequency(&self) -> u32 {
self.inner.yield_frequency()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_reflects_recorded_traffic() {
let stats = SessionStats::new();
stats.record_frame_sent(100);
stats.record_frame_sent(50);
assert!(stats.first_send_since_recv_ms() > 0);
stats.record_recv_batch(300, 2);
stats.record_message_sent();
stats.record_message_received();
stats.record_reconnect();
stats.record_event_dropped();
stats.record_event_dropped();
let snap = stats.snapshot();
assert_eq!(snap.bytes_sent, 150);
assert_eq!(snap.frames_sent, 2);
assert_eq!(snap.bytes_received, 300);
assert_eq!(snap.frames_received, 2);
assert_eq!(snap.messages_sent, 1);
assert_eq!(snap.messages_received, 1);
assert_eq!(snap.reconnects, 1);
assert_eq!(snap.events_dropped, 2);
assert!(snap.last_data_received_ms > 0);
}
#[test]
fn dead_socket_anchor_holds_across_continued_sends() {
use crate::protocol::keepalive::is_dead_socket;
let stats = SessionStats::new();
assert_eq!(stats.first_send_since_recv_ms(), 0, "unarmed initially");
stats.record_frame_sent(10);
let armed = stats.first_send_since_recv_ms();
assert!(armed > 0, "the first send arms the dead-socket anchor");
std::thread::sleep(Duration::from_millis(2));
stats.record_frame_sent(10);
stats.record_frame_sent(10);
assert_eq!(
stats.first_send_since_recv_ms(),
armed,
"later sends keep the earliest anchor"
);
let now = crate::time::now_millis().max(0) as u64;
let stale = now.saturating_sub(21_000);
assert!(
is_dead_socket(stale, stale.saturating_sub(5_000)),
"20s past the anchor with no receive => dead"
);
stats.mark_recv_activity();
assert_eq!(
stats.first_send_since_recv_ms(),
0,
"a receive cancels the anchor"
);
stats.record_frame_sent(10);
assert!(
stats.first_send_since_recv_ms() > 0,
"the next send after a receive re-arms the anchor"
);
}
#[test]
fn stale_pre_receive_anchor_self_heals_on_next_send() {
use crate::protocol::keepalive::is_dead_socket;
let stats = SessionStats::new();
let base = SessionStats::now_ms();
stats.last_data_received_ms.store(base, Ordering::Relaxed);
stats
.first_send_since_recv_ms
.store(base.saturating_sub(1_000), Ordering::Relaxed);
stats.record_frame_sent(10);
let rearmed = stats.first_send_since_recv_ms();
assert!(
rearmed >= base,
"a stale pre-receive anchor must re-arm to a post-receive send, got {rearmed} < {base}"
);
assert!(
!is_dead_socket(rearmed, base),
"the re-armed anchor is after the receive, so the socket is not dead"
);
}
#[test]
fn reset_connection_activity_keeps_traffic() {
let stats = SessionStats::new();
stats.record_frame_sent(10);
stats.record_recv_batch(20, 1);
stats.reset_connection_activity();
let snap = stats.snapshot();
assert_eq!(stats.first_send_since_recv_ms(), 0);
assert_eq!(snap.last_data_received_ms, 0);
assert_eq!(snap.bytes_sent, 10);
assert_eq!(snap.bytes_received, 20);
}
#[test]
fn cpu_meter_counts_polls_and_busy_time() {
let meter = Arc::new(CpuMeter::new());
let instrument: Arc<dyn TaskInstrument> = meter.clone();
let mut fut = MeteredFuture::new(Box::pin(async {}), instrument);
let waker = std::task::Waker::noop();
let mut cx = Context::from_waker(waker);
assert!(Pin::new(&mut fut).poll(&mut cx).is_ready());
let snap = meter.snapshot();
assert_eq!(snap.polls, 1);
}
#[test]
fn alloc_meter_charges_only_the_active_scope() {
let meter = AllocMeter::new();
AllocMeter::on_alloc(9999);
meter.on_poll_start();
AllocMeter::on_alloc(1000);
AllocMeter::on_alloc(500);
AllocMeter::on_dealloc(200);
meter.on_poll_end();
AllocMeter::on_alloc(7777);
AllocMeter::on_dealloc(7777);
let snap = meter.snapshot();
assert_eq!(snap.allocated_bytes, 1500);
assert_eq!(snap.freed_bytes, 200);
assert_eq!(snap.allocations, 2);
assert_eq!(snap.net_bytes(), 1300);
}
#[test]
fn alloc_meter_attributes_nested_scopes_to_the_innermost() {
let outer = AllocMeter::new();
let inner = AllocMeter::new();
outer.on_poll_start();
AllocMeter::on_alloc(100); inner.on_poll_start();
AllocMeter::on_alloc(30); inner.on_poll_end();
AllocMeter::on_alloc(70); outer.on_poll_end();
assert_eq!(outer.snapshot().allocated_bytes, 170);
assert_eq!(inner.snapshot().allocated_bytes, 30);
}
#[test]
fn alloc_meter_survives_realloc_reentrancy_during_poll_start() {
let meters: Vec<AllocMeter> = (0..64).map(|_| AllocMeter::new()).collect();
for m in &meters {
m.on_poll_start();
AllocMeter::on_alloc(1);
}
for m in meters.iter().rev() {
m.on_poll_end();
}
assert_eq!(meters.last().unwrap().snapshot().allocations, 1);
}
#[test]
fn alloc_meter_scope_outlives_a_dropped_handle() {
let keep = AllocMeter::new();
let temp = keep.clone(); temp.on_poll_start();
drop(temp); AllocMeter::on_alloc(128);
keep.on_poll_end(); assert_eq!(keep.snapshot().allocated_bytes, 128);
}
#[test]
fn resource_report_total_bytes_saturate() {
let t = TransportResourceReport {
read_buffer_bytes: Some(u64::MAX),
write_buffer_bytes: Some(10),
tls_state_bytes: Some(10),
};
assert_eq!(t.total_bytes(), u64::MAX, "transport total must not wrap");
let h = HttpResourceReport {
pool_connections: Some(3),
pool_buffer_bytes: Some(u64::MAX),
inflight_bytes: Some(1),
};
assert_eq!(h.total_bytes(), u64::MAX, "http total must not wrap");
}
}