Percentiles
Streaming percentiles kept in memory by name: p50, p95 and p99 of response times or order values with a t-digest that stays small, and the rank of any value.
Version 1.0.0 · 5 functions · 1 crate
Functions
What use quantiles.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use quantile_add, quantile_get from quantiles (Quillo 0.6).
quantile_add(name: str, values: list) → intor an errorAdds values (up to 100,000 a call; numbers that are not finite are skipped) to the digest name, made on first use (up to 1,000 names, about 10 KB each). Gives how many values it holds.
quantile_get(name: str, q: float) → float?The value below which a share q (0 to 1) of the values lie: 0.5 is the median, 0.99 the 99th percentile. Null when the digest is empty or unknown. Exact at the ends, within a fraction of a percent in between.
quantile_summary(name: str) → anyThe usual figures of digest name: { count, min, max, mean, p50, p90, p95, p99 }, for a dashboard. Nulls when empty.
quantile_rank(name: str, value: float) → float?The share of values in digest name below value, 0 to 1: "this request was slower than 97% of them". Null when empty.
quantile_reset(name: str) → nullForgets digest name, to start a new period or free its memory.
Get it
$ quillo install quantiles # quantiles.qwer into this folder$ quillo install quantiles --example # and its example, quantiles.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/quantiles.qwer$ curl -O https://quillolang.org/plugins/files/quantiles.qweThen, in your program, all of it or only what you call:
use quantiles.qweruse quantile_add, quantile_get from quantiles // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
tdigest = "1"Example
// Response-time percentiles for a status page, kept in memory as requests come in.//// quillo run quantiles.qwe then open http://localhost:8080/latencyuse "quantiles.qwer"// Each API request adds its time; here, 10,000 made-up timings: most fast, a few slow.route GET /latency { quantile_reset("api") quantile_add("api", range(1, 10001) | map((i) => i % 100 == 0 ? 900 + i % 7 : 20 + i % 61))? { summary: quantile_summary("api"), p999: quantile_get("api", 0.999), slower_than: quantile_rank("api", 85.0) }}route GET /order-values = { added: quantile_add("orders", [12.5, 40, 22, 99.9, 15, 18, 250, 31])?, median: quantile_get("orders", 0.5), empty: quantile_summary("nothing").p50,}$ quillo run quantiles.qwe$ curl 'localhost:8080/latency'$ curl 'localhost:8080/order-values'Source
//! Streaming percentiles kept in memory by name, shared by all requests: p50, p95 and p99 of response times, order values or queue waits, with a t-digest that stays small however many values come in.//! version: 1.0.0//!//! [dependencies]//! tdigest = "1"use serde_json::{json, Value};use std::collections::HashMap;use std::sync::Mutex;use tdigest::TDigest;static DIGESTS: Mutex<Option<HashMap<String, TDigest>>> = Mutex::new(None);fn round(x: f64) -> f64 { (x * 1000.0).round() / 1000.0}/// Adds `values` (up to 100,000 a call; numbers that are not finite are skipped) to the digest `name`, made on first use (up to 1,000 names, about 10 KB each). Gives how many values it holds.pub fn quantile_add(name: &str, values: Vec<f64>) -> Result<i64, String> { if values.len() > 100_000 { return Err("up to 100,000 values a call".into()); } let mut g = DIGESTS.lock().unwrap_or_else(|e| e.into_inner()); let all = g.get_or_insert_with(HashMap::new); if !all.contains_key(name) && all.len() >= 1000 { return Err("up to 1,000 digests: quantile_reset old ones".into()); } let d = all.entry(name.to_string()).or_insert_with(|| TDigest::new_with_size(200)); d.extend_values(values.into_iter().filter(|v| v.is_finite())); d.flush(); Ok(d.count() as i64)}/// The value below which a share `q` (0 to 1) of the values lie: 0.5 is the median, 0.99 the 99th percentile. Null when the digest is empty or unknown. Exact at the ends, within a fraction of a percent in between.pub fn quantile_get(name: &str, q: f64) -> Option<f64> { let g = DIGESTS.lock().unwrap_or_else(|e| e.into_inner()); g.as_ref()?.get(name)?.estimate_quantile(q.clamp(0.0, 1.0)).map(round)}/// The usual figures of digest `name`: `{ count, min, max, mean, p50, p90, p95, p99 }`, for a dashboard. Nulls when empty.pub fn quantile_summary(name: &str) -> Value { let g = DIGESTS.lock().unwrap_or_else(|e| e.into_inner()); let Some(d) = g.as_ref().and_then(|a| a.get(name)) else { return json!({ "count": 0, "min": null, "max": null, "mean": null, "p50": null, "p90": null, "p95": null, "p99": null }); }; let q = |x: f64| d.estimate_quantile(x).map(round); json!({ "count": d.count() as i64, "min": d.min(), "max": d.max(), "mean": d.mean().map(round), "p50": q(0.5), "p90": q(0.9), "p95": q(0.95), "p99": q(0.99) })}/// The share of values in digest `name` below `value`, 0 to 1: "this request was slower than 97% of them". Null when empty.pub fn quantile_rank(name: &str, value: f64) -> Option<f64> { let g = DIGESTS.lock().unwrap_or_else(|e| e.into_inner()); g.as_ref()?.get(name)?.estimate_rank(value).map(round)}/// Forgets digest `name`, to start a new period or free its memory.pub fn quantile_reset(name: &str) { if let Some(a) = DIGESTS.lock().unwrap_or_else(|e| e.into_inner()).as_mut() { a.remove(name); }}Related
sketches.qwer · 1.0.0Counting at scale in little memory, kept by name for all requests: unique visitors by HyperLogLog, merged across days, and top pages or search terms by a count-min sketch.7 views · 7 downloadsAutocomplete trie.qwer · 1.0.0A prefix index for autocomplete kept in memory by name: entries with weights and data, the best completions of what was typed, at a word's start too.8 views · 11 downloadsLeaderboards leaderboard.qwer · 1.0.0Leaderboards in memory shared by all requests: scores set, kept at their best or added to, a player's rank, the top N, and the players around one.7 views · 6 downloadsMemory cache memory_cache.qwer · 1.0.0An in-memory cache shared by all requests, with a lifetime per entry, a size limit that drops the least used first, counters for rate limits, and hit statistics.8 views · 8 downloadsRoaring sets roaring.qwer · 1.0.0Compressed sets of ids as Roaring bitmaps: who liked, who is in a segment, which products match; added to, combined by union, intersection and difference, counted, and kept as base64 text.6 views · 8 downloadsRolling windows rolling_windows.qwer · 1.0.0Moving counts, sums, averages and rates over time windows, kept in memory by name: requests a minute, error rates, this hour against the last, and moving averages over rows.8 views · 8 downloadsThread 0 posts
On Percentiles: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.