use std::collections::BTreeMap;
use std::sync::RwLock;
use std::time::Duration;
use dashmap::DashMap;
use tokio::time::Instant;
use freenet_stdlib::prelude::*;
use crate::ring::PeerKeyLocation;
use crate::topology::rate::Rate;
use super::running_average::RunningAverage;
const DEFAULT_USAGE_PERCENTILE: f64 = 0.5;
const ESTIMATED_USAGE_RATE_CACHE_TIME: Duration = Duration::from_secs(60);
pub(crate) const MAX_ATTRIBUTION_SOURCES: usize = 4096;
pub(crate) const ATTRIBUTION_SOURCE_TTL: Duration = Duration::from_secs(15 * 60);
const EXEC_CPU_COST_FLOOR_MICROS_PER_SEC: f64 = 50_000.0;
const BROADCAST_FANOUT_COST_FLOOR_BYTES_PER_SEC: f64 = 128.0 * 1024.0;
const BROADCAST_MESSAGES_COST_FLOOR_PER_SEC: f64 = 10.0;
pub(crate) const COST_SUSTAINED_WINDOW: Duration = Duration::from_secs(300);
fn new_running_average(window_size: usize, resource: ResourceType) -> RunningAverage {
match resource.cost_pressure_floor() {
Some(floor) => {
RunningAverage::new_cost_sustained(window_size, floor, COST_SUSTAINED_WINDOW)
}
None => RunningAverage::new(window_size),
}
}
pub(crate) struct Meter {
attribution_meters: AttributionMeters,
running_average_window_size: usize,
cached_estimated_usage_rate: RwLock<BTreeMap<ResourceType, (Rate, Instant)>>,
}
impl Meter {
pub fn new_with_window_size(running_average_window_size: usize) -> Self {
Meter {
attribution_meters: DashMap::new(),
running_average_window_size,
cached_estimated_usage_rate: RwLock::new(BTreeMap::new()),
}
}
pub(crate) fn attributed_usage_rate(
&self,
attribution: &AttributionSource,
resource: &ResourceType,
at_time: Instant,
) -> Option<Rate> {
match self.attribution_meters.get(attribution) {
Some(attribution_meters) => {
match attribution_meters.map.get(resource) {
Some(meter) => {
meter.get_rate_at_time(at_time)
}
None => Some(Rate::new(0.0, Duration::from_secs(1))), }
}
None => None, }
}
pub(crate) fn get_adjusted_usage_rate(
&mut self,
resource: &ResourceType,
at_time: Instant,
) -> Option<Rate> {
{
let cache = self.cached_estimated_usage_rate.read().unwrap();
if let Some((cached_rate, cached_time)) = cache.get(resource) {
if at_time - *cached_time <= ESTIMATED_USAGE_RATE_CACHE_TIME {
return Some(*cached_rate);
}
}
}
match self.calculate_estimated_usage_rate(resource, at_time) {
Some(estimated_usage_rate) => {
let mut cache = self.cached_estimated_usage_rate.write().unwrap();
cache.insert(*resource, (estimated_usage_rate, at_time));
Some(estimated_usage_rate)
}
None => None,
}
}
pub(crate) fn get_usage_rates(
&self,
resource: &ResourceType,
at_time: Instant,
) -> BTreeMap<AttributionSource, Rate> {
let mut rates = BTreeMap::new();
for entry in self.attribution_meters.iter() {
if let Some(meter) = entry.value().map.get(resource) {
if let Some(rate) = meter.get_rate_at_time(at_time) {
rates.insert(entry.key().clone(), rate);
}
}
}
rates
}
#[cfg(test)]
pub(crate) fn contract_cost_rates(
&self,
resource: &ResourceType,
at_time: Instant,
min_window: Duration,
) -> (f64, std::collections::HashMap<ContractInstanceId, f64>) {
let mut out =
self.contract_cost_rates_multi(std::slice::from_ref(resource), at_time, min_window);
out.pop()
.expect("exactly one result for one requested axis")
}
pub(crate) fn contract_cost_rates_multi(
&self,
resources: &[ResourceType],
at_time: Instant,
min_window: Duration,
) -> Vec<(f64, std::collections::HashMap<ContractInstanceId, f64>)> {
let sustained_min_span = min_window / 2;
let mut results: Vec<(f64, std::collections::HashMap<ContractInstanceId, f64>)> = resources
.iter()
.map(|_| (0.0_f64, std::collections::HashMap::new()))
.collect();
if resources.is_empty() {
return results;
}
for entry in self.attribution_meters.iter() {
let AttributionSource::Contract(id) = entry.key() else {
continue;
};
let source_map = &entry.value().map;
for (i, resource) in resources.iter().enumerate() {
let Some(avg) = source_map.get(resource) else {
continue;
};
let Some(windowed) = avg.windowed_rate(at_time, min_window) else {
continue;
};
let per_second = windowed.rate.per_second();
if per_second > 0.0 {
results[i].0 += per_second;
if windowed.activity_span >= sustained_min_span {
results[i].1.insert(*id, per_second);
}
}
}
}
results
}
fn calculate_estimated_usage_rate(
&self,
resource: &ResourceType,
at_time: Instant,
) -> Option<Rate> {
let rates: Vec<Rate> = self
.attribution_meters
.iter()
.filter_map(|t| {
t.value()
.map
.get(resource)
.and_then(|m| m.get_rate_at_time(at_time))
})
.collect();
if rates.is_empty() {
return None;
}
let mut sorted_rates = rates;
sorted_rates.sort_unstable();
let percentile_index =
(DEFAULT_USAGE_PERCENTILE * sorted_rates.len() as f64).round() as usize;
let estimated_index = percentile_index.min(sorted_rates.len().saturating_sub(1));
sorted_rates.get(estimated_index).cloned()
}
pub(crate) fn retain_peer_sources(&self, live: &std::collections::HashSet<PeerKeyLocation>) {
self.attribution_meters.retain(|source, _| match source {
AttributionSource::Peer(peer) => live.contains(peer),
AttributionSource::Delegate(_) | AttributionSource::Contract(_) => true,
});
}
pub(crate) fn report(
&self,
attribution: &AttributionSource,
resource: ResourceType,
value: f64,
at_time: Instant,
) {
use dashmap::mapref::entry::Entry;
let window_size = self.running_average_window_size;
match self.attribution_meters.entry(attribution.clone()) {
Entry::Occupied(mut occupied) => {
let totals = occupied.get_mut();
totals.last_reported = totals.last_reported.max(at_time);
totals
.map
.entry(resource)
.or_insert_with(|| new_running_average(window_size, resource))
.insert_with_time(at_time, value);
return;
}
Entry::Vacant(_) => {}
}
self.evict_if_full(at_time);
let mut totals = self
.attribution_meters
.entry(attribution.clone())
.or_insert_with(|| ResourceTotals::new(at_time));
totals.last_reported = totals.last_reported.max(at_time);
totals
.map
.entry(resource)
.or_insert_with(|| new_running_average(window_size, resource))
.insert_with_time(at_time, value);
}
fn evict_if_full(&self, now: Instant) {
self.attribution_meters.retain(|_, totals| {
now.saturating_duration_since(totals.last_reported) < ATTRIBUTION_SOURCE_TTL
});
if self.attribution_meters.len() < MAX_ATTRIBUTION_SOURCES {
return;
}
let oldest = self
.attribution_meters
.iter()
.min_by_key(|entry| entry.value().last_reported)
.map(|entry| entry.key().clone());
if let Some(key) = oldest {
self.attribution_meters.remove(&key);
}
}
}
#[allow(dead_code)] #[derive(Eq, Hash, PartialEq, Clone, Debug)]
pub(crate) enum AttributionSource {
Peer(PeerKeyLocation),
Delegate(DelegateKey),
Contract(ContractInstanceId),
}
impl PartialOrd for AttributionSource {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl AttributionSource {
pub(crate) fn contributes_to(&self, resource: &ResourceType) -> bool {
use AttributionSource::*;
use ResourceType::*;
match (self, resource) {
(Peer(_), InboundBandwidthBytes) => true,
(Peer(_), OutboundBandwidthBytes) => true,
(Peer(_), ExecCpuMicros) => false,
(Peer(_), ExecFuelUnits) => false,
(Peer(_), StateBytesWritten) => false,
(Peer(_), BroadcastFanoutCost) => false,
(Peer(_), BroadcastMessagesSent) => false,
(Delegate(_), InboundBandwidthBytes) => true,
(Delegate(_), OutboundBandwidthBytes) => true,
(Delegate(_), ExecCpuMicros) => false,
(Delegate(_), ExecFuelUnits) => false,
(Delegate(_), StateBytesWritten) => false,
(Delegate(_), BroadcastFanoutCost) => false,
(Delegate(_), BroadcastMessagesSent) => false,
(Contract(_), InboundBandwidthBytes) => false,
(Contract(_), OutboundBandwidthBytes) => false,
(Contract(_), ExecCpuMicros) => true,
(Contract(_), ExecFuelUnits) => true,
(Contract(_), StateBytesWritten) => true,
(Contract(_), BroadcastFanoutCost) => true,
(Contract(_), BroadcastMessagesSent) => true,
}
}
}
impl Ord for AttributionSource {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
fn rank(source: &AttributionSource) -> u8 {
match source {
AttributionSource::Peer(_) => 0,
AttributionSource::Delegate(_) => 1,
AttributionSource::Contract(_) => 2,
}
}
match (self, other) {
(AttributionSource::Peer(a), AttributionSource::Peer(b)) => a.cmp(b),
(AttributionSource::Delegate(a), AttributionSource::Delegate(b)) => {
format!("{:?}", a).cmp(&format!("{:?}", b))
}
(AttributionSource::Contract(a), AttributionSource::Contract(b)) => a.cmp(b),
(a, b) => rank(a).cmp(&rank(b)),
}
}
}
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Copy, Debug)]
pub(crate) enum ResourceType {
InboundBandwidthBytes,
OutboundBandwidthBytes,
ExecCpuMicros,
ExecFuelUnits,
StateBytesWritten,
BroadcastFanoutCost,
BroadcastMessagesSent,
}
impl ResourceType {
pub(crate) fn all() -> [ResourceType; 2] {
[
ResourceType::InboundBandwidthBytes,
ResourceType::OutboundBandwidthBytes,
]
}
pub(crate) fn cost_pressure_floor(&self) -> Option<f64> {
match self {
ResourceType::ExecCpuMicros => Some(EXEC_CPU_COST_FLOOR_MICROS_PER_SEC),
ResourceType::BroadcastFanoutCost => Some(BROADCAST_FANOUT_COST_FLOOR_BYTES_PER_SEC),
ResourceType::BroadcastMessagesSent => Some(BROADCAST_MESSAGES_COST_FLOOR_PER_SEC),
ResourceType::InboundBandwidthBytes
| ResourceType::OutboundBandwidthBytes
| ResourceType::ExecFuelUnits
| ResourceType::StateBytesWritten => None,
}
}
#[allow(dead_code)] pub(crate) fn all_tracked() -> [ResourceType; 7] {
[
ResourceType::InboundBandwidthBytes,
ResourceType::OutboundBandwidthBytes,
ResourceType::ExecCpuMicros,
ResourceType::ExecFuelUnits,
ResourceType::StateBytesWritten,
ResourceType::BroadcastFanoutCost,
ResourceType::BroadcastMessagesSent,
]
}
}
type AttributionMeters = DashMap<AttributionSource, ResourceTotals>;
struct ResourceTotals {
pub map: BTreeMap<ResourceType, RunningAverage>,
last_reported: Instant,
}
impl ResourceTotals {
fn new(at_time: Instant) -> Self {
ResourceTotals {
map: BTreeMap::new(),
last_reported: at_time,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_meter() {
let meter = Meter::new_with_window_size(100);
assert!(
meter
.attributed_usage_rate(
&AttributionSource::Peer(PeerKeyLocation::random()),
&ResourceType::InboundBandwidthBytes,
Instant::now(),
)
.is_none()
);
assert!(meter.attribution_meters.is_empty());
}
fn contract_source(byte: u8) -> AttributionSource {
AttributionSource::Contract(ContractInstanceId::new([byte; 32]))
}
#[test]
fn test_meter_attributed_usage() {
let meter = Meter::new_with_window_size(100);
let attribution = AttributionSource::Peer(PeerKeyLocation::random());
assert!(
meter
.attributed_usage_rate(
&attribution,
&ResourceType::InboundBandwidthBytes,
Instant::now()
)
.is_none()
);
assert!(
meter
.attributed_usage_rate(
&attribution,
&ResourceType::OutboundBandwidthBytes,
Instant::now()
)
.is_none()
);
meter.report(
&attribution,
ResourceType::InboundBandwidthBytes,
100.0,
Instant::now(),
);
assert_eq!(
meter
.attributed_usage_rate(
&attribution,
&ResourceType::InboundBandwidthBytes,
Instant::now()
)
.unwrap()
.per_second(),
100.0
);
}
#[test]
fn test_meter_report() -> anyhow::Result<()> {
let meter = Meter::new_with_window_size(100);
let attribution = AttributionSource::Peer(PeerKeyLocation::random());
meter.report(
&attribution,
ResourceType::InboundBandwidthBytes,
100.0,
Instant::now(),
);
assert_eq!(
meter
.attributed_usage_rate(
&attribution,
&ResourceType::InboundBandwidthBytes,
Instant::now()
)
.unwrap()
.per_second(),
100.0
);
meter.report(
&attribution,
ResourceType::InboundBandwidthBytes,
200.0,
Instant::now(),
);
assert_eq!(
meter
.attributed_usage_rate(
&attribution,
&ResourceType::InboundBandwidthBytes,
Instant::now()
)
.unwrap()
.per_second(),
300.0
);
let other_attribution = AttributionSource::Peer(PeerKeyLocation::random());
meter.report(
&other_attribution,
ResourceType::InboundBandwidthBytes,
150.0,
Instant::now(),
);
assert_eq!(
meter
.attributed_usage_rate(
&other_attribution,
&ResourceType::InboundBandwidthBytes,
Instant::now()
)
.unwrap()
.per_second(),
150.0
);
Ok(())
}
#[test]
fn contract_cost_rates_aggregates_contract_sources_with_min_window() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let now = t0 + Duration::from_secs(600);
meter.report(
&contract_source(1),
ResourceType::ExecCpuMicros,
30_000.0,
t0,
);
meter.report(
&contract_source(1),
ResourceType::ExecCpuMicros,
30_000.0,
t0 + Duration::from_secs(590),
);
meter.report(
&contract_source(2),
ResourceType::ExecCpuMicros,
30_000_000.0,
now - Duration::from_secs(1),
);
meter.report(
&AttributionSource::Peer(PeerKeyLocation::random()),
ResourceType::InboundBandwidthBytes,
999_999.0,
t0,
);
for i in 0..150u64 {
meter.report(
&contract_source(3),
ResourceType::BroadcastMessagesSent,
58.0,
t0 + Duration::from_millis(1600 * i),
);
}
let msgs_now = t0 + Duration::from_millis(1600 * 149) + Duration::from_secs(1);
let (cpu_total, cpu_rates) =
meter.contract_cost_rates(&ResourceType::ExecCpuMicros, now, min_window);
let id1 = ContractInstanceId::new([1u8; 32]);
let id2 = ContractInstanceId::new([2u8; 32]);
assert!(
!cpu_rates.contains_key(&id1),
"a lone within-window sample must not be a candidate"
);
assert!(
!cpu_rates.contains_key(&id2),
"burst must not be a candidate"
);
let expected_total = 30_000.0 / 300.0 + 30_000_000.0 / 300.0;
assert!((cpu_total - expected_total).abs() < 1e-6);
let (msgs_total, msgs_rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, msgs_now, min_window);
let id3 = ContractInstanceId::new([3u8; 32]);
let rate3 = msgs_rates[&id3];
assert!(
rate3 > 30.0,
"saturated buffer must read the true storm rate (~36.6/s), got {rate3}/s"
);
assert!((msgs_total - rate3).abs() < 1e-9);
}
#[test]
fn contract_cost_rates_old_first_sample_then_burst_is_not_a_candidate() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
t0,
);
let burst_start = t0 + Duration::from_secs(540);
for i in 0..120u64 {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
burst_start + Duration::from_millis(500 * i),
);
}
let now = t0 + Duration::from_secs(601);
let (total, rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, now, min_window);
let id1 = ContractInstanceId::new([1u8; 32]);
assert!(
!rates.contains_key(&id1),
"an old-first-sample source's short burst must NOT be a candidate \
(the gap-reset restarts the run at the burst, activity_span ~50s \
< min_window/2)"
);
assert!(
total > 0.0,
"the burst still counts toward the node total (share denominator)"
);
}
#[test]
fn contract_cost_rates_quiet_contract_decays_out_within_min_window() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let id1 = ContractInstanceId::new([1u8; 32]);
for i in 0..150u64 {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
t0 + Duration::from_millis(1600 * i),
);
}
let storm_end = t0 + Duration::from_millis(1600 * 149);
let (live_total, live_rates) = meter.contract_cost_rates(
&ResourceType::BroadcastMessagesSent,
storm_end + Duration::from_secs(1),
min_window,
);
assert!(live_rates[&id1] > 30.0);
assert!(live_total > 30.0);
let (_, stale_rates) = meter.contract_cost_rates(
&ResourceType::BroadcastMessagesSent,
storm_end + Duration::from_secs(180),
min_window,
);
assert!(
!stale_rates.contains_key(&id1),
"a quiet contract must drop out of candidacy once its activity run \
goes stale"
);
let (gone_total, gone_rates) = meter.contract_cost_rates(
&ResourceType::BroadcastMessagesSent,
storm_end + min_window + Duration::from_secs(1),
min_window,
);
assert!(gone_rates.is_empty());
assert_eq!(
gone_total, 0.0,
"a source quiet for min_window must stop inflating total_rate"
);
}
#[test]
fn contract_cost_rates_fast_storm_cadences_nominate() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let run = Duration::from_secs(220);
let mut t = Duration::ZERO;
while t <= run {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
t0 + t,
);
t += Duration::from_millis(1400);
}
let mut t = Duration::ZERO;
while t <= run {
meter.report(
&contract_source(2),
ResourceType::BroadcastMessagesSent,
58.0,
t0 + t,
);
t += Duration::from_millis(500);
}
let now = t0 + run + Duration::from_secs(1);
let (_total, rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, now, min_window);
let id1 = ContractInstanceId::new([1u8; 32]);
let id2 = ContractInstanceId::new([2u8; 32]);
assert!(
rates.contains_key(&id1),
"a 1.4s-cadence storm must be a candidate"
);
assert!(
rates.contains_key(&id2),
"a 0.5s-cadence storm (buffer span ~50s ≪ min_window/2) must STILL \
be a candidate — the BLOCKER inversion is fixed"
);
}
#[test]
fn contract_cost_rates_heal_interleaved_storm_nominates() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let mut t = Duration::ZERO;
let mut n = 0u64;
while t <= Duration::from_secs(220) {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
t0 + t,
);
if n % 5 == 0 {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
1.0,
t0 + t + Duration::from_millis(700),
);
}
n += 1;
t += Duration::from_millis(1600);
}
let now = t0 + Duration::from_secs(221);
let (_total, rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, now, min_window);
assert!(
rates.contains_key(&ContractInstanceId::new([1u8; 32])),
"a heal-interleaved (irregular-cadence) storm must be a candidate"
);
}
#[test]
fn contract_cost_rates_multi_target_cpu_flood_nominates() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let mut dispatch = Duration::ZERO;
for _ in 0..120u64 {
for target in 0..58u64 {
meter.report(
&contract_source(1),
ResourceType::ExecCpuMicros,
2000.0,
t0 + dispatch + Duration::from_millis(15 * target),
);
}
dispatch += Duration::from_millis(1600);
}
let now = t0 + dispatch + Duration::from_secs(1);
let (total, rates) =
meter.contract_cost_rates(&ResourceType::ExecCpuMicros, now, min_window);
assert!(
rates.contains_key(&ContractInstanceId::new([1u8; 32])),
"a multi-target (58/dispatch) above-floor CPU flood must nominate \
on the ExecCpuMicros axis"
);
assert!(total > 0.0);
}
#[test]
fn contract_cost_rates_single_dense_burst_never_nominates() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
for i in 0..200u64 {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
t0 + Duration::from_millis(150 * i),
);
}
let now = t0 + Duration::from_secs(31);
let (total, rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, now, min_window);
assert!(
!rates.contains_key(&ContractInstanceId::new([1u8; 32])),
"a 30s dense burst must not be sustained (run < min_window/2)"
);
assert!(total > 0.0, "the burst still counts toward the total");
}
#[test]
fn contract_cost_rates_slow_cadence_does_not_gap_reset() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let mut t = Duration::ZERO;
while t <= Duration::from_secs(240) {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
400.0,
t0 + t,
);
t += Duration::from_secs(30);
}
let now = t0 + Duration::from_secs(241);
let (_total, rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, now, min_window);
assert!(
rates.contains_key(&ContractInstanceId::new([1u8; 32])),
"a continuous 30s-cadence reporter must stay one sustained run — the \
60s gap threshold must not spuriously reset a sub-60s cadence"
);
}
#[test]
fn contract_cost_rates_intermittent_bursts_do_not_accumulate_span() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let cycle = Duration::from_secs(93);
for c in 0..3u64 {
let start = cycle * c as u32;
for i in 0..7u64 {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
t0 + start + Duration::from_millis(500 * i),
);
}
}
let now = t0 + cycle * 2 + Duration::from_secs(4);
let (total, rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, now, min_window);
assert!(
!rates.contains_key(&ContractInstanceId::new([1u8; 32])),
"intermittent bursts separated by >60s silences must never become \
sustained — span must not accumulate across the gaps"
);
assert!(
total > 0.0,
"the recent burst still counts toward the total"
);
}
#[test]
fn contract_cost_rates_trickle_then_single_burst_is_not_a_candidate() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let id1 = ContractInstanceId::new([1u8; 32]);
let mut t = Duration::ZERO;
while t <= Duration::from_secs(180) {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
0.1,
t0 + t,
);
t += Duration::from_secs(30);
}
let burst_start = t0 + Duration::from_secs(181);
for i in 0..120u64 {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
burst_start + Duration::from_millis(25 * i),
);
}
let now = burst_start + Duration::from_secs(4);
let (total, rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, now, min_window);
assert!(
!rates.contains_key(&id1),
"a continuous below-floor trickle then a SINGLE dense burst must \
NOT be a candidate: sample continuity is sustained but above-FLOOR \
cost is not (Codex round-3)"
);
assert!(
total > 0.0,
"the burst still counts toward the node total (share denominator)"
);
}
#[test]
fn contract_cost_rates_repeated_dense_bursts_above_floor_is_a_candidate() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let min_window = Duration::from_secs(300);
let id1 = ContractInstanceId::new([1u8; 32]);
let mut i = 0u64;
while Duration::from_millis(25 * i) <= Duration::from_secs(200) {
meter.report(
&contract_source(1),
ResourceType::BroadcastMessagesSent,
58.0,
t0 + Duration::from_millis(25 * i),
);
i += 1;
}
let now = t0 + Duration::from_millis(25 * (i - 1)) + Duration::from_secs(1);
let (_total, rates) =
meter.contract_cost_rates(&ResourceType::BroadcastMessagesSent, now, min_window);
assert!(
rates.contains_key(&id1),
"a dense burst sustained ABOVE the floor for > min_window/2 IS a \
candidate (sustained above-floor cost)"
);
}
#[test]
fn test_eviction_skipped_below_cap() {
let meter = Meter::new_with_window_size(100);
let now = Instant::now();
for i in 0..8u8 {
meter.report(
&contract_source(i),
ResourceType::StateBytesWritten,
1.0,
now,
);
}
assert_eq!(meter.attribution_meters.len(), 8);
for i in 0..8u8 {
assert!(
meter
.attributed_usage_rate(
&contract_source(i),
&ResourceType::StateBytesWritten,
now
)
.is_some()
);
}
}
#[test]
fn test_ttl_evicts_stale_source_on_insert() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let stale = contract_source(1);
meter.report(&stale, ResourceType::StateBytesWritten, 1.0, t0);
assert_eq!(meter.attribution_meters.len(), 1);
let later = t0 + ATTRIBUTION_SOURCE_TTL + Duration::from_secs(1);
let fresh = contract_source(2);
meter.report(&fresh, ResourceType::StateBytesWritten, 1.0, later);
assert!(!meter.attribution_meters.contains_key(&stale));
assert!(meter.attribution_meters.contains_key(&fresh));
assert_eq!(meter.attribution_meters.len(), 1);
}
#[test]
fn test_ttl_refreshed_by_repeated_reports() {
let meter = Meter::new_with_window_size(100);
let t0 = Instant::now();
let kept = contract_source(1);
meter.report(&kept, ResourceType::StateBytesWritten, 1.0, t0);
let refresh = t0 + ATTRIBUTION_SOURCE_TTL - Duration::from_secs(1);
meter.report(&kept, ResourceType::StateBytesWritten, 1.0, refresh);
let later = refresh + Duration::from_secs(2);
meter.report(
&contract_source(2),
ResourceType::StateBytesWritten,
1.0,
later,
);
assert!(meter.attribution_meters.contains_key(&kept));
}
#[test]
fn test_cap_enforced_via_lru_eviction() {
let meter = Meter::new_with_window_size(100);
let base = Instant::now();
for i in 0..MAX_ATTRIBUTION_SOURCES {
let src = AttributionSource::Contract(ContractInstanceId::new(id_bytes(i as u32)));
let at = base + Duration::from_millis(i as u64);
meter.report(&src, ResourceType::StateBytesWritten, 1.0, at);
}
assert_eq!(meter.attribution_meters.len(), MAX_ATTRIBUTION_SOURCES);
let oldest = AttributionSource::Contract(ContractInstanceId::new(id_bytes(0)));
let newcomer = AttributionSource::Contract(ContractInstanceId::new(id_bytes(
MAX_ATTRIBUTION_SOURCES as u32,
)));
let at = base + Duration::from_millis(MAX_ATTRIBUTION_SOURCES as u64);
meter.report(&newcomer, ResourceType::StateBytesWritten, 1.0, at);
assert_eq!(meter.attribution_meters.len(), MAX_ATTRIBUTION_SOURCES);
assert!(!meter.attribution_meters.contains_key(&oldest));
assert!(meter.attribution_meters.contains_key(&newcomer));
}
#[test]
fn test_combined_phase_ttl_prune_avoids_lru() {
let meter = Meter::new_with_window_size(100);
let base = Instant::now();
for i in 0..MAX_ATTRIBUTION_SOURCES {
let src = AttributionSource::Contract(ContractInstanceId::new(id_bytes(i as u32)));
meter.report(&src, ResourceType::StateBytesWritten, 1.0, base);
}
assert_eq!(meter.attribution_meters.len(), MAX_ATTRIBUTION_SOURCES);
let later = base + ATTRIBUTION_SOURCE_TTL + Duration::from_secs(1);
let newcomer = AttributionSource::Contract(ContractInstanceId::new(id_bytes(
MAX_ATTRIBUTION_SOURCES as u32,
)));
meter.report(&newcomer, ResourceType::StateBytesWritten, 1.0, later);
assert_eq!(
meter.attribution_meters.len(),
1,
"TTL prune should have dropped all stale entries before LRU ran"
);
assert!(meter.attribution_meters.contains_key(&newcomer));
}
fn id_bytes(i: u32) -> [u8; 32] {
let mut bytes = [0u8; 32];
bytes[0..4].copy_from_slice(&i.to_le_bytes());
bytes
}
}