NAME
Data::Histogram::Shared - shared-memory HdrHistogram for Linux
SYNOPSIS
use Data::Histogram::Shared;
# track values in [1, 3_600_000_000] with 3 significant figures, anonymous
my $h = Data::Histogram::Shared->new(undef, 1, 3_600_000_000, 3);
$h->record(120); # record the value 120 once
$h->record(2500, 4); # record the value 2500 four times
$h->value_at_percentile(50); # median (50th percentile)
$h->percentile(99); # 99th percentile (alias)
$h->min; $h->max; $h->mean; # min / max / arithmetic mean
$h->total_count; # number of recorded values
# bulk record in a single lock acquisition
$h->record_many([ 100, 250, 250, 900, 1500 ]);
# merge another histogram of identical geometry (cellwise add)
my $other = Data::Histogram::Shared->new(undef, 1, 3_600_000_000, 3);
$other->record_many([ 50, 75, 80 ]);
$h->merge($other);
# share across processes via a backing file
my $shared = Data::Histogram::Shared->new("/tmp/latency.hdr", 1, 3_600_000_000, 3);
DESCRIPTION
A High Dynamic Range histogram (HdrHistogram) in shared memory: a compact, fixed-size structure that records integer values across a very wide range and answers percentile, minimum, maximum and mean queries within a fixed, configurable relative error. It is the standard tool for latency and response- time measurement, where the values of interest span many orders of magnitude (microseconds to seconds) yet a constant number of significant figures must be preserved across the whole range.
You construct the histogram with the lowest and highest values it must track and the number of sig_figs (significant figures) of precision you require. Values are bucketed logarithmically -- one bucket per power of two of magnitude -- and linearly within each bucket, so that any two values that agree to sig_figs significant figures fall into the same sub-bucket and are treated as equivalent. A percentile query is therefore accurate to within 1 / 10**sig_figs relative error: with the default 3 significant figures, a reported percentile is within 0.1% of the true value. Memory is proportional to the dynamic range and the precision, not to the number of values recorded: you can record billions of samples into a few kilobytes of counters.
The histogram stores integer values only. To record floating-point quantities, scale them to integers yourself before recording -- for example, record a latency in microseconds (or nanoseconds) rather than fractional seconds, and divide the reported percentiles back down.
Because the counts array lives in a shared mapping, several processes share one histogram: any process that opens the same backing file, inherits the anonymous mapping across fork, or reopens a passed memfd, sees the others' recordings and contributes its own. A write-preferring futex rwlock with dead-process recovery guards mutation, so many processes may record and query concurrently. Two histograms of identical geometry (same lowest, highest and sig_figs) can be combined with merge (cellwise add of their counts arrays), which yields a histogram whose distribution is the union of the two input streams. Linux-only. Requires 64-bit Perl.
METHODS
Constructors
my $h = Data::Histogram::Shared->new($path, $lowest, $highest, $sig_figs);
my $h = Data::Histogram::Shared->new(undef, 1, 3_600_000_000, 3); # defaults
my $h = Data::Histogram::Shared->new_memfd($name, $lowest, $highest, $sig_figs);
my $h = Data::Histogram::Shared->new_from_fd($fd);
$path is the backing file (undef or omitted for an anonymous mapping). $lowest is the lowest value that can be distinguished from 0 and must be >= 1 (default 1). $highest is the highest value that can be tracked and must be >= 2 * $lowest (default 3_600_000_000, i.e. one hour in microseconds). $sig_figs is the number of significant figures of precision and must be in the range 1..5 (default 3). new and new_memfd croak if any argument is out of range.
$lowest additionally must satisfy floor(log2($lowest)) + ceil(log2(2 * 10**$sig_figs)) - 1 <= 61 (a histogram-geometry limit); in practice this only rejects a very large $lowest -- above roughly 2**51 at the default sig_figs = 3. Such a histogram croaks at construction.
From these the histogram derives its bucket geometry: a unit magnitude of floor(log2($lowest)), 2 * 10**sig_figs sub-buckets per power of two (rounded up to a power of two), and as many buckets as are needed to cover $highest. When reopening an existing file or memfd, the stored geometry wins and the caller's $lowest/$highest/$sig_figs arguments are ignored. new_memfd creates a Linux memfd (transferable via its memfd descriptor); new_from_fd reopens one in another process.
Recording values
my $count = $h->record($value); # record once; returns new total_count
my $count = $h->record($value, $n); # record $n times; returns new total_count
my $added = $h->record_many(\@values); # record each once; returns how many
$h->reset; # clear every count back to empty
record adds $n (default 1) occurrences of the integer $value to the histogram and returns the new total_count. $value must be in the range 0 .. $highest: a negative value croaks, and a value above $highest croaks ("value ... exceeds highest_trackable_value"). Values strictly below $lowest are recorded but collapse into the lowest sub-bucket. $n is an unsigned integer.
record_many takes an array reference and records each element once under a single write lock, returning the number recorded. Every element is range-checked before the lock is taken, so an out-of-range element croaks without recording any of the batch (and without holding the lock).
The precision contract: any two values that agree to sig_figs significant figures are equivalent -- they fall into the same sub-bucket and are indistinguishable once recorded. A value read back out of the histogram (for example via a percentile query) is the highest value equivalent to the bucket it landed in, so it is always >= the recorded value and within 1 / 10**sig_figs relative error of it.
Querying
my $v = $h->value_at_percentile($p); # value at the $p-th percentile (0..100)
my $v = $h->percentile($p); # alias for value_at_percentile
my $c = $h->count_at_value($value); # count recorded in $value's bucket
my $lo = $h->min; # lowest recorded value (0 if empty)
my $hi = $h->max; # highest recorded value (0 if empty)
my $mu = $h->mean; # arithmetic mean (0 if empty)
my $n = $h->total_count; # number of recorded values
my $n = $h->count; # alias for total_count
value_at_percentile returns the value below which $p percent of the recorded values lie: the highest equivalent value of the bucket at that percentile, so it never underestimates. $p is a percentile in the range 0..100 (not a 0..1 quantile). percentile is a short alias. An empty histogram returns 0 for any percentile.
count_at_value returns the number of values recorded in the bucket that $value falls into (so values equivalent to $value are counted together). $value is range-checked like record: a negative value, or a value above $highest, croaks. min and max are the exact lowest and highest values ever recorded (each 0 when nothing has been recorded). mean is the arithmetic mean, computed from each bucket's median-equivalent value. total_count (aliased count) is the number of recorded values -- the sum of all the per-bucket counts.
Merging
$h->merge($other);
Folds $other's counts array into $h by cellwise addition, so $h then represents the combined distribution of both histograms; $h's total_count, min and max are updated to span both. Both histograms must have identical geometry -- the same lowest, highest and sig_figs (merge croaks on a mismatch). $other is read under its own lock into a private snapshot first, so merging is deadlock-free even if two processes merge each other concurrently; $other is not modified. Counts that would overflow a 64-bit cell saturate at the maximum value.
Introspection and lifecycle
$h->lowest; $h->highest; $h->sig_figs; $h->counts_len; $h->stats;
$h->path; $h->memfd; $h->sync; $h->unlink; # or Class->unlink($path)
lowest, highest and sig_figs return the configured geometry; counts_len is the number of 64-bit counter cells. sync flushes the mapping to its backing store (a no-op for anonymous and memfd histograms, which have none); unlink removes the backing file (also callable as Class->unlink($path)); path returns the backing path (undef for anonymous, memfd, or fd-reopened histograms) and memfd the backing descriptor -- the memfd of a new_memfd histogram or the dup'd fd of a new_from_fd histogram, and -1 for file-backed or anonymous histograms.
STATS
stats() returns a hashref describing the histogram:
lowest-- the lowest trackable value.highest-- the highest trackable value.sig_figs-- the significant figures of precision.count-- the number of recorded values (total_count).min-- the lowest recorded value (0 if empty).max-- the highest recorded value (0 if empty).mean-- the arithmetic mean of the recorded values (0.0 if empty).counts_len-- the number of 64-bit counter cells.bucket_count-- the number of logarithmic buckets.sub_bucket_count-- the number of linear sub-buckets per bucket.ops-- running count of mutating operations (record,record_many,merge,reset).mmap_size-- bytes of the shared mapping.
SHARING ACROSS PROCESSES
The histogram lives in a shared mapping, shared the same three ways as the rest of the family: a backing file (every process calls new($path, ...) on the same path with matching geometry), an anonymous mapping inherited across fork, or a memfd whose descriptor is passed to an unrelated process (over a UNIX socket via SCM_RIGHTS, or via /proc/$pid/fd/$n) and reopened with new_from_fd($fd). Because the mapping is shared, every process records into and queries the same counts array, so the distribution reflects the combined stream all of them have recorded.
# producer and consumer share one histogram with no coordination
my $h = Data::Histogram::Shared->new(undef, 1, 1_000_000, 3); # before fork
unless (fork) { $h->record(500) for 1 .. 10; exit }
wait;
print $h->value_at_percentile(50), "\n"; # the child's recordings
SECURITY
The mmap region is writable by all processes that open it. Do not share backing files with untrusted processes.
CRASH SAFETY
Mutation is guarded by a futex-based write-preferring rwlock with PID-encoded ownership; if a holder dies, the next contender detects the dead owner and recovers. Each count increment is a single word store, so a crash leaves the histogram consistent up to the last completed record. Limitation: PID reuse is not detected (very unlikely in practice).
SEE ALSO
Data::CountMinSketch::Shared, Data::HyperLogLog::Shared, Data::BloomFilter::Shared, Data::Intern::Shared, Data::SortedSet::Shared, Data::SpatialHash::Shared, and the rest of the Data::*::Shared family.
AUTHOR
vividsnow
LICENSE
This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.