From 41e8876da5a2edc1efd9f006b15d8f9c038b0029 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Tue, 23 Sep 2025 17:08:44 +0200 Subject: [PATCH 1/2] Fix HdrHistogram range issues by computing ratio from min/max expected values The fix computes an appropriate `highestToLowestValueRatio` from the configured `minimumExpectedValue` and `maximumExpectedValue` in `DistributionStatisticConfig`, allowing HdrHistogram to cover the expected range without constant resizing. ## Problem The current implementation hardcodes a ratio of 2: ```java new DoubleHistogram(percentilePrecision(distributionStatisticConfig)); // This constructor internally calls: this(2, numberOfSignificantValueDigits, ...) ``` This means the histogram can only handle values that differ by a factor of 2 (e.g., 1ms to 2ms). When recording typical operation times that span microseconds to seconds, HdrHistogram constantly attempts to resize, frequently throwing `ArrayIndexOutOfBoundsException` during the resize operations. In production environments, this can result in hundreds of thousands of exceptions being thrown, causing significant performance overhead. ## Solution This PR modifies `TimeWindowPercentileHistogram` to: 1. **Compute the ratio from configuration**: When `minimumExpectedValue` and `maximumExpectedValue` are provided, calculate an appropriate ratio 2. **Use the computed ratio**: Pass this ratio to HdrHistogram constructors instead of relying on the default 3. **Maintain backward compatibility**: When min/max values are not configured, fall back to the original behavior (ratio=2) ## Performance Impact ### Before - Frequent `ArrayIndexOutOfBoundsException` as values exceed the narrow range - Initial `HdrHistogram#counts` has length of 127 ### After - No exceptions when values are within the configured min/max range - Initial `HdrHistogram#counts` has length of 272 when using defaults (1ms - 30s) This last bullet could be a problem and require a different solution. ## Issues - Fixes #4327 Signed-off-by: Knut Wannheden --- .../TimeWindowPercentileHistogram.java | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/TimeWindowPercentileHistogram.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/TimeWindowPercentileHistogram.java index 3e73b4867d..e5edc751d1 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/TimeWindowPercentileHistogram.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/TimeWindowPercentileHistogram.java @@ -64,7 +64,10 @@ public TimeWindowPercentileHistogram(Clock clock, DistributionStatisticConfig di protected TimeWindowPercentileHistogram(Clock clock, DistributionStatisticConfig distributionStatisticConfig, boolean supportsAggregablePercentiles, boolean isCumulativeBucketCounts, boolean includeInfinityBucket) { super(clock, distributionStatisticConfig, DoubleRecorder.class); - intervalHistogram = new DoubleHistogram(percentilePrecision(distributionStatisticConfig)); + intervalHistogram = new DoubleHistogram(computeHighestToLowestValueRatio(distributionStatisticConfig), + percentilePrecision(distributionStatisticConfig)); + intervalHistogram.setAutoResize(true); + this.isCumulativeBucketCounts = isCumulativeBucketCounts; Set monitoredBuckets = distributionStatisticConfig.getHistogramBuckets(supportsAggregablePercentiles); @@ -80,7 +83,8 @@ protected TimeWindowPercentileHistogram(Clock clock, DistributionStatisticConfig @Override DoubleRecorder newBucket() { - return new DoubleRecorder(percentilePrecision(distributionStatisticConfig)); + return new DoubleRecorder(computeHighestToLowestValueRatio(distributionStatisticConfig), + percentilePrecision(distributionStatisticConfig)); } @Override @@ -100,7 +104,8 @@ void resetBucket(DoubleRecorder bucket) { @Override DoubleHistogram newAccumulatedHistogram(DoubleRecorder[] ringBuffer) { - return new DoubleHistogram(percentilePrecision(distributionStatisticConfig)); + return new DoubleHistogram(computeHighestToLowestValueRatio(distributionStatisticConfig), + percentilePrecision(distributionStatisticConfig)); } @Override @@ -143,6 +148,27 @@ private int percentilePrecision(DistributionStatisticConfig config) { return config.getPercentilePrecision() == null ? 1 : config.getPercentilePrecision(); } + /** + * Compute the highestToLowestValueRatio based on the configured min/max expected values. + * This allows HdrHistogram to cover the expected range without frequent resizing. + * + * @param config The distribution statistic configuration + * @return The computed ratio, or 2 if not enough information is available + */ + private long computeHighestToLowestValueRatio(DistributionStatisticConfig config) { + Double min = config.getMinimumExpectedValueAsDouble(); + Double max = config.getMaximumExpectedValueAsDouble(); + + // Only compute ratio if both min and max are explicitly set and finite + if (min != null && max != null && min > 0 && max > min && !Double.isInfinite(max)) { + // Compute the ratio, ensuring it's at least 2 + long ratio = (long) Math.ceil(max / min); + return Math.max(ratio, 2L); + } + + return 2L; + } + @Override void outputSummary(PrintStream out, double bucketScaling) { accumulatedHistogram().outputPercentileDistribution(out, bucketScaling); From 983bcd6c9ea96c5cf01742c6e5dd55d298d19b3f Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Wed, 24 Sep 2025 14:50:54 +0200 Subject: [PATCH 2/2] Apply code formatter --- .../distribution/TimeWindowPercentileHistogram.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/TimeWindowPercentileHistogram.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/TimeWindowPercentileHistogram.java index e5edc751d1..3ddb5f0d8a 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/TimeWindowPercentileHistogram.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/TimeWindowPercentileHistogram.java @@ -65,7 +65,7 @@ protected TimeWindowPercentileHistogram(Clock clock, DistributionStatisticConfig boolean supportsAggregablePercentiles, boolean isCumulativeBucketCounts, boolean includeInfinityBucket) { super(clock, distributionStatisticConfig, DoubleRecorder.class); intervalHistogram = new DoubleHistogram(computeHighestToLowestValueRatio(distributionStatisticConfig), - percentilePrecision(distributionStatisticConfig)); + percentilePrecision(distributionStatisticConfig)); intervalHistogram.setAutoResize(true); this.isCumulativeBucketCounts = isCumulativeBucketCounts; @@ -84,7 +84,7 @@ protected TimeWindowPercentileHistogram(Clock clock, DistributionStatisticConfig @Override DoubleRecorder newBucket() { return new DoubleRecorder(computeHighestToLowestValueRatio(distributionStatisticConfig), - percentilePrecision(distributionStatisticConfig)); + percentilePrecision(distributionStatisticConfig)); } @Override @@ -105,7 +105,7 @@ void resetBucket(DoubleRecorder bucket) { @Override DoubleHistogram newAccumulatedHistogram(DoubleRecorder[] ringBuffer) { return new DoubleHistogram(computeHighestToLowestValueRatio(distributionStatisticConfig), - percentilePrecision(distributionStatisticConfig)); + percentilePrecision(distributionStatisticConfig)); } @Override @@ -149,9 +149,9 @@ private int percentilePrecision(DistributionStatisticConfig config) { } /** - * Compute the highestToLowestValueRatio based on the configured min/max expected values. - * This allows HdrHistogram to cover the expected range without frequent resizing. - * + * Compute the highestToLowestValueRatio based on the configured min/max expected + * values. This allows HdrHistogram to cover the expected range without frequent + * resizing. * @param config The distribution statistic configuration * @return The computed ratio, or 2 if not enough information is available */