Histogram¶
Header: libxs_hist.h
Thread-safe histogram with running statistics. Buckets by vals[0];
additional values per entry track user-defined statistics.
Types¶
typedef struct libxs_hist_t libxs_hist_t; /* opaque */
typedef struct libxs_span_t libxs_span_t; /* opaque */
typedef void (*libxs_hist_update_t)(double* dst, const double* src, int count);
typedef void (*libxs_hist_fold_t)(void* ctx, double* queue, int nvals, int nsamples);
An update function reduces one value into one bucket, pairwise and in an order that must not matter. A fold instead sees a whole batch of samples at once and keeps state of its own, which is what a bucket cannot express — relating samples to one another rather than summarizing them independently.
typedef struct libxs_hist_info_t {
const int* buckets; /* per-bucket counts [nbuckets] */
const double* vals; /* per-bucket values [nbuckets * nvals] */
const double* sum; /* running total per value [nvals] */
double range[2]; /* [min, max] of vals[0] */
int nbuckets, nvals, nsamples;
} libxs_hist_info_t;
sum is accumulated on push and is therefore independent of the update
functions: it is a true total even where a bucket keeps a mean, a minimum or a
maximum. nsamples equals the sum of the bucket counts.
Functions¶
libxs_hist_t* libxs_hist_create(int nbuckets, int nvals,
const libxs_hist_update_t update[], libxs_hist_fold_t fold, void* ctx);
void libxs_hist_destroy(libxs_hist_t* hist);
Create/destroy. update[nvals] specifies per-slot accumulation
(NULL defaults to avg). fold is optional (NULL for none) and receives ctx
unchanged; the histogram only borrows it, so it must outlive the histogram.
Destroy accepts NULL.
Insert one sample. Samples are queued and folded into the buckets in batches,
so a histogram is exact while a batch fits and summarizes only once it is full.
Re-bins automatically if vals[0] exceeds the established range.
Query statistics, folding whatever is still queued. Repeated calls report the same result rather than folding the same samples twice.
void libxs_hist_query_percentile(libxs_lock_t* lock,
const libxs_hist_t* hist, double vals[], double percentile);
void libxs_hist_query_median(libxs_lock_t* lock,
const libxs_hist_t* hist, double vals[]);
void libxs_hist_query_mode(libxs_lock_t* lock,
const libxs_hist_t* hist, double vals[]);
Values at a percentile [0..1], at the median (0.5), or of the most populated
bucket. All report per-bucket aggregates, blending only towards a populated
neighbour, so the result describes samples that were actually observed. The mode
is preferable for a multi-modal distribution, where a percentile can fall
between clusters; its vals[0] is the bucket's upper bound rather than an
aggregate.
void libxs_hist_print(FILE* ostream, const libxs_hist_t* hist,
const int prec[], const char fmt[], ...);
Print to stream. prec[k] controls precision; negative skips output.
NULL ostream/fmt accepted.
Interval Union¶
libxs_span_t* libxs_span_create(int nsegments);
void libxs_span_destroy(libxs_span_t* span);
void libxs_span_push(libxs_span_t* span, double begin, double end);
double libxs_span_total(const libxs_span_t* span, int* inexact);
void libxs_hist_fold_union(void* ctx, double* queue, int nvals, int nsamples);
The time a set of intervals covers, as opposed to the sum of their lengths,
which counts overlapping intervals more than once. nsegments bounds how far
out of order intervals may arrive, not how many there may be: an interval
beginning before what has already been retired is added whole and counted in
inexact, which makes the total an upper bound. It never understates, so a
ratio of summed length over covered time is a lower bound on how many intervals
overlapped.
libxs_hist_fold_union is a ready-made libxs_hist_fold_t that expects ctx
to be a libxs_span_t* and reads the last two values of a sample as
(begin, end); pass it to libxs_hist_create to obtain both a distribution of
durations and the time they covered from one push.
Values must share an origin a double can hold exactly. Wall-clock nanoseconds do not — far above 2^53 the spacing between representable values is hundreds of nanoseconds, enough to make adjacent intervals appear to touch — so subtract an epoch first.
Update Functions¶
void libxs_hist_update_avg(double* dst, const double* src, int count);
void libxs_hist_update_add(double* dst, const double* src, int count);
void libxs_hist_update_min(double* dst, const double* src, int count);
void libxs_hist_update_max(double* dst, const double* src, int count);
avg— Welford online mean:*dst += (*src - *dst) / countadd— sum:*dst += *srcmin—*dst = min(*dst, *src)max—*dst = max(*dst, *src)
Custom callbacks use the same signature; count enables online
algorithms without external state. A callback that needs the whole sample, or
state outside a bucket, is a fold rather than an update.