Data structures

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

#analytics#monitoring#reports#memory

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 error

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.

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) → any

The 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) → null

Forgets 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.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/quantiles.qwer$ curl -O https://quillolang.org/plugins/files/quantiles.qwe

Then, in your program, all of it or only what you call:

main.qwe
use quantiles.qweruse quantile_add, quantile_get from quantiles      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
tdigest = "1"

Example

quantiles.qwe
// 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

quantiles.qwer
//! 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);    }}
Sketches 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.analyticscountingmemoryreports7 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.searchautocompleteformsmemory8 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.gamesrankingmemorygamification7 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.cachingperformancerate-limitsmemory8 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.segmentssetsanalyticsdatabase6 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.metricsmonitoringcountersdashboardsalerts8 views · 8 downloads
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Percentiles: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.