Note: The initial draft of this README was generated by AI (Antigravity) based on real-machine benchmark tests and source code architecture analysis, and subsequently reviewed and fine-tuned by the project author.
HdrHistogram-Kotlin is a high-performance, High Dynamic Range (HDR) histogram library specifically built for Kotlin Multiplatform (KMP), benchmarked against and inspired by Gil Tene's official Java native implementation HdrHistogram.
It supports recording and analyzing sampled data value distributions across a configurable integer range with configurable relative precision (number of significant digits). It is designed specifically for latency-sensitive performance monitoring, APM, network testing, system metrics, and related use cases.
- Core Features
- Why HDR Histogram?
- Supported Platforms
- Core API & Architecture
- Quick Start
- Performance Benchmarks (Benchmark Report)
- Test Machine Environment & Methodology
- 1. JVM Throughput Comparison (Throughput - Best Single Run)
- 2. JVM Write Latency Comparison (Latency - Best Single Run)
- 3. Memory Footprint & Zero-Allocation Testing
- 4. JVM Statistics Query and Reset Latency
- 5. KMP Cross-Platform Performance (JVM vs WasmJS vs JS)
- Commands to Reproduce Benchmarks
- Low-Level Performance Analysis & Design Comparison
- Acknowledgments & License
- Constant Space and Time Complexity: Physical memory footprint is constant, determined entirely by the configured value range and precision at construction; zero memory allocations (Zero GC / Allocation-free) during value recording, with no linked lists, hash tables, or dynamic array resizing overhead.
- Branchless Bitwise Addressing: Computes bucket and sub-bucket indices directly using
countLeadingZeroBits(hardware CPU CLZ instruction) and bitmasks, eliminatingifbranches from critical hot paths. - Efficient Concurrency & Double Buffering:
- The double-buffering
Recorderis built on a two-phase rotating synchronizer (WriterReaderPhaser). Writer threads record data with just a single atomic increment, while reader threads periodically flip phases to extract interval snapshots without locking active writers. AtomicHistogramleverages CPU hardware-level atomic bus instructions (XADD / LDADD) to support high-concurrency multi-threaded writes directly.
- The double-buffering
- Native Kotlin Multiplatform Support: A unified codebase fully covers JVM, Android, Linux (x64), macOS/iOS (Native), WebAssembly (WasmJS), and JavaScript.
In system performance monitoring (such as SLAs, service latencies, and transaction durations), averages often conceal critical tail latencies (such as the 99.9th or 99.99th percentile request spikes).
Traditional histograms either use fixed-width linear buckets (causing memory explosion over large dynamic ranges) or coarse logarithmic buckets (causing severe loss of resolution at higher values).
The HDR Histogram Solution: HDR Histograms partition ranges adaptively based on a configured number of Significant Value Digits:
- For example, configured with a precision of
3(guaranteeing relative resolution better than 0.1%):- In the
$1,\mu\text{s} \sim 2,\text{ms}$ range, resolution is accurate to$1,\mu\text{s}$ ; - In the
$2,\text{ms} \sim 2,\text{s}$ range, resolution is accurate to$1,\text{ms}$ ; - In the
$2,\text{s} \sim 2,000,\text{s}$ range, resolution is accurate to$1,\text{s}$ .
- In the
- Regardless of the magnitude of the measured value, measurement error is strictly bounded within 0.1%, while the entire histogram occupies only tens to hundreds of kilobytes of memory.
| Platform Target | Runtime / Architecture | Concurrency Model Implementation |
|---|---|---|
| JVM | Java 11+ / OpenJDK | Atomic operations + Thread.onSpinWait() + Double-buffered Phaser |
| Android | Android 7.0+ (API 24+) | Atomic operations + Double-buffered Phaser |
| Native | Linux (x64), iOS (Arm64/Simulator) | POSIX / Atomic operations + pause/yield inline assembly |
| WasmJS | WebAssembly (Node.js / Browser) | Single-threaded efficient double buffering |
| JS | JavaScript (Node.js / Browser) | Single-threaded efficient double buffering |
classDiagram
direction TB
class Histogram {
+totalCount: Long
+minValue: Long
+maxValue: Long
+mean: Double
+recordValue(value: Long, count: Long)
+valueAtPercentile(percentile: Double) Long
+add(other: Histogram)
+reset()
}
class AtomicHistogram {
+totalCount: Long
+minValue: Long
+maxValue: Long
+mean: Double
+recordValue(value: Long, count: Long)
+valueAtPercentile(percentile: Double) Long
+reset()
}
class Recorder {
+recordValue(value: Long, count: Long)
+getIntervalHistogram(histogramToRecycle: Histogram?) Histogram
}
Recorder ..> Histogram : produces interval
Recorder ..> AtomicHistogram : internal buffer (concurrent)
Histogram: Non-thread-safe base histogram with nanosecond-level recording speed, suitable for single-threaded, coroutine-confined, or batch aggregation scenarios.AtomicHistogram: Lock-free histogram supporting high-concurrency multi-threaded writes, backed by atomic arrays.Recorder: Double-buffered recorder prioritized for throughput. Worker threads record lock-free, while a background reporting thread periodically extracts interval snapshots without stalling writers.
Add the dependency to your build.gradle.kts:
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.dsqrwym:hdrhistogram-kotlin:0.0.2")
}
}
}Suitable for single-threaded recording, offline log analysis, or as an aggregation container:
import io.github.dsqrwym.hdrhistogram.Histogram
// Create a histogram: lowest discernible value 1ns, highest trackable value 1 hour (3.6e12 ns), 3 significant digits (0.1% precision)
val histogram = Histogram(
lowestDiscernibleValue = 1L,
highestTrackableValue = 3_600_000_000_000L,
numberOfSignificantValueDigits = 3
)
// Record latency measurements (nanoseconds)
histogram.recordValue(1_500L)
histogram.recordValue(25_000L)
histogram.recordValue(100_000L, count = 10L) // Batch record 10 occurrences
// Query key metrics
println("Total count: ${histogram.totalCount}")
println("Min value: ${histogram.minValue} ns")
println("Max value: ${histogram.maxValue} ns")
println("Mean: ${histogram.mean} ns")
println("P50 Median: ${histogram.valueAtPercentile(50.0)} ns")
println("P99: ${histogram.valueAtPercentile(99.0)} ns")
println("P99.9: ${histogram.valueAtPercentile(99.9)} ns")
// Reset the histogram (without reallocating memory)
histogram.reset()Recommended for continuous high-throughput recording (e.g., API gateways, RPC services, trading engines) where metrics are reported periodically in the background:
import io.github.dsqrwym.hdrhistogram.Recorder
import io.github.dsqrwym.hdrhistogram.Histogram
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
val recorder = Recorder(
lowestDiscernibleValue = 1_000L, // 1 microsecond
highestTrackableValue = 60_000_000_000L, // 60 seconds
numberOfSignificantValueDigits = 3
)
// Simulate multiple concurrent writer threads / coroutines
repeat(8) {
CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
val latencyNs = measureRequestTime()
recorder.recordValue(latencyNs)
}
}
}
// Independent metric collection and logging thread (e.g. collecting every 10 ms)
CoroutineScope(Dispatchers.Default).launch {
// Passing null initially allocates a Histogram instance on the heap;
// Passing the same instance back to getIntervalHistogram on subsequent cycles resets and reuses it with zero GC allocation.
var recycleHistogram: Histogram? = null
while (isActive) {
delay(10.milliseconds)
// Flip phase and seamlessly obtain interval histogram, reusing the old instance
recycleHistogram = recorder.getIntervalHistogram(recycleHistogram)
println("Interval count: ${recycleHistogram.totalCount}, " +
"P99: ${recycleHistogram.valueAtPercentile(99.0)} ns, " +
"P99.9: ${recycleHistogram.valueAtPercentile(99.9)} ns")
}
}Used when multiple threads concurrently record directly into a single shared histogram:
import io.github.dsqrwym.hdrhistogram.AtomicHistogram
val atomicHistogram = AtomicHistogram(
lowestDiscernibleValue = 1L,
highestTrackableValue = 10_000_000L,
numberOfSignificantValueDigits = 3
)
// Can be called concurrently from any thread; utilizes atomic instructions under the hood
atomicHistogram.recordValue(12345L)Important Note: The atomicity of
AtomicHistogramonly guarantees thread safety for write operations (no lost counts or data races). However, concurrent reads (such as callingvalueAtPercentile) do not provide a globally consistent atomic snapshot across all buckets (i.e. writing threads may update subsequent buckets while a reader traverses the array). For production scenarios requiring high-frequency multi-threaded writes alongside periodic consistent metric reads, always useRecorder.
To provide rigorous and objective data, we compared this project against the official native Java library (org.hdrhistogram:HdrHistogram:2.2.2) under identical hardware and runtime conditions (including Gil Tene's 1:1 benchmark logic, typical workload models, and memory overhead).
- Processor (CPU): AMD Ryzen 5 7530U with Radeon Graphics (6 cores / 12 threads, base 2.0 GHz, boost 4.5 GHz)
- Memory (RAM): 16 GB DDR4 (15.3 GB usable)
- Operating System: Windows 11 64-bit
- Java Runtime: OpenJDK 64-Bit Server VM (build 25.0.3+11-LTS)
- Node.js Runtime: v24.15.0 (used for JS and WasmJS tests)
- Benchmark Framework: kotlinx-benchmark (based on JMH 1.37)
- Methodology: Executed 3 independent benchmark runs, selecting the overall best-performing full run (Run 2) to ensure temporal and system load consistency across all reported metrics.
Unit:
ops/sec(operations completed per second). Higher is better.
| Benchmark Scenario | Official Java (HdrHistogram 2.2.2) |
This Project (HdrHistogram-Kotlin) |
Relative Performance |
|---|---|---|---|
Official 1:1 Benchmark Write (rawRecordingSpeed)(Matches official benchmark: 1h range, 3 digits, varying values) |
341,864,964 ops/s (~342M ops/s) |
371,195,554 ops/s (~371M ops/s) |
~8.58% faster |
Best-Case Write (recordConstant)(Extreme L1 CPU cache hit) |
377,108,051 ops/s (~377M ops/s) |
387,143,369 ops/s (~387M ops/s) |
~2.66% faster |
Realistic Dynamic Write (recordVarying)(Spanning $\mu\text{s}$ to ms tail, testing bucket indexing & branch prediction) |
266,457,747 ops/s (~266M ops/s) |
304,287,786 ops/s (~304M ops/s) |
~14.19% faster |
Batch Write with Count (recordVaryingWithCount)(Single call adding count = 10) |
278,048,933 ops/s (~278M ops/s) |
305,382,571 ops/s (~305M ops/s) |
~9.83% faster |
Unit:
ns/op(nanoseconds per operation). Lower is better.
| Benchmark Scenario | Official Java | This Project (HdrHistogram-Kotlin) |
Difference |
|---|---|---|---|
Official 1:1 Latency (rawRecordingLatency) |
2.869 ns | 2.700 ns | ~5.9% lower latency |
Single-Value Constant Latency (recordConstantLatency) |
2.633 ns | 2.595 ns | On par |
Varying Cross-Bucket Latency (recordVaryingLatency) |
3.746 ns | 3.280 ns | ~12.4% lower latency |
Batch Value Latency (recordValueLatency) |
3.604 ns | 3.263 ns | ~9.5% lower latency |
One of the defining designs of HDR Histogram is its strictly constant and minimal physical memory footprint, along with zero heap allocations (Zero Allocation) while recording data.
Because over 99.9% of histogram memory is occupied by the underlying LongArray count storage, memory consumption is determined entirely by constructor parameters (object headers and fields take only ~100 bytes):
| Typical Configuration | Covered Range | Official Java Implementation | This Project (HdrHistogram-Kotlin) |
Array Element Count (counts) |
|---|---|---|---|---|
| 10 Million |
115.2 KB (115,200 bytes) | 114.7 KB (114,752 bytes) | 14,336 Longs | |
| 1 Hour |
188.9 KB (188,928 bytes) | 188.4 KB (188,480 bytes) | 23,552 Longs | |
| 1 Day |
221.7 KB (221,696 bytes) | 221.2 KB (221,248 bytes) | 27,648 Longs |
- Constant Footprint: Whether recording 1 value or 1 billion samples, the physical footprint remains strictly constant, with zero dynamic array resizing.
- Direct Calculation: The underlying array size can be derived directly via
countsArrayLength * 8 bytes.
To verify zero-allocation claims empirically, we used JVM's com.sun.management.ThreadMXBean.getThreadAllocatedBytes() to inspect the actual heap bytes allocated by the operating system and JVM to the thread before and after massive operations, after class loading and JIT warmup:
| Core Operation Tested | Invocations | Total Heap Bytes Allocated | Average Allocation (B/op) | Conclusion |
|---|---|---|---|---|
Histogram.recordValue |
1,000,000 | 0 bytes | 0.000 B/op | Primitive array mutation, zero GC |
Recorder.recordValue |
1,000,000 | 0 bytes | 0.000 B/op | Atomic increment, zero GC |
Recorder.getIntervalHistogram (Recycled) |
50,000 | 0 bytes | 0.000 B/op | Double-buffer rotation reusing instance, zero GC |
Histogram.reset |
100,000 | 0 bytes | 0.000 B/op | Fast array zeroing, zero heap allocation |
Evaluated on a pre-populated histogram containing 100,000 realistic samples spanning
$\mu\text{s}$ to tail ms (data from the best full run).
| Query Method | Official Java | This Project (HdrHistogram-Kotlin) |
Description |
|---|---|---|---|
Get Total Count totalCount |
0.541 ns | 0.540 ns | Register-level constant read |
Get Min Value minValue |
1.690 ns | 1.485 ns | Includes equivalent value alignment calculation |
Get Max Value maxValue |
1.261 ns | 1.116 ns | Includes equivalent value alignment calculation |
Median getP50 |
720.46 ns | 628.99 ns | Traversing to bucket matching rank |
P90 Percentile getP90 |
850.10 ns | 849.45 ns | Traversing to bucket matching rank |
P99 Percentile getP99 |
1710.36 ns | 1712.73 ns | Traversing to tail sub-bucket |
P99.9 Percentile getP999 |
3401.90 ns | 3411.36 ns | Traversing to extreme tail sub-bucket |
Full Mean Calculation getMean |
86.086 |
14.354 |
~6.0x faster (direct flat array iteration) |
Full Histogram Reset reset |
1.137 |
1.115 |
Fast array zeroing |
Benefiting from pure bitwise logic and zero-allocation design, this library demonstrates exceptional performance on WebAssembly (Wasm):
| Benchmark Item | JVM (OpenJDK 25) | WasmJS (V8 Engine) | JS (Node.js V8) |
|---|---|---|---|
Official Benchmark Throughput (rawRecordingSpeed) |
371.20 M ops/s | 122.68 M ops/s (~33% of JVM) | 4.97 M ops/s |
Constant Write Throughput (recordConstant) |
388.68 M ops/s | 297.43 M ops/s (~76% of JVM) | 3.64 M ops/s |
Varying Value Throughput (recordVarying) |
304.29 M ops/s | 109.06 M ops/s | 3.24 M ops/s |
Official Latency Benchmark (rawRecordingLatency) |
2.70 ns | 8.21 ns | 251.33 ns |
| P50 Percentile Query | 0.63 |
2.23 |
104.50 |
| P99 Percentile Query | 1.71 |
6.47 |
312.13 |
Cross-Platform Recommendation: When collecting high-frequency performance metrics in browser or Node.js environments, WasmJS is strongly recommended—it delivers 25x ~ 80x higher throughput than plain JS, with write latency as low as 7 ~ 9 nanoseconds.
You can reproduce all benchmark data locally using the project's Gradle tasks:
# Run full JVM benchmarks (including both official Java library and this project)
./gradlew :library:jvmBenchmarkBenchmark# Run JVM ThreadMXBean-based heap allocation and footprint tests
./gradlew :library:jvmTest --tests "test.JvmMemoryAllocationTest"# Requires Node.js installed locally
./gradlew :library:wasmJsBenchmarkBenchmark./gradlew :library:jsBenchmarkBenchmark# Run all unit tests across platforms (including multi-threaded consistency tests)
./gradlew allTestsGil Tene's branchless bucket mapping algorithm based on leading zeros (leadingZeroCountBase - Long.numberOfLeadingZeros(value | subBucketMask)) forms the mathematical cornerstone enabling HDR Histograms to record in nanoseconds.
While inheriting this mathematical model, this project introduces focused engineering simplifications:
- Elimination of Normalizing Index Offset (
normalizingIndexOffset): The official Java library dynamically callsnormalizeIndex(index, offset, length)during every write to support auto-resizing and floating-point conversions. This project focuses on fixed-range and fixed-precision high-performance integer scenarios, mapping indices directly to the physical array and eliminating an extra addition, modulo, and potential bounds checking branches. - End-to-End Inlining (
inline): Core operators such ascountsArrayIndex,bucketIndex, andlowestEquivalentValueare marked asinline. In combination with Kotlin's compiler inlining, this eliminates call stack frame overhead and virtual method dispatch. - Direct Array Traversal for Mean Calculation (
mean): Official Java'sgetMean()instantiates aRecordedValuesIteratorto traverse the histogram. This project iterates directly across the physicalLongArray, calculating equivalent midpoint values on non-zero buckets without heap allocations or iterator state overhead.
- Licensed under the Apache License 2.0.
- Core algorithms, data structures, and mathematical models originate from the outstanding work of Gil Tene: HdrHistogram. Many thanks to the original author and the community for providing such an exceptional performance metric infrastructure to the open-source ecosystem.