use std::collections::HashMap;
use std::time::Duration;
use freenet_stdlib::prelude::ContractInstanceId;
use parking_lot::Mutex;
use crate::node::background_task_monitor::BackgroundTaskMonitor;
const ROLLUP_WINDOW: Duration = Duration::from_secs(60);
const MAX_TRACKED_CONTRACTS: usize = 256;
const TOP_CONTRACTS_REPORTED: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum PayloadArm {
Delta,
FullDeltaSuppressed,
FullNotEfficient,
FullComputeFailed,
FullNoSummary,
}
impl PayloadArm {
pub(crate) const ALL: [PayloadArm; 5] = [
PayloadArm::Delta,
PayloadArm::FullDeltaSuppressed,
PayloadArm::FullNotEfficient,
PayloadArm::FullComputeFailed,
PayloadArm::FullNoSummary,
];
const COUNT: usize = Self::ALL.len();
const fn index(self) -> usize {
match self {
PayloadArm::Delta => 0,
PayloadArm::FullDeltaSuppressed => 1,
PayloadArm::FullNotEfficient => 2,
PayloadArm::FullComputeFailed => 3,
PayloadArm::FullNoSummary => 4,
}
}
pub(crate) const fn label(self) -> &'static str {
match self {
PayloadArm::Delta => "delta",
PayloadArm::FullDeltaSuppressed => "full_delta_suppressed",
PayloadArm::FullNotEfficient => "full_not_efficient",
PayloadArm::FullComputeFailed => "full_compute_failed",
PayloadArm::FullNoSummary => "full_no_summary",
}
}
pub(crate) const fn is_full_state(self) -> bool {
!matches!(self, PayloadArm::Delta)
}
}
struct Window {
sends: [u64; PayloadArm::COUNT],
bytes: [u64; PayloadArm::COUNT],
contract_full_state_bytes: HashMap<ContractInstanceId, u64>,
attribution_dropped_sends: u64,
attribution_dropped_bytes: u64,
}
impl Default for Window {
fn default() -> Self {
Self {
sends: [0; PayloadArm::COUNT],
bytes: [0; PayloadArm::COUNT],
contract_full_state_bytes: HashMap::new(),
attribution_dropped_sends: 0,
attribution_dropped_bytes: 0,
}
}
}
pub(crate) struct PayloadMix {
window: Mutex<Window>,
}
impl PayloadMix {
pub(crate) fn new() -> Self {
Self {
window: Mutex::new(Window::default()),
}
}
pub(crate) fn record_delivered(
&self,
arm: PayloadArm,
contract: &ContractInstanceId,
payload_bytes: usize,
) {
let bytes = payload_bytes as u64;
let idx = arm.index();
let mut w = self.window.lock();
w.sends[idx] = w.sends[idx].saturating_add(1);
w.bytes[idx] = w.bytes[idx].saturating_add(bytes);
if arm.is_full_state() {
if let Some(tally) = w.contract_full_state_bytes.get_mut(contract) {
*tally = tally.saturating_add(bytes);
} else if w.contract_full_state_bytes.len() < MAX_TRACKED_CONTRACTS {
w.contract_full_state_bytes.insert(*contract, bytes);
} else {
w.attribution_dropped_sends = w.attribution_dropped_sends.saturating_add(1);
w.attribution_dropped_bytes = w.attribution_dropped_bytes.saturating_add(bytes);
}
}
}
fn take_window(&self) -> Window {
std::mem::take(&mut *self.window.lock())
}
}
impl Window {
fn arms(&self) -> Vec<(PayloadArm, u64, u64)> {
PayloadArm::ALL
.iter()
.map(|arm| {
let idx = arm.index();
(*arm, self.sends[idx], self.bytes[idx])
})
.collect()
}
fn top_contracts(&self) -> Vec<(ContractInstanceId, u64)> {
let mut tallies: Vec<(ContractInstanceId, u64)> = self
.contract_full_state_bytes
.iter()
.map(|(k, v)| (*k, *v))
.collect();
tallies.sort_by(|a, b| {
b.1.cmp(&a.1)
.then_with(|| a.0.as_bytes().cmp(b.0.as_bytes()))
});
tallies.truncate(TOP_CONTRACTS_REPORTED);
tallies
}
}
fn payload_mix_json(
arms: &[(PayloadArm, u64, u64)],
contracts: &[(ContractInstanceId, u64)],
tracked_full_state_bytes: u64,
contracts_tracked: u64,
attribution_dropped_sends: u64,
attribution_dropped_bytes: u64,
window_secs: u64,
) -> serde_json::Value {
let mut obj = serde_json::Map::new();
let mut total_sends = 0u64;
let mut total_bytes = 0u64;
let mut full_state_bytes = 0u64;
for (arm, sends, bytes) in arms {
obj.insert(format!("{}_sends", arm.label()), (*sends).into());
obj.insert(format!("{}_bytes", arm.label()), (*bytes).into());
total_sends += sends;
total_bytes += bytes;
if arm.is_full_state() {
full_state_bytes += bytes;
}
}
obj.insert("total_sends".into(), total_sends.into());
obj.insert("total_bytes".into(), total_bytes.into());
obj.insert("full_state_bytes".into(), full_state_bytes.into());
let full_state_share = if total_bytes == 0 {
0.0
} else {
full_state_bytes as f64 / total_bytes as f64
};
obj.insert(
"full_state_byte_share".into(),
serde_json::Number::from_f64(full_state_share)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
);
obj.insert(
"top_contracts_by_full_state_bytes".into(),
serde_json::Value::Array(
contracts
.iter()
.map(
|(id, bytes)| serde_json::json!({ "contract": id.to_string(), "bytes": bytes }),
)
.collect(),
),
);
let top_sum: u64 = contracts.iter().map(|(_, b)| *b).sum();
let other_contracts_bytes = tracked_full_state_bytes.saturating_sub(top_sum);
obj.insert("other_contracts_bytes".into(), other_contracts_bytes.into());
obj.insert("contracts_tracked".into(), contracts_tracked.into());
obj.insert(
"attribution_dropped_sends".into(),
attribution_dropped_sends.into(),
);
obj.insert(
"attribution_dropped_bytes".into(),
attribution_dropped_bytes.into(),
);
obj.insert("window_secs".into(), window_secs.into());
serde_json::Value::Object(obj)
}
pub(crate) fn emit_payload_mix_rollup(
mix: &PayloadMix,
local_peer_id: &str,
window_secs: u64,
) -> serde_json::Value {
let window = mix.take_window();
let payload = payload_mix_json(
&window.arms(),
&window.top_contracts(),
window.contract_full_state_bytes.values().sum(),
window.contract_full_state_bytes.len() as u64,
window.attribution_dropped_sends,
window.attribution_dropped_bytes,
window_secs,
);
crate::tracing::telemetry::send_standalone_shadow_event_with_peer_id(
"broadcast_payload_mix",
local_peer_id,
payload.clone(),
);
payload
}
fn rollup_window_secs(elapsed: Duration) -> u64 {
(elapsed.as_secs_f64().round() as u64).max(1)
}
pub(crate) fn spawn_payload_mix_aggregator(
mix: std::sync::Arc<PayloadMix>,
local_peer_id: String,
monitor: &BackgroundTaskMonitor,
) {
let handle = tokio::spawn(async move {
let mut ticker = tokio::time::interval(ROLLUP_WINDOW);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
ticker.tick().await; let mut last_rollup = tokio::time::Instant::now();
loop {
ticker.tick().await;
let now = tokio::time::Instant::now();
let elapsed = now.saturating_duration_since(last_rollup);
last_rollup = now;
emit_payload_mix_rollup(&mix, &local_peer_id, rollup_window_secs(elapsed));
}
});
monitor.register("broadcast_payload_mix_aggregator", handle);
}
#[cfg(test)]
mod tests {
use super::*;
fn contract(byte: u8) -> ContractInstanceId {
ContractInstanceId::new([byte; 32])
}
#[test]
fn take_window_resets_the_window() {
let mix = PayloadMix::new();
mix.record_delivered(PayloadArm::Delta, &contract(1), 100);
let first = mix.take_window().arms();
assert_eq!(first[PayloadArm::Delta.index()].1, 1);
assert_eq!(first[PayloadArm::Delta.index()].2, 100);
let second = mix.take_window().arms();
assert!(
second
.iter()
.all(|(_, sends, bytes)| *sends == 0 && *bytes == 0),
"second take must be empty, got {second:?}"
);
}
#[test]
fn each_arm_counts_separately() {
let mix = PayloadMix::new();
for (i, arm) in PayloadArm::ALL.iter().enumerate() {
for _ in 0..=i {
mix.record_delivered(*arm, &contract(i as u8), 10);
}
}
let drained = mix.take_window().arms();
for (i, (arm, sends, bytes)) in drained.iter().enumerate() {
assert_eq!(*arm, PayloadArm::ALL[i]);
assert_eq!(*sends, i as u64 + 1, "wrong send count for {arm:?}");
assert_eq!(*bytes, (i as u64 + 1) * 10, "wrong byte count for {arm:?}");
}
}
#[test]
fn per_contract_tallies_reconcile_with_arm_totals() {
let mix = PayloadMix::new();
mix.record_delivered(PayloadArm::FullNotEfficient, &contract(1), 500);
mix.record_delivered(PayloadArm::FullNoSummary, &contract(2), 300);
mix.record_delivered(PayloadArm::FullDeltaSuppressed, &contract(1), 200);
mix.record_delivered(PayloadArm::Delta, &contract(3), 50);
let window = mix.take_window();
let full_state_total: u64 = window
.arms()
.iter()
.filter(|(arm, _, _)| arm.is_full_state())
.map(|(_, _, bytes)| bytes)
.sum();
assert_eq!(
full_state_total, 1000,
"full-state arm bytes should exclude the delta send"
);
assert_eq!(window.contract_full_state_bytes[&contract(1)], 700);
assert_eq!(window.contract_full_state_bytes[&contract(2)], 300);
assert_reconciles(&window);
}
fn assert_reconciles(window: &Window) {
let json = payload_mix_json(
&window.arms(),
&window.top_contracts(),
window.contract_full_state_bytes.values().sum(),
window.contract_full_state_bytes.len() as u64,
window.attribution_dropped_sends,
window.attribution_dropped_bytes,
60,
);
let top_sum: u64 = json["top_contracts_by_full_state_bytes"]
.as_array()
.expect("top contracts must be an array")
.iter()
.map(|e| e["bytes"].as_u64().expect("bytes must be a number"))
.sum();
let other = json["other_contracts_bytes"].as_u64().unwrap();
let dropped = json["attribution_dropped_bytes"].as_u64().unwrap();
let full_state = json["full_state_bytes"].as_u64().unwrap();
assert_eq!(
top_sum + other + dropped,
full_state,
"published schema must add up: sum(top_contracts) + \
other_contracts_bytes + attribution_dropped_bytes == \
full_state_bytes (got {top_sum} + {other} + {dropped} != \
{full_state})"
);
}
#[test]
fn window_with_more_contracts_than_top_n_still_reconciles() {
let mix = PayloadMix::new();
for i in 0..(TOP_CONTRACTS_REPORTED + 1) {
mix.record_delivered(PayloadArm::FullNotEfficient, &contract(i as u8), 100);
}
let window = mix.take_window();
assert_reconciles(&window);
let json = payload_mix_json(
&window.arms(),
&window.top_contracts(),
window.contract_full_state_bytes.values().sum(),
window.contract_full_state_bytes.len() as u64,
window.attribution_dropped_sends,
window.attribution_dropped_bytes,
60,
);
assert_eq!(json["full_state_bytes"], 1100);
assert_eq!(
json["other_contracts_bytes"], 100,
"the 11th contract's bytes must be reported as the untruncated \
remainder, not silently lost"
);
assert_eq!(
json["attribution_dropped_bytes"], 0,
"nothing was DROPPED here — the cap was never reached; this is \
truncation, which is a different field"
);
assert_eq!(json["contracts_tracked"], 11);
}
#[test]
fn truncation_and_over_cap_drops_reconcile_together() {
let mix = PayloadMix::new();
for i in 0..(MAX_TRACKED_CONTRACTS + 5) {
let mut raw = [0u8; 32];
raw[0] = (i % 256) as u8;
raw[1] = (i / 256) as u8;
mix.record_delivered(PayloadArm::FullNoSummary, &ContractInstanceId::new(raw), 10);
}
let window = mix.take_window();
assert!(
window.attribution_dropped_bytes > 0,
"cap must have been hit"
);
assert_reconciles(&window);
}
#[test]
fn concurrent_records_racing_a_rollup_conserve_bytes() {
use std::sync::Arc;
const THREADS: usize = 4;
const PER_THREAD: usize = 500;
let mix = Arc::new(PayloadMix::new());
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let drained_total = Arc::new(Mutex::new(0u64));
let drainer = {
let mix = Arc::clone(&mix);
let stop = Arc::clone(&stop);
let drained_total = Arc::clone(&drained_total);
std::thread::spawn(move || {
while !stop.load(std::sync::atomic::Ordering::Relaxed) {
let w = mix.take_window();
let sum: u64 = w.arms().iter().map(|(_, _, b)| b).sum();
*drained_total.lock() += sum;
std::thread::sleep(Duration::from_micros(50));
}
})
};
let writers: Vec<_> = (0..THREADS)
.map(|t| {
let mix = Arc::clone(&mix);
std::thread::spawn(move || {
for _ in 0..PER_THREAD {
mix.record_delivered(PayloadArm::FullNotEfficient, &contract(t as u8), 7);
}
})
})
.collect();
for w in writers {
w.join().unwrap();
}
stop.store(true, std::sync::atomic::Ordering::Relaxed);
drainer.join().unwrap();
let leftover: u64 = mix.take_window().arms().iter().map(|(_, _, b)| b).sum();
let total = *drained_total.lock() + leftover;
assert_eq!(
total,
(THREADS * PER_THREAD * 7) as u64,
"bytes were lost or double-counted across a concurrent rollover"
);
}
#[test]
fn full_state_share_excludes_deltas() {
let arms = vec![
(PayloadArm::Delta, 3, 300),
(PayloadArm::FullDeltaSuppressed, 0, 0),
(PayloadArm::FullNotEfficient, 1, 700),
(PayloadArm::FullComputeFailed, 0, 0),
(PayloadArm::FullNoSummary, 0, 0),
];
let json = payload_mix_json(&arms, &[], 0, 0, 0, 0, 60);
assert_eq!(json["total_bytes"], 1000);
assert_eq!(json["full_state_bytes"], 700);
assert_eq!(json["full_state_byte_share"], 0.7);
assert_eq!(json["delta_sends"], 3);
assert_eq!(json["full_not_efficient_bytes"], 700);
}
#[test]
fn empty_window_reports_zero_share_not_nan() {
let arms: Vec<_> = PayloadArm::ALL.iter().map(|a| (*a, 0, 0)).collect();
let json = payload_mix_json(&arms, &[], 0, 0, 0, 0, 60);
assert_eq!(json["full_state_byte_share"], 0.0);
assert_eq!(json["total_bytes"], 0);
}
#[test]
fn contract_attribution_is_bounded_and_reports_overflow() {
let mix = PayloadMix::new();
for i in 0..(MAX_TRACKED_CONTRACTS + 20) {
let mut raw = [0u8; 32];
raw[0] = (i % 256) as u8;
raw[1] = (i / 256) as u8;
mix.record_delivered(
PayloadArm::FullNotEfficient,
&ContractInstanceId::new(raw),
5,
);
}
let window = mix.take_window();
assert!(
window.contract_full_state_bytes.len() <= MAX_TRACKED_CONTRACTS,
"attribution map exceeded its cap: {}",
window.contract_full_state_bytes.len()
);
assert_eq!(
window.attribution_dropped_sends, 20,
"overflow must be reported, not dropped silently"
);
assert_eq!(
window.attribution_dropped_bytes, 100,
"the bytes behind unattributed sends must be reported too"
);
assert!(window.top_contracts().len() <= TOP_CONTRACTS_REPORTED);
let next = mix.take_window();
assert!(next.contract_full_state_bytes.is_empty());
assert_eq!(next.attribution_dropped_sends, 0);
assert_eq!(next.attribution_dropped_bytes, 0);
}
#[test]
fn repeated_sends_from_one_over_cap_contract_count_as_sends() {
let mix = PayloadMix::new();
for i in 0..MAX_TRACKED_CONTRACTS {
let mut raw = [0u8; 32];
raw[0] = (i % 256) as u8;
raw[1] = (i / 256) as u8;
mix.record_delivered(
PayloadArm::FullNotEfficient,
&ContractInstanceId::new(raw),
1,
);
}
let mut raw = [9u8; 32];
raw[31] = 7;
let over_cap = ContractInstanceId::new(raw);
for _ in 0..1000 {
mix.record_delivered(PayloadArm::FullNotEfficient, &over_cap, 3);
}
let window = mix.take_window();
assert_eq!(window.attribution_dropped_sends, 1000);
assert_eq!(window.attribution_dropped_bytes, 3000);
assert!(!window.contract_full_state_bytes.contains_key(&over_cap));
}
#[test]
fn top_contracts_sort_is_deterministic() {
let mix = PayloadMix::new();
mix.record_delivered(PayloadArm::FullNoSummary, &contract(9), 100);
mix.record_delivered(PayloadArm::FullNoSummary, &contract(2), 100);
mix.record_delivered(PayloadArm::FullNoSummary, &contract(5), 500);
let top = mix.take_window().top_contracts();
assert_eq!(top[0].0, contract(5), "largest first");
assert_eq!(top[1].0, contract(2), "tie broken by contract id");
assert_eq!(top[2].0, contract(9));
}
#[test]
fn delta_sends_are_not_attributed_as_full_state() {
let mix = PayloadMix::new();
mix.record_delivered(PayloadArm::Delta, &contract(7), 1234);
let window = mix.take_window();
assert!(
window.contract_full_state_bytes.is_empty(),
"delta bytes leaked into full-state attribution: {:?}",
window.contract_full_state_bytes
);
assert_eq!(window.arms()[PayloadArm::Delta.index()].2, 1234);
}
#[test]
fn reported_window_tracks_actual_elapsed_not_nominal_cadence() {
assert_eq!(rollup_window_secs(ROLLUP_WINDOW), 60);
assert_eq!(rollup_window_secs(Duration::from_secs(150)), 150);
assert_eq!(
rollup_window_secs(Duration::from_millis(90_400)),
90,
"sub-second remainder rounds to nearest"
);
assert_eq!(rollup_window_secs(Duration::ZERO), 1);
assert_eq!(rollup_window_secs(Duration::from_millis(200)), 1);
}
#[test]
fn every_full_state_arm_attributes_to_its_contract() {
for arm in PayloadArm::ALL.iter().filter(|a| a.is_full_state()) {
let mix = PayloadMix::new();
mix.record_delivered(*arm, &contract(3), 99);
let top = mix.take_window().top_contracts();
assert_eq!(
top,
vec![(contract(3), 99)],
"{arm:?} must attribute its full-state bytes to the contract"
);
}
}
}