Skip to content
71 linesCodeBlameRaw
1/**
2 * Durations kept as counts in fixed buckets, so a minute's latency (or
3 * CPU) is a short list of numbers that adds to any other minute's, and
4 * percentiles over any period come from the sum. Each bucket is read back
5 * as a point between its bounds, which keeps a percentile within the
6 * bucket's width of the true one: a few percent here, as bounds go up in
7 * steps of about 1.5×.
8 */
9import type { Percentiles } from "@g1t/contracts/telemetry";
10
11/** Upper bounds, in milliseconds; the last bucket holds everything above the last bound. */
12export const BOUNDS_MS = [
13 1, 2, 3, 5, 7, 10, 15, 20, 30, 50, 75, 100, 150, 200, 300, 500, 750, 1000, 1500, 2000, 3000, 5000, 7500, 10000, 15000, 30000, 60000,
14];
15
16export const BUCKETS = BOUNDS_MS.length + 1;
17
18export function bucketOf(ms: number): number {
19 let i = 0;
20 while (i < BOUNDS_MS.length && ms > BOUNDS_MS[i]) i++;
21 return i;
22}
23
24export function emptyHistogram(): number[] {
25 return new Array(BUCKETS).fill(0);
26}
27
28export function addInto(into: number[], from: readonly number[]): number[] {
29 for (let i = 0; i < BUCKETS; i++) into[i] = (into[i] ?? 0) + (from[i] ?? 0);
30 return into;
31}
32
33/** A histogram as stored: counts joined by commas, trailing zeros dropped. */
34export function encode(counts: readonly number[]): string {
35 let end = counts.length;
36 while (end > 0 && !counts[end - 1]) end--;
37 return counts.slice(0, end).join(",");
38}
39
40export function decode(text: string | null | undefined): number[] {
41 const out = emptyHistogram();
42 if (!text) return out;
43 text.split(",").forEach((n, i) => {
44 if (i < BUCKETS) out[i] = Number(n) || 0;
45 });
46 return out;
47}
48
49/** The value at quantile `q` (0–1), interpolated within its bucket. */
50export function quantile(counts: readonly number[], q: number): number | null {
51 const total = counts.reduce((a, b) => a + b, 0);
52 if (!total) return null;
53 const rank = q * total;
54 let seen = 0;
55 for (let i = 0; i < BUCKETS; i++) {
56 const n = counts[i] ?? 0;
57 if (!n) continue;
58 if (seen + n >= rank) {
59 const low = i === 0 ? 0 : BOUNDS_MS[i - 1];
60 const high = i < BOUNDS_MS.length ? BOUNDS_MS[i] : BOUNDS_MS[BOUNDS_MS.length - 1] * 2;
61 const within = n === 0 ? 0 : (rank - seen) / n;
62 return Math.round((low + (high - low) * within) * 100) / 100;
63 }
64 seen += n;
65 }
66 return BOUNDS_MS[BOUNDS_MS.length - 1];
67}
68
69export function percentiles(counts: readonly number[]): Percentiles {
70 return { p50: quantile(counts, 0.5), p75: quantile(counts, 0.75), p95: quantile(counts, 0.95), p99: quantile(counts, 0.99) };
71}