#[cfg(feature = "statistics")]
use embassy_time::Instant;
#[cfg(feature = "statistics")]
use portable_atomic::AtomicBool;
#[cfg(feature = "statistics")]
use portable_atomic::{AtomicU32, AtomicU64, Ordering};
#[cfg(feature = "statistics")]
use crate::logging::logging::reset_global_log_drops;
#[cfg(feature = "statistics")]
pub(crate) struct GlobalStats {
pub tx_count: AtomicU64,
pub rx_count: AtomicU64,
pub rx_drop_count: AtomicU32,
pub rx_filtered: AtomicU32,
pub rx_oversize: AtomicU32,
pub rx_queue_full: AtomicU32,
pub rx_seq_gap: AtomicU32,
pub rx_policy_suppressed: AtomicU32,
pub capture_start_time: AtomicU64,
pub tx_rate_hz: AtomicU32,
pub rx_rate_hz: AtomicU32,
}
#[cfg(feature = "statistics")]
pub(crate) static BB_FORMAT_HIST: [AtomicU32; 8] = [
AtomicU32::new(0),
AtomicU32::new(0),
AtomicU32::new(0),
AtomicU32::new(0),
AtomicU32::new(0),
AtomicU32::new(0),
AtomicU32::new(0),
AtomicU32::new(0),
];
#[cfg(feature = "statistics")]
pub(crate) static STATS: GlobalStats = GlobalStats {
tx_count: AtomicU64::new(0),
rx_count: AtomicU64::new(0),
rx_drop_count: AtomicU32::new(0),
rx_filtered: AtomicU32::new(0),
rx_oversize: AtomicU32::new(0),
rx_queue_full: AtomicU32::new(0),
rx_seq_gap: AtomicU32::new(0),
rx_policy_suppressed: AtomicU32::new(0),
capture_start_time: AtomicU64::new(0),
tx_rate_hz: AtomicU32::new(0),
rx_rate_hz: AtomicU32::new(0),
};
#[cfg(feature = "statistics")]
pub(crate) static RESET_SEQ_TRACKER: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "statistics")]
static SEQ_DROP_DETECTION_ENABLED: AtomicBool = AtomicBool::new(false);
pub(crate) fn set_seq_drop_detection(enabled: bool) {
#[cfg(feature = "statistics")]
{
SEQ_DROP_DETECTION_ENABLED.store(enabled, Ordering::Relaxed);
}
#[cfg(not(feature = "statistics"))]
{
let _ = enabled;
}
}
#[cfg(feature = "statistics")]
pub(crate) fn seq_drop_detection_enabled() -> bool {
SEQ_DROP_DETECTION_ENABLED.load(Ordering::Relaxed)
}
#[cfg(feature = "statistics")]
pub(crate) fn reset() {
#[cfg(feature = "statistics")]
{
STATS.tx_count.store(0, Ordering::Relaxed);
STATS.rx_count.store(0, Ordering::Relaxed);
STATS.rx_drop_count.store(0, Ordering::Relaxed);
STATS.rx_filtered.store(0, Ordering::Relaxed);
STATS.rx_oversize.store(0, Ordering::Relaxed);
STATS.rx_queue_full.store(0, Ordering::Relaxed);
STATS.rx_seq_gap.store(0, Ordering::Relaxed);
STATS.rx_policy_suppressed.store(0, Ordering::Relaxed);
PER_TX.lock(|t| t.borrow_mut().clear());
STATS.tx_rate_hz.store(0, Ordering::Relaxed);
STATS.rx_rate_hz.store(0, Ordering::Relaxed);
for slot in BB_FORMAT_HIST.iter() {
slot.store(0, Ordering::Relaxed);
}
reset_global_log_drops();
}
}
#[cfg(feature = "statistics")]
pub fn get_total_rx_packets() -> u64 {
STATS.rx_count.load(Ordering::Relaxed)
}
#[cfg(feature = "statistics")]
pub fn get_total_tx_packets() -> u64 {
STATS.tx_count.load(Ordering::Relaxed)
}
#[cfg(feature = "statistics")]
pub fn get_rx_rate_hz() -> u32 {
STATS.rx_rate_hz.load(Ordering::Relaxed)
}
#[cfg(feature = "statistics")]
pub fn get_tx_rate_hz() -> u32 {
STATS.tx_rate_hz.load(Ordering::Relaxed)
}
#[cfg(feature = "statistics")]
pub fn get_pps_rx() -> u64 {
let start_time = Instant::from_ticks(STATS.capture_start_time.load(Ordering::Relaxed));
let elapsed_secs = start_time.elapsed().as_secs();
let total_packets = STATS.rx_count.load(Ordering::Relaxed);
if elapsed_secs == 0 {
return total_packets;
}
total_packets / elapsed_secs
}
#[cfg(feature = "statistics")]
pub(crate) fn record_tx() {
STATS.tx_count.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "statistics")]
pub fn record_emitter_tx() {
record_tx();
}
#[cfg(feature = "statistics")]
pub fn record_collector_rx() {
STATS.rx_count.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "statistics")]
pub fn record_collector_rx_drop() {
record_loss(&STATS.rx_queue_full, 1);
}
#[cfg(feature = "statistics")]
pub fn stats_begin_run() {
reset();
STATS
.capture_start_time
.store(Instant::now().as_ticks(), Ordering::Relaxed);
}
#[cfg(feature = "statistics")]
pub fn get_pps_tx() -> u64 {
let start_time = Instant::from_ticks(STATS.capture_start_time.load(Ordering::Relaxed));
let elapsed_secs = start_time.elapsed().as_secs();
let total_packets = STATS.tx_count.load(Ordering::Relaxed);
if elapsed_secs == 0 {
return total_packets;
}
total_packets / elapsed_secs
}
#[cfg(feature = "statistics")]
pub fn get_dropped_packets_rx() -> u32 {
STATS.rx_drop_count.load(Ordering::Relaxed)
}
#[cfg(feature = "statistics")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropBreakdown {
pub filtered: u32,
pub policy_suppressed: u32,
pub oversize: u32,
pub queue_full: u32,
pub seq_gap: u32,
pub log_dropped: u32,
}
#[cfg(feature = "statistics")]
pub fn get_drop_breakdown() -> DropBreakdown {
DropBreakdown {
filtered: STATS.rx_filtered.load(Ordering::Relaxed),
policy_suppressed: STATS.rx_policy_suppressed.load(Ordering::Relaxed),
oversize: STATS.rx_oversize.load(Ordering::Relaxed),
queue_full: STATS.rx_queue_full.load(Ordering::Relaxed),
seq_gap: STATS.rx_seq_gap.load(Ordering::Relaxed),
log_dropped: crate::logging::logging::get_log_packet_drops(),
}
}
#[cfg(feature = "statistics")]
pub(crate) fn record_loss(counter: &AtomicU32, n: u32) {
counter.fetch_add(n, Ordering::Relaxed);
STATS.rx_drop_count.fetch_add(n, Ordering::Relaxed);
}
#[cfg(feature = "statistics")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct TxStats {
pub mac: [u8; 6],
pub frames: u32,
pub seq_gaps: u32,
pub retries: u32,
last_seq: u16,
last_used: u32,
resync: bool,
}
#[cfg(feature = "statistics")]
pub const MAX_TRACKED_TX: usize = 16;
#[cfg(feature = "statistics")]
type PerTxTable = heapless::Vec<TxStats, MAX_TRACKED_TX>;
#[cfg(feature = "statistics")]
static PER_TX: embassy_sync::blocking_mutex::Mutex<
embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex,
core::cell::RefCell<PerTxTable>,
> = embassy_sync::blocking_mutex::Mutex::new(core::cell::RefCell::new(heapless::Vec::new()));
#[cfg(feature = "statistics")]
static PER_TX_CLOCK: AtomicU32 = AtomicU32::new(0);
#[cfg(feature = "statistics")]
pub(crate) fn record_tx_frame(mac: [u8; 6], seq: Option<u16>, retry: Option<bool>) {
let now = PER_TX_CLOCK.fetch_add(1, Ordering::Relaxed);
let resync_all = RESET_SEQ_TRACKER.swap(false, Ordering::Relaxed);
let mut gap = 0u32;
PER_TX.lock(|t| {
let mut t = t.borrow_mut();
if resync_all {
for e in t.iter_mut() {
e.resync = true;
}
}
let idx = match t.iter().position(|e| e.mac == mac) {
Some(i) => i,
None => {
let fresh = TxStats {
mac,
last_seq: seq.unwrap_or(0),
..TxStats::default()
};
if t.push(fresh).is_err() {
let lru = t
.iter()
.enumerate()
.min_by_key(|(_, e)| e.last_used)
.map(|(i, _)| i)
.unwrap_or(0);
t[lru] = fresh;
lru
} else {
t.len() - 1
}
}
};
let e = &mut t[idx];
let first = e.frames == 0 || core::mem::take(&mut e.resync);
e.frames += 1;
e.last_used = now;
if retry == Some(true) {
e.retries += 1;
return;
}
if let Some(seq) = seq {
if !first {
let diff = seq.wrapping_sub(e.last_seq) & 0x0FFF;
if diff > 1 && diff < 500 {
gap = (diff - 1) as u32;
e.seq_gaps += gap;
}
}
e.last_seq = seq;
}
});
if gap > 0 {
record_loss(&STATS.rx_seq_gap, gap);
}
}
#[cfg(feature = "statistics")]
pub fn snapshot_tx_stats(out: &mut [TxStats]) -> usize {
PER_TX.lock(|t| {
let t = t.borrow();
let n = t.len().min(out.len());
out[..n].copy_from_slice(&t[..n]);
n
})
}
#[cfg(feature = "statistics")]
pub fn snapshot_bb_format_histogram() -> [u32; 8] {
let mut out = [0u32; 8];
for (slot, count) in BB_FORMAT_HIST.iter().zip(out.iter_mut()) {
*count = slot.load(Ordering::Relaxed);
}
out
}
#[cfg(all(feature = "statistics", any(feature = "esp32c5", feature = "esp32c6")))]
pub(crate) fn record_cur_bb_format(fmt: u32) {
if fmt < 8 {
BB_FORMAT_HIST[fmt as usize].fetch_add(1, Ordering::Relaxed);
}
}