use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::usage::{UsageInfo, UsageWindow};
fn window_util(usage: &UsageInfo, label: &str) -> Option<f64> {
usage
.windows()
.into_iter()
.find(|(l, _)| *l == label)
.map(|(_, w)| w.utilization)
}
fn reset_boundary(entries: &[(u64, f64)]) -> usize {
for i in (1..entries.len()).rev() {
if entries[i].1 < entries[i - 1].1 {
return i;
}
}
0
}
fn gap_boundary(entries: &[(u64, f64)], max_gap_ms: u64) -> usize {
for i in (1..entries.len()).rev() {
if entries[i].1 == entries[i - 1].1
&& entries[i].0.saturating_sub(entries[i - 1].0) > max_gap_ms
{
return i;
}
}
0
}
pub(crate) fn compute_burn_rates_from_history(
history: &[(u64, UsageInfo)],
windows: &[(&str, &UsageWindow)],
lookback_ms: u64,
min_samples: usize,
gap_cut_ms: u64,
) -> HashMap<String, Option<f64>> {
let now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
let half_life_ms = (lookback_ms as f64 / 4.0).max(1.0);
let mut rates = HashMap::new();
for (label, window) in windows {
let mut entries: Vec<(u64, f64)> = history
.iter()
.filter_map(|(ts, u)| window_util(u, label).map(|p| (*ts, p)))
.collect();
entries.push((now_ms, window.utilization));
if gap_cut_ms > 0 && entries.len() >= 2 {
let gap = gap_boundary(&entries, gap_cut_ms);
if gap > 0 {
entries = entries[gap..].to_vec();
}
}
if entries.len() >= 2 {
entries.reverse();
entries.dedup_by(|a, b| a.1 == b.1);
entries.reverse();
}
let cutoff = now_ms.saturating_sub(lookback_ms);
let cap = entries
.iter()
.position(|&(ts, _)| ts >= cutoff)
.unwrap_or(0);
let start = reset_boundary(&entries).max(cap);
let recent = &entries[start..];
let rate = if recent.len() >= min_samples {
weighted_rate_per_hour(recent, half_life_ms)
} else {
None
};
rates.insert(label.to_string(), rate);
}
rates
}
fn weighted_rate_per_hour(entries: &[(u64, f64)], half_life_ms: f64) -> Option<f64> {
let last_ts = entries[entries.len() - 1].0;
let base = entries[0].0 as f64;
let (mut sw, mut swx, mut swy, mut swxx, mut swxy) = (0.0, 0.0, 0.0, 0.0, 0.0);
for &(ts, util) in entries {
let age = last_ts.saturating_sub(ts) as f64;
let w = 0.5_f64.powf(age / half_life_ms);
let x = ts as f64 - base;
sw += w;
swx += w * x;
swy += w * util;
swxx += w * x * x;
swxy += w * x * util;
}
let denom = sw * swxx - swx * swx;
if denom.abs() < f64::EPSILON {
return None;
}
let slope_per_ms = (sw * swxy - swx * swy) / denom;
Some(slope_per_ms * 3_600_000.0)
}
#[cfg(test)]
#[path = "../../tests/inline/burn.rs"]
mod tests;