Skip to content

Add realistic data pattern benchmarks for TSDB codec#140390

Merged
salvatore-campagna merged 46 commits intoelastic:mainfrom
salvatore-campagna:benchmarks/tsdb-codec-realistic-data-patterns
Jan 14, 2026
Merged

Add realistic data pattern benchmarks for TSDB codec#140390
salvatore-campagna merged 46 commits intoelastic:mainfrom
salvatore-campagna:benchmarks/tsdb-codec-realistic-data-patterns

Conversation

@salvatore-campagna
Copy link
Contributor

@salvatore-campagna salvatore-campagna commented Jan 8, 2026

The existing TSDB codec benchmarks rely on synthetic patterns (constants, monotonic sequences, random values) that do not reflect real metrics behavior. This PR adds benchmarks with realistic data patterns to better evaluate compression efficiency and encoding throughput.

Real metrics exhibit characteristic shapes: timestamps advance at near-constant intervals, counters increase and occasionally reset, gauges fluctuate around a baseline, and error metrics are mostly zero with rare spikes. To model this, the PR introduces six data generators:

  • TimestampLike: near-constant deltas simulating TSDB timestamps, parameterized by jitterProbability to vary regularity
  • CounterWithResets: monotonically increasing values with occasional resets, parameterized by resetProbability
  • GaugeLike: bounded random walk around a baseline, parameterized by varianceRatio
  • NearConstant: mostly identical values with rare outliers, parameterized by outlierProbability
  • GcdFriendly: values sharing a common divisor (e.g. ms, KB), parameterized by gcd
  • LowCardinality: small value sets sampled with a Zipf distribution, parameterized by distinctValues and skew

Each supplier exposes getNominalBitsPerValue(), computing the theoretical bit width from the value range (max–min) after offset removal. This allows compression ratios to be compared against a meaningful lower bound rather than a fixed 64-bit baseline.

Encode benchmarks expose throughput() (encoding speed) and compression() (compression efficiency). Decode benchmarks report throughput() only, since compression is an encode-time property.

Unit tests validate determinism, value bounds, and nominal bit calculations for all suppliers.

Running the benchmarks

# Encode throughput
./gradlew :benchmarks:run --args='.*Encode.*(Timestamp|Gcd|NearConstant|Gauge|Counter|LowCardinality).*throughput'

# Encode compression
./gradlew :benchmarks:run --args='.*Encode.*(Timestamp|Gcd|NearConstant|Gauge|Counter|LowCardinality).*compression'

# Decode throughput
./gradlew :benchmarks:run --args='.*Decode.*(Timestamp|Gcd|NearConstant|Gauge|Counter|LowCardinality).*throughput'

salvatore-campagna and others added 17 commits January 7, 2026 18:36
The previous benchmarks only measured the bit-packing step using
DocValuesForUtil. This change switches to TSDBDocValuesEncoder to
measure the full encoding pipeline including delta encoding, offset
removal, GCD compression, and bit packing.

Changes:
- Replace DocValuesForUtil with TSDBDocValuesEncoder
- Add MetricsConfig for setup configuration
- Add CompressionMetrics (@AuxCounters) for benchmark metrics reporting
- Switch to SampleTime mode for latency distribution
- Test at encoding boundaries (1,4,8,9,16,17,24,25,32,33,40,48,56,57,64)
Add comprehensive javadoc documentation to internal benchmark classes:

- CompressionMetrics: document all metrics fields, usage pattern, and
  JMH auxiliary counters integration
- MetricsConfig: document configuration parameters and injection pattern
- AbstractTSDBCodecBenchmark: document template method pattern and
  encoding pipeline stages
- EncodeBenchmark/DecodeBenchmark: add class-level documentation
MetricsConfig uses @State(Scope.Benchmark), meaning a single instance
is shared across all benchmark threads. While JMH ensures @setup
completes before @benchmark methods run, the happens-before relationship
requires volatile to guarantee visibility of writes to reader threads.

Added volatile modifier to all fields with documentation explaining
the JMH lifecycle and why volatile is sufficient (no synchronization
needed since there is no write contention).
…vadoc

Extract the magic number 64 into a named constant EXTRA_METADATA_SIZE
with documentation explaining its purpose: buffer headroom for encoding
metadata written by TSDBDocValuesEncoder during delta, offset, and GCD
compression steps.

Also simplify and align javadoc across EncodeBenchmark and DecodeBenchmark
for consistency, removing redundant details while keeping essential
information about what each class measures.
…arity

Rename getEncodedBytes() to getEncodedSize() across the benchmark API
to better communicate that this method returns a size value (number of
bytes) rather than the bytes themselves.

Also rename getEncodedBytesPerBlock() to getEncodedSizePerBlock() in
MetricsConfig and CompressionMetrics for consistency.
…y null check

We always pass a non-null reference, hence the null check is not needed.
JMH guarantees @teardown(Level.Iteration) runs after benchmark operations
complete. Since recordOperation is called on every operation, config will
always be set before computeMetrics runs.
…er passing

Remove the MetricsConfig intermediary class and simplify the benchmark
architecture by passing values directly to CompressionMetrics.recordOperation().

Changes:
- Delete MetricsConfig.java entirely
- Update CompressionMetrics.recordOperation() to accept blockSize, encodedBytes,
  and nominalBits parameters directly instead of a MetricsConfig object
- Simplify all 8 benchmark classes by removing MetricsConfig from method
  signatures and passing values directly from the benchmark context

This eliminates an unnecessary abstraction layer and makes the data flow
more explicit.
…etrics

Change metric fields from public to private and expose them via public
getter methods for better encapsulation. JMH @AuxCounters supports both
public fields and public getters for metric discovery.
Remove throughput metrics that only report raw iteration totals. These
cannot be converted to per-operation metrics without breaking the
compression efficiency metrics. Keep only the meaningful compression
metrics: encodedBytesPerValue, compressionRatio, encodedBitsPerValue,
and overheadRatio.
Add ThroughputMetrics class using JMH @AuxCounters with Type.OPERATIONS
to track bytes/s and values/s throughput rates.

Update all TSDB codec benchmarks to use Mode.Throughput for compatibility
with both metric types.

Encode benchmarks provide two methods:
- throughput() reports encodedBytes and valuesProcessed rates
- compression() reports compressionRatio, encodedBitsPerValue, etc.

Decode benchmarks provide only throughput() since compression metrics
are a property of the encoded data, not the decoding process.

This allows running benchmarks selectively:
- ./gradlew :benchmarks:jmh -Pjmh.includes='.*Encode.*throughput'
- ./gradlew :benchmarks:jmh -Pjmh.includes='.*Encode.*compression'
- ./gradlew :benchmarks:jmh -Pjmh.includes='.*Decode.*throughput'
@salvatore-campagna salvatore-campagna force-pushed the benchmarks/tsdb-codec-realistic-data-patterns branch 2 times, most recently from 98570e3 to 628c57e Compare January 8, 2026 18:06
salvatore-campagna and others added 5 commits January 8, 2026 20:56
The TSDB encoder mutates the input array in-place during encoding
(subtracting min offset, dividing by GCD, computing deltas). This
caused incorrect compression metrics because after the first encoding,
subsequent operations encoded already-zeroed data.

Changes:
- Store original input array and restore via System.arraycopy
- Make EncodeBenchmark and DecodeBenchmark final classes since they
  use composition pattern and are not designed for inheritance
…ration

Add method-level @WarmUp(iterations=0) and @measurement(iterations=1)
to compression benchmarks. Compression metrics are deterministic since
the same input data always produces the same encoded size, unlike
throughput measurements which vary due to JIT compilation and CPU state.
@salvatore-campagna salvatore-campagna force-pushed the benchmarks/tsdb-codec-realistic-data-patterns branch from f86ae7c to 4c411be Compare January 9, 2026 10:52
Change benchmark setup from Level.Iteration to Level.Trial since the input
data is deterministic (fixed seed) and does not need to be regenerated before
each iteration. This reduces setup overhead while maintaining correct behavior.

The setupInvocation() method continues to restore the input array via
System.arraycopy before each benchmark invocation.
@salvatore-campagna salvatore-campagna force-pushed the benchmarks/tsdb-codec-realistic-data-patterns branch from 4c411be to be30ee0 Compare January 9, 2026 11:22
salvatore-campagna and others added 5 commits January 9, 2026 15:40
Java shift semantics cause 1L << 64 to wrap to 1 instead of producing
a 64-bit range. This made bitsPerValue=64 benchmarks generate only
zeros, skewing results.

Use unbounded nextLong() for 64-bit values to get proper full-range
random numbers.
Add six new benchmark classes for realistic time series data patterns:
- TimestampLike: monotonically increasing with jitter
- GaugeLike: oscillating values around a baseline
- CounterWithResets: monotonic counters that periodically reset to zero
- NearConstant: constant values with occasional outliers
- LowCardinality: few distinct values with Zipf distribution
- GcdFriendly: values that are multiples of a common divisor

Each pattern includes both encode and decode benchmarks with configurable
parameters to test codec behavior across different data characteristics.
Add method-level @WarmUp(iterations = 0) and @measurement(iterations = 1)
annotations to compression benchmark methods in the realistic pattern
encode benchmarks.

Compression metrics are deterministic: the same input data always produces
the same encoded size. Unlike throughput measurements which vary due to
JIT compilation and CPU state, compression ratios are constant across runs.
@salvatore-campagna salvatore-campagna force-pushed the benchmarks/tsdb-codec-realistic-data-patterns branch from ace6fd5 to 0da20cb Compare January 9, 2026 16:25
@salvatore-campagna salvatore-campagna marked this pull request as ready for review January 13, 2026 12:51
@elasticsearchmachine elasticsearchmachine added the needs:triage Requires assignment of a team area label label Jan 13, 2026
@salvatore-campagna salvatore-campagna added >non-issue :StorageEngine/TSDB You know, for Metrics and removed needs:triage Requires assignment of a team area label labels Jan 13, 2026
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-storage-engine (Team:StorageEngine)

Copy link
Member

@martijnvg martijnvg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@salvatore-campagna salvatore-campagna merged commit 59f3a6e into elastic:main Jan 14, 2026
35 checks passed
spinscale pushed a commit to spinscale/elasticsearch that referenced this pull request Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants