Skip to main content

base/
metrics.rs

1// metrics -- measurements of running systems without keeping single values: counters, gauges,
2// log-linear histograms and a sliding window of them (simulators, servers, benchmarks)
3//
4//   Counter        inc / add / get (saturating u64)
5//   Gauge          set / add / get / max (i64; max is the highest value seen)
6//   Hist           log-linear buckets: values below 2^b exactly, above in 2^b linear steps
7//                  per power of two (b = 5: at most 1/32 = 3.1 % bucket width, the midpoint
8//                  is off by half of that); record / record_n / count / sum / min / max /
9//                  mean / quantile(per_mille) / p50 / p95 / p99 / merge / clear / buckets
10//   Window         n slots of `span` time units each (Window::new(300, 1_000_000_000): five
11//                  minutes in 1 s slots of nanoseconds); record(t, v) / quantile(t, pm) /
12//                  p50 / p95 / p99 / count / sum / mean / min / max over the slots still in
13//                  the window at t / per_slot(t, pm) for charts / hist(t)
14//                  hist_between(from, to): the slots that start in [from, to) as one
15//                  histogram (a time series: one point per interval, quantiles of any span);
16//                  bytes(): the heap memory it holds -- its slots come with the first value,
17//                  so a window that never got one costs nothing (a simulator keeps one per
18//                  device and copies them often)
19//
20// Quantiles use the nearest rank (rank = ceil(pm * n / 1000)) and give the midpoint of that
21// rank's bucket, clamped to the smallest and largest value seen (the first and the last rank
22// are those values), so a run of equal values reports that value exactly.  A window slot keeps a sparse histogram (bucket, count) and its
23// own count, sum, min and max; nothing stores the values themselves.  All integer arithmetic:
24// the same values give the same results on every target.
25
26use alloc::vec::Vec;
27
28/// a monotonic count
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub struct Counter(u64);
31
32impl Counter {
33    pub const fn new() -> Counter {
34        Counter(0)
35    }
36    pub fn inc(&mut self) {
37        self.0 = self.0.saturating_add(1);
38    }
39    pub fn add(&mut self, n: u64) {
40        self.0 = self.0.saturating_add(n);
41    }
42    pub fn get(&self) -> u64 {
43        self.0
44    }
45}
46
47/// a value that goes up and down, with the highest value it reached
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub struct Gauge {
50    v: i64,
51    max: i64,
52}
53
54impl Gauge {
55    pub const fn new() -> Gauge {
56        Gauge { v: 0, max: 0 }
57    }
58    pub fn set(&mut self, v: i64) {
59        self.v = v;
60        self.max = self.max.max(v);
61    }
62    pub fn add(&mut self, d: i64) {
63        self.set(self.v.saturating_add(d));
64    }
65    pub fn get(&self) -> i64 {
66        self.v
67    }
68    pub fn max(&self) -> i64 {
69        self.max
70    }
71}
72
73/// the default resolution: 32 linear steps per power of two
74pub const HIST_BITS: u8 = 5;
75
76/// the bucket of `v` with `b` sub-bucket bits
77fn bucket_of(b: u32, v: u64) -> usize {
78    let sub = 1u64 << b;
79    if v < sub {
80        return v as usize;
81    }
82    let e = 63 - v.leading_zeros();
83    let s = (v >> (e - b)) - sub;
84    (((e - b + 1) as usize) << b) | s as usize
85}
86
87/// the smallest value of bucket `i` and its width
88fn bucket_range(b: u32, i: usize) -> (u64, u64) {
89    let sub = 1usize << b;
90    if i < sub {
91        return (i as u64, 1);
92    }
93    let k = (i >> b) as u32;
94    let s = (i & (sub - 1)) as u64;
95    (((1u64 << b) + s) << (k - 1), 1u64 << (k - 1))
96}
97
98/// the value reported for a bucket: its midpoint, within what was seen
99fn estimate(b: u32, i: usize, min: u64, max: u64) -> u64 {
100    let (lo, w) = bucket_range(b, i);
101    (lo + (w - 1) / 2).clamp(min, max)
102}
103
104/// the nearest rank of a quantile in per mille (1-based)
105fn rank(pm: u32, n: u64) -> u64 {
106    let r = (pm.min(1000) as u128 * n as u128).div_ceil(1000) as u64;
107    r.clamp(1, n)
108}
109
110/// a log-linear histogram
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct Hist {
113    bits: u8,
114    counts: Vec<u64>,
115    n: u64,
116    sum: u128,
117    min: u64,
118    max: u64,
119}
120
121impl Default for Hist {
122    fn default() -> Hist {
123        Hist::new()
124    }
125}
126
127impl Hist {
128    pub fn new() -> Hist {
129        Hist::with_bits(HIST_BITS)
130    }
131    /// `bits` linear steps per power of two as a power of two (1..=8)
132    pub fn with_bits(bits: u8) -> Hist {
133        Hist { bits: bits.clamp(1, 8), counts: Vec::new(), n: 0, sum: 0, min: u64::MAX, max: 0 }
134    }
135    pub fn bits(&self) -> u8 {
136        self.bits
137    }
138    /// the bucket a value falls into
139    pub fn bucket(&self, v: u64) -> usize {
140        bucket_of(self.bits as u32, v)
141    }
142    /// the smallest value and the width of a bucket
143    pub fn range(&self, i: usize) -> (u64, u64) {
144        bucket_range(self.bits as u32, i)
145    }
146    pub fn record(&mut self, v: u64) {
147        self.record_n(v, 1);
148    }
149    pub fn record_n(&mut self, v: u64, n: u64) {
150        if n == 0 {
151            return;
152        }
153        let i = self.bucket(v);
154        if self.counts.len() <= i {
155            self.counts.resize(i + 1, 0);
156        }
157        self.counts[i] = self.counts[i].saturating_add(n);
158        self.n = self.n.saturating_add(n);
159        self.sum = self.sum.saturating_add(v as u128 * n as u128);
160        self.min = self.min.min(v);
161        self.max = self.max.max(v);
162    }
163    pub fn count(&self) -> u64 {
164        self.n
165    }
166    pub fn sum(&self) -> u128 {
167        self.sum
168    }
169    pub fn min(&self) -> Option<u64> {
170        if self.n == 0 { None } else { Some(self.min) }
171    }
172    pub fn max(&self) -> Option<u64> {
173        if self.n == 0 { None } else { Some(self.max) }
174    }
175    /// the mean, rounded down
176    pub fn mean(&self) -> Option<u64> {
177        if self.n == 0 { None } else { Some((self.sum / self.n as u128) as u64) }
178    }
179    /// the value at `per_mille` (500: median, 990: p99, 1000: the largest)
180    pub fn quantile(&self, per_mille: u32) -> Option<u64> {
181        if self.n == 0 {
182            return None;
183        }
184        let r = rank(per_mille, self.n);
185        if r == self.n {
186            return Some(self.max);
187        }
188        if r == 1 {
189            return Some(self.min);
190        }
191        let mut cum = 0;
192        for (i, &c) in self.counts.iter().enumerate() {
193            cum += c;
194            if cum >= r {
195                return Some(estimate(self.bits as u32, i, self.min, self.max));
196            }
197        }
198        Some(self.max)
199    }
200    pub fn p50(&self) -> Option<u64> {
201        self.quantile(500)
202    }
203    pub fn p95(&self) -> Option<u64> {
204        self.quantile(950)
205    }
206    pub fn p99(&self) -> Option<u64> {
207        self.quantile(990)
208    }
209    /// adds the values of another histogram of the same resolution (false otherwise)
210    pub fn merge(&mut self, o: &Hist) -> bool {
211        if o.bits != self.bits {
212            return false;
213        }
214        if self.counts.len() < o.counts.len() {
215            self.counts.resize(o.counts.len(), 0);
216        }
217        for (i, &c) in o.counts.iter().enumerate() {
218            self.counts[i] = self.counts[i].saturating_add(c);
219        }
220        self.n = self.n.saturating_add(o.n);
221        self.sum = self.sum.saturating_add(o.sum);
222        self.min = self.min.min(o.min);
223        self.max = self.max.max(o.max);
224        true
225    }
226    pub fn clear(&mut self) {
227        *self = Hist::with_bits(self.bits);
228    }
229    /// the buckets that hold values: (smallest value, width, count)
230    pub fn buckets(&self) -> Vec<(u64, u64, u64)> {
231        self.counts.iter().enumerate().filter(|(_, c)| **c > 0).map(|(i, &c)| {
232            let (lo, w) = self.range(i);
233            (lo, w, c)
234        }).collect()
235    }
236}
237
238/// one slot of a window: a sparse histogram of one span of time
239#[derive(Clone, Debug, PartialEq, Eq)]
240struct Slot {
241    /// the slot number (time / span); u64::MAX: never used
242    idx: u64,
243    n: u64,
244    sum: u128,
245    min: u64,
246    max: u64,
247    /// (bucket, count), ascending
248    cells: Vec<(u16, u32)>,
249}
250
251impl Slot {
252    fn empty() -> Slot {
253        Slot { idx: u64::MAX, n: 0, sum: 0, min: u64::MAX, max: 0, cells: Vec::new() }
254    }
255    fn reset(&mut self, idx: u64) {
256        *self = Slot { idx, cells: core::mem::take(&mut self.cells), ..Slot::empty() };
257        self.cells.clear();
258    }
259    fn add(&mut self, bucket: usize, v: u64, n: u32) {
260        let b = bucket as u16;
261        match self.cells.binary_search_by_key(&b, |c| c.0) {
262            Ok(i) => self.cells[i].1 = self.cells[i].1.saturating_add(n),
263            Err(i) => self.cells.insert(i, (b, n)),
264        }
265        self.n = self.n.saturating_add(n as u64);
266        self.sum = self.sum.saturating_add(v as u128 * n as u128);
267        self.min = self.min.min(v);
268        self.max = self.max.max(v);
269    }
270}
271
272/// adds the values of a slot to a histogram of the same resolution
273fn add_slot(h: &mut Hist, s: &Slot) {
274    for &(b, c) in &s.cells {
275        let i = b as usize;
276        if h.counts.len() <= i {
277            h.counts.resize(i + 1, 0);
278        }
279        h.counts[i] = h.counts[i].saturating_add(c as u64);
280    }
281    h.n = h.n.saturating_add(s.n);
282    h.sum = h.sum.saturating_add(s.sum);
283    h.min = h.min.min(s.min);
284    h.max = h.max.max(s.max);
285}
286
287/// a histogram over the last `slots` spans of time
288#[derive(Clone, Debug, PartialEq, Eq)]
289pub struct Window {
290    span: u64,
291    bits: u8,
292    /// `len` slots, made when the first value comes (empty until then)
293    ring: Vec<Slot>,
294    len: usize,
295    /// the newest slot number recorded into
296    head: u64,
297}
298
299impl Window {
300    /// `slots` slots of `span` time units each (at least one of each)
301    pub fn new(slots: usize, span: u64) -> Window {
302        Window::with_bits(slots, span, HIST_BITS)
303    }
304    pub fn with_bits(slots: usize, span: u64, bits: u8) -> Window {
305        Window { span: span.max(1), bits: bits.clamp(1, 8), ring: Vec::new(), len: slots.max(1), head: 0 }
306    }
307    pub fn slots(&self) -> usize {
308        self.len
309    }
310    /// the heap memory it holds: its slots and their histograms (none before the first value)
311    pub fn bytes(&self) -> usize {
312        self.ring.capacity() * core::mem::size_of::<Slot>() + self.ring.iter().map(|s| s.cells.capacity() * core::mem::size_of::<(u16, u32)>()).sum::<usize>()
313    }
314    pub fn span(&self) -> u64 {
315        self.span
316    }
317    /// counts `v` at time `t`; false when t lies before the window that ends at the newest
318    /// time recorded so far
319    pub fn record(&mut self, t: u64, v: u64) -> bool {
320        self.record_n(t, v, 1)
321    }
322    pub fn record_n(&mut self, t: u64, v: u64, n: u32) -> bool {
323        let i = t / self.span;
324        let len = self.len as u64;
325        if i == u64::MAX {
326            return false; // the slot number of "never used"
327        }
328        if i > self.head {
329            self.head = i;
330        }
331        if i.saturating_add(len) <= self.head || n == 0 {
332            return false;
333        }
334        let bucket = bucket_of(self.bits as u32, v);
335        if self.ring.is_empty() {
336            self.ring = alloc::vec![Slot::empty(); self.len];
337        }
338        let s = &mut self.ring[(i % len) as usize];
339        if s.idx != i {
340            s.reset(i);
341        }
342        s.add(bucket, v, n);
343        true
344    }
345    /// the slots still in the window at time t
346    fn live(&self, t: u64) -> impl Iterator<Item = &Slot> {
347        let now = t / self.span;
348        let len = self.len as u64;
349        self.ring.iter().filter(move |s| s.idx != u64::MAX && s.idx <= now && s.idx.saturating_add(len) > now)
350    }
351    /// everything in the window at time t as one histogram
352    pub fn hist(&self, t: u64) -> Hist {
353        let mut h = Hist::with_bits(self.bits);
354        for s in self.live(t) {
355            add_slot(&mut h, s);
356        }
357        h
358    }
359    /// the slots that start in [from, to) as one histogram: what was recorded between two
360    /// times, to the resolution of the slots; slots the ring no longer holds are left out
361    pub fn hist_between(&self, from: u64, to: u64) -> Hist {
362        let mut h = Hist::with_bits(self.bits);
363        let len = self.len as u64;
364        let (a, b) = (from.div_ceil(self.span), to.div_ceil(self.span));
365        // only the newest `len` slot numbers can still be in the ring
366        for i in a.max(b.saturating_sub(len))..b {
367            if let Some(s) = self.ring.get((i % len) as usize)
368                && s.idx == i
369            {
370                add_slot(&mut h, s);
371            }
372        }
373        h
374    }
375    pub fn count(&self, t: u64) -> u64 {
376        self.live(t).map(|s| s.n).sum()
377    }
378    pub fn sum(&self, t: u64) -> u128 {
379        self.live(t).map(|s| s.sum).sum()
380    }
381    pub fn mean(&self, t: u64) -> Option<u64> {
382        let n = self.count(t);
383        if n == 0 { None } else { Some((self.sum(t) / n as u128) as u64) }
384    }
385    pub fn min(&self, t: u64) -> Option<u64> {
386        self.live(t).filter(|s| s.n > 0).map(|s| s.min).min()
387    }
388    pub fn max(&self, t: u64) -> Option<u64> {
389        self.live(t).filter(|s| s.n > 0).map(|s| s.max).max()
390    }
391    pub fn quantile(&self, t: u64, per_mille: u32) -> Option<u64> {
392        self.hist(t).quantile(per_mille)
393    }
394    pub fn p50(&self, t: u64) -> Option<u64> {
395        self.quantile(t, 500)
396    }
397    pub fn p95(&self, t: u64) -> Option<u64> {
398        self.quantile(t, 950)
399    }
400    pub fn p99(&self, t: u64) -> Option<u64> {
401        self.quantile(t, 990)
402    }
403    /// one value per slot of the window at time t, oldest first: (start time of the slot,
404    /// count, quantile or None when the slot is empty)
405    pub fn per_slot(&self, t: u64, per_mille: u32) -> Vec<(u64, u64, Option<u64>)> {
406        let now = t / self.span;
407        let len = self.len as u64;
408        let first = now.saturating_sub(len - 1);
409        (first..=now)
410            .map(|i| {
411                let Some(s) = self.ring.get((i % len) as usize).filter(|s| s.idx == i && s.n > 0) else {
412                    return (i * self.span, 0, None);
413                };
414                let r = rank(per_mille, s.n);
415                let mut cum = 0u64;
416                let mut v = if r == 1 { s.min } else { s.max };
417                for &(b, c) in s.cells.iter().filter(|_| r > 1 && r < s.n) {
418                    cum += c as u64;
419                    if cum >= r {
420                        v = estimate(self.bits as u32, b as usize, s.min, s.max);
421                        break;
422                    }
423                }
424                (i * self.span, s.n, Some(v))
425            })
426            .collect()
427    }
428}