high_roller/lib.rs
1//! # High Roller
2//!
3//! This `no_std` library includes tools for tracking rolling-window
4//! statistics in latency-sensitive systems. The motivating case was
5//! reporting downsampled performance telemetry in embedded applications,
6//! but it's hopefully useful for other domains as well.
7//!
8//! This crate contains three members:
9//! - Rolling Max: tracks the greatest value in a fixed-size window.
10//! - Rolling Sum: tracks the sum of entries in a fixed-size window.
11//! - Decimal32: a 32-bit fixed-precision decimal type. Pairs with `RollingSum`
12//! when float precision is needed. Native floating point types are incompatible
13//! with `RollingSum`'s overflow recovery mechanism.
14//!
15//! This crate has the following design motivations:
16//! - Algorithmic optimality: Max and overflow-resilient Sum expose asymptotically
17//! optimal operations.
18//! - Performance orientation: no_std, no heap allocs, and a performance-aware
19//! approach to implementation. Demonstrated performance improvement contributions
20//! are also very welcome.
21//! - Code simplicity: this crate has more lines of docs than actual code. When feature
22//! availability and simplicity collide, the latter is chosen.
23//!
24//! # Example
25//!
26//! The example below shows how `high_roller` could be used to track
27//! and publish request latency telemetry in an application with
28//! scheduled ticks and structured I/O patterns.
29//!
30//! The example expects a request every 1/3 ticks, publishes telemetry
31//! every 100 ticks, and tracks a window of 1000 ticks.
32//!
33//! ```rust
34//! use core::cmp::Reverse;
35//!
36//! use high_roller::decimal::D5;
37//! use high_roller::rolling_max::RollingMax;
38//! use high_roller::rolling_sum::RollingSum;
39//!
40//! /// Track a rolling window of this many ticks.
41//! const WINDOW: usize = 1000;
42//!
43//! /// Expect a request every 1 / 3 ticks.
44//! const EXPECTED_INTERVAL: u32 = 3;
45//!
46//! /// Emits telemetry on the rolling window of 1000 ticks.
47//! /// Emitted every 100 ticks.
48//! #[allow(unused)]
49//! struct TelemetryMsg {
50//! /// The maximum number of ticks between requests.
51//! max_latency: Option<u32>,
52//!
53//! /// The minimum number of ticks between requests.
54//! min_latency: Option<u32>,
55//!
56//! /// Root Mean Square Error of request latency from what is expected.
57//! rmse: Option<D5>,
58//! }
59//!
60//! let mut io = IoLayer::new();
61//! let mut telemetry = Telemetry::default();
62//!
63//! while io.tick() {
64//! let req = io.next_request();
65//! if let Some(req) = &req {
66//! process_request(req);
67//! }
68//! telemetry.log_tick(req.is_some());
69//!
70//! if io.count % 100 == 0 {
71//! let max_latency = telemetry.max_latency_ticks.max().copied();
72//! let min_latency = telemetry.min_latency_ticks.max().map(|m| m.0);
73//! let rmse = {
74//! let sum_sq = telemetry.rmse_acc.total().copied().map(D5::get);
75//! let sample_ct = telemetry.rmse_samples.total().copied().unwrap_or(0);
76//! sum_sq.and_then(|sum_sq| {
77//! (sample_ct != 0)
78//! .then(|| (sum_sq / sample_ct as f64).sqrt())
79//! .map(D5::cast)
80//! })
81//! };
82//!
83//! io.log_telemetry(TelemetryMsg {
84//! max_latency,
85//! min_latency,
86//! rmse,
87//! });
88//! }
89//! }
90//!
91//! /// An accumulator for dynamic system telemetry.
92//! #[derive(Default)]
93//! struct Telemetry {
94//! tick: u32,
95//! last_req_tick: u32,
96//! rmse_acc: RollingSum<D5, WINDOW>,
97//! rmse_samples: RollingSum<u32, WINDOW>,
98//! max_latency_ticks: RollingMax<u32, WINDOW>,
99//! min_latency_ticks: RollingMax<Reverse<u32>, WINDOW>,
100//! }
101//!
102//! impl Telemetry {
103//! /// Call this once every tick to log statistics based on
104//! /// whether a request was received.
105//! fn log_tick(&mut self, received_req: bool) {
106//! self.tick = self.tick.wrapping_add(1);
107//!
108//! if !received_req {
109//! self.max_latency_ticks.push(0);
110//! self.min_latency_ticks.push(Reverse(u32::MAX));
111//! self.rmse_acc.add(D5::ZERO);
112//! self.rmse_samples.add(0);
113//! return;
114//! }
115//!
116//! let interval = self
117//! .tick
118//! .checked_sub(self.last_req_tick)
119//! .expect("irrational last_req");
120//! self.last_req_tick = self.tick;
121//!
122//! // RMSE = sqrt(mean(sq_err)).
123//! // Saturate worst-case error at `D5::MAX`.
124//! let sq_err = D5::checked((interval as f64 - EXPECTED_INTERVAL as f64).powf(2.))
125//! .unwrap_or(D5::MAX);
126//!
127//! self.max_latency_ticks.push(interval);
128//! self.min_latency_ticks.push(Reverse(interval));
129//! self.rmse_acc.add(sq_err);
130//! self.rmse_samples.add(1);
131//! }
132//! }
133//!
134//! /// A dummy I/O layer.
135//! struct IoLayer {
136//! rng: rand::rngs::ThreadRng,
137//! dist: rand::distr::Bernoulli,
138//! // How many ticks this contrived IO stack will sustain. Otherwise the
139//! // example would run forever.
140//! count: usize,
141//! }
142//!
143//! impl IoLayer {
144//! /// Creates a new IoLayer instance. A real app presumably loops forever,
145//! /// but this dummy stack self-destructs after a certain number of ticks.
146//! fn new() -> Self {
147//! Self {
148//! rng: rand::rng(),
149//! dist: rand::distr::Bernoulli::from_ratio(1, 3).expect("good range"),
150//! count: 10_000,
151//! }
152//! }
153//!
154//! /// Returns Some(Request) if one was received and None if not.
155//! fn next_request(&mut self) -> Option<Request> {
156//! use rand::distr::Distribution;
157//! self.dist.sample(&mut self.rng).then_some(Request)
158//! }
159//!
160//! /// In a real system, this would implement some timed tick mechanism.
161//! /// Here it's just a pass through. Returns false if the example should exit.
162//! fn tick(&mut self) -> bool {
163//! let prev = self.count;
164//! self.count = prev.saturating_sub(1);
165//! prev != 0
166//! }
167//!
168//! /// Pushes a telemetry message into some structured logging pipeline.
169//! fn log_telemetry(&self, msg: TelemetryMsg) {
170//! core::hint::black_box((&self, msg));
171//! }
172//! }
173//!
174//! struct Request;
175//!
176//! fn process_request(req: &Request) {
177//! core::hint::black_box(req);
178//! }
179//!
180//! ```
181
182#![no_std]
183
184pub mod decimal;
185pub mod rolling_max;
186pub mod rolling_sum;