metrale_telemetry/
energy.rs1use crate::seqcell::SeqCell;
22use std::sync::atomic::{AtomicU64, Ordering};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum Advance {
27 First,
29 Delta(u64),
31 Wrapped(u64),
33 Reset(u64),
36}
37
38impl Advance {
39 pub fn millijoules(self) -> u64 {
40 match self {
41 Self::First => 0,
42 Self::Delta(d) | Self::Wrapped(d) | Self::Reset(d) => d,
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default)]
49pub struct CounterTracker {
50 prev: Option<u64>,
51}
52
53const QUARTER: u64 = u64::MAX / 4;
57
58impl CounterTracker {
59 pub fn advance(&mut self, raw: u64) -> Advance {
60 let prev = self.prev.replace(raw);
61 match prev {
62 None => Advance::First,
63 Some(p) if raw >= p => Advance::Delta(raw - p),
64 Some(p) if p > u64::MAX - QUARTER && raw < QUARTER => {
65 Advance::Wrapped(raw.wrapping_sub(p))
66 }
67 Some(_) => Advance::Reset(raw),
68 }
69 }
70}
71
72pub fn joules_per_token(delta_mj: u64, tokens: u64) -> Option<f64> {
75 (tokens > 0 && delta_mj > 0).then(|| delta_mj as f64 / 1000.0 / tokens as f64)
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct EnergyPoint {
81 pub at_ns: u64,
82 pub energy_mj: u64,
83 pub tokens: u64,
84}
85
86pub const RING_SLOTS: usize = 8192;
90
91pub struct EnergyRing {
94 slots: Box<[SeqCell<3>]>,
95 written: AtomicU64,
96}
97
98impl std::fmt::Debug for EnergyRing {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("EnergyRing")
101 .field("written", &self.written.load(Ordering::Relaxed))
102 .finish_non_exhaustive()
103 }
104}
105
106impl EnergyRing {
107 pub fn with_capacity(slots: usize) -> Self {
108 assert!(slots >= 2, "a ring needs two points to form a window");
109 Self {
110 slots: (0..slots).map(|_| SeqCell::new()).collect(),
111 written: AtomicU64::new(0),
112 }
113 }
114
115 pub fn push(&self, p: EnergyPoint) {
116 let n = self.written.load(Ordering::Relaxed);
117 let i = (n % self.slots.len() as u64) as usize;
118 self.slots[i].store([p.at_ns, p.energy_mj, p.tokens]);
119 self.written.store(n + 1, Ordering::Release);
120 }
121
122 fn point(&self, logical: u64) -> EnergyPoint {
123 let (_, [at_ns, energy_mj, tokens]) =
124 self.slots[(logical % self.slots.len() as u64) as usize].load();
125 EnergyPoint {
126 at_ns,
127 energy_mj,
128 tokens,
129 }
130 }
131
132 fn live(&self) -> (u64, u64) {
134 let hi = self.written.load(Ordering::Acquire);
135 let lo = hi.saturating_sub(self.slots.len() as u64 - 1);
137 (lo, hi)
138 }
139
140 pub fn back(&self, back: u64) -> Option<EnergyPoint> {
142 let (lo, hi) = self.live();
143 let i = hi.checked_sub(1 + back)?;
144 (i >= lo).then(|| self.point(i))
145 }
146
147 pub fn at_or_before(&self, t_ns: u64) -> Option<EnergyPoint> {
150 let (lo, hi) = self.live();
151 if lo == hi {
152 return None;
153 }
154 let (mut a, mut b) = (lo, hi);
156 while a < b {
157 let mid = a + (b - a) / 2;
158 if self.point(mid).at_ns <= t_ns {
159 a = mid + 1;
160 } else {
161 b = mid;
162 }
163 }
164 Some(self.point(if a == lo { lo } else { a - 1 }))
165 }
166}
167
168pub fn request_millijoules(
172 ring: &EnergyRing,
173 start_ns: u64,
174 end_ns: u64,
175 tokens: u64,
176) -> Option<u64> {
177 let p0 = ring.at_or_before(start_ns)?;
178 let p1 = ring.at_or_before(end_ns)?;
179 let de = p1.energy_mj.checked_sub(p0.energy_mj)?;
180 let dt = p1.tokens.checked_sub(p0.tokens).filter(|t| *t > 0)?;
181 Some((de as u128 * tokens as u128 / dt as u128) as u64)
182}
183
184#[cfg(test)]
185#[path = "energy_tests.rs"]
186mod tests;