Skip to content

Commit aa89f71

Browse files
Reduce memory usage of Histogram
1 parent 73f8879 commit aa89f71

File tree

4 files changed

+135
-28
lines changed

4 files changed

+135
-28
lines changed

micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogram.java

+54-13
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,32 @@ class FixedBoundaryHistogram {
2727

2828
private final boolean isCumulativeBucketCounts;
2929

30+
/**
31+
* Creates a FixedBoundaryHistogram which tracks the count of values for each bucket
32+
* bound).
33+
* @param buckets - sorted bucket boundaries
34+
* @param isCumulativeBucketCounts - whether the count values should be cumulative
35+
* count of lower buckets and current bucket.
36+
*/
3037
FixedBoundaryHistogram(double[] buckets, boolean isCumulativeBucketCounts) {
3138
this.buckets = buckets;
3239
this.values = new AtomicLongArray(buckets.length);
3340
this.isCumulativeBucketCounts = isCumulativeBucketCounts;
3441
}
3542

36-
long countAtValue(double value) {
37-
int index = Arrays.binarySearch(buckets, value);
43+
double[] getBuckets() {
44+
return this.buckets;
45+
}
46+
47+
/**
48+
* Returns the number of values that was recorded between previous bucket and the
49+
* queried bucket (upper bound inclusive)
50+
* @param bucket - the bucket to find values for
51+
* @return 0 if bucket is not a valid bucket otherwise number of values recorded
52+
* between (index(bucket) - 1, bucket]
53+
*/
54+
private long countAtBucket(double bucket) {
55+
int index = Arrays.binarySearch(buckets, bucket);
3856
if (index < 0)
3957
return 0;
4058
return values.get(index);
@@ -53,18 +71,20 @@ void record(long value) {
5371
}
5472

5573
/**
56-
* The least bucket that is less than or equal to a sample.
74+
* The least bucket that is less than or equal to a valueToRecord. Returns -1, if the
75+
* valueToRecord is greater than the highest bucket.
5776
*/
58-
int leastLessThanOrEqualTo(double key) {
77+
// VisibleForTesting
78+
int leastLessThanOrEqualTo(long valueToRecord) {
5979
int low = 0;
6080
int high = buckets.length - 1;
6181

6282
while (low <= high) {
6383
int mid = (low + high) >>> 1;
64-
double value = buckets[mid];
65-
if (value < key)
84+
double bucket = buckets[mid];
85+
if (bucket < valueToRecord)
6686
low = mid + 1;
67-
else if (value > key)
87+
else if (bucket > valueToRecord)
6888
high = mid - 1;
6989
else
7090
return mid; // exact match
@@ -73,28 +93,49 @@ else if (value > key)
7393
return low < buckets.length ? low : -1;
7494
}
7595

76-
Iterator<CountAtBucket> countsAtValues(Iterator<Double> values) {
96+
Iterator<CountAtBucket> countsAtValues(Iterator<Double> buckets) {
7797
return new Iterator<CountAtBucket>() {
7898
private double cumulativeCount = 0.0;
7999

80100
@Override
81101
public boolean hasNext() {
82-
return values.hasNext();
102+
return buckets.hasNext();
83103
}
84104

85105
@Override
86106
public CountAtBucket next() {
87-
double value = values.next();
88-
double count = countAtValue(value);
107+
double bucket = buckets.next();
108+
double count = countAtBucket(bucket);
89109
if (isCumulativeBucketCounts) {
90110
cumulativeCount += count;
91-
return new CountAtBucket(value, cumulativeCount);
111+
return new CountAtBucket(bucket, cumulativeCount);
92112
}
93113
else {
94-
return new CountAtBucket(value, count);
114+
return new CountAtBucket(bucket, count);
95115
}
96116
}
97117
};
98118
}
99119

120+
/**
121+
* Returns the list of {@link CountAtBucket} for each of the buckets tracked by this
122+
* histogram.
123+
*/
124+
CountAtBucket[] getCountsAtBucket() {
125+
CountAtBucket[] countAtBuckets = new CountAtBucket[this.buckets.length];
126+
long cumulativeCount = 0;
127+
128+
for (int i = 0; i < this.buckets.length; i++) {
129+
final long valueAtCurrentBucket = values.get(i);
130+
if (isCumulativeBucketCounts) {
131+
cumulativeCount += valueAtCurrentBucket;
132+
countAtBuckets[i] = new CountAtBucket(buckets[i], cumulativeCount);
133+
}
134+
else {
135+
countAtBuckets[i] = new CountAtBucket(buckets[i], valueAtCurrentBucket);
136+
}
137+
}
138+
return countAtBuckets;
139+
}
140+
100141
}

micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/StepBucketHistogram.java

+6-14
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
import io.micrometer.core.instrument.config.InvalidConfigurationException;
2020
import io.micrometer.core.instrument.step.StepValue;
2121

22-
import java.util.Arrays;
23-
import java.util.Iterator;
2422
import java.util.NavigableSet;
2523
import java.util.Objects;
2624
import java.util.function.Supplier;
@@ -36,16 +34,14 @@ public class StepBucketHistogram extends StepValue<CountAtBucket[]> implements H
3634

3735
private final FixedBoundaryHistogram fixedBoundaryHistogram;
3836

39-
private final double[] buckets;
40-
4137
public StepBucketHistogram(Clock clock, long stepMillis, DistributionStatisticConfig distributionStatisticConfig,
4238
boolean supportsAggregablePercentiles, boolean isCumulativeBucketCounts) {
4339
super(clock, stepMillis, getEmptyCounts(
4440
getBucketsFromDistributionStatisticConfig(distributionStatisticConfig, supportsAggregablePercentiles)));
4541

46-
this.buckets = getBucketsFromDistributionStatisticConfig(distributionStatisticConfig,
47-
supportsAggregablePercentiles);
48-
this.fixedBoundaryHistogram = new FixedBoundaryHistogram(buckets, isCumulativeBucketCounts);
42+
this.fixedBoundaryHistogram = new FixedBoundaryHistogram(
43+
getBucketsFromDistributionStatisticConfig(distributionStatisticConfig, supportsAggregablePercentiles),
44+
isCumulativeBucketCounts);
4945
}
5046

5147
@Override
@@ -66,13 +62,9 @@ public HistogramSnapshot takeSnapshot(long count, double total, double max) {
6662
@Override
6763
protected Supplier<CountAtBucket[]> valueSupplier() {
6864
return () -> {
69-
CountAtBucket[] countAtBuckets = new CountAtBucket[buckets.length];
65+
CountAtBucket[] countAtBuckets;
7066
synchronized (fixedBoundaryHistogram) {
71-
final Iterator<CountAtBucket> iterator = fixedBoundaryHistogram
72-
.countsAtValues(Arrays.stream(buckets).iterator());
73-
for (int i = 0; i < countAtBuckets.length; i++) {
74-
countAtBuckets[i] = iterator.next();
75-
}
67+
countAtBuckets = fixedBoundaryHistogram.getCountsAtBucket();
7668
fixedBoundaryHistogram.reset();
7769
}
7870
return countAtBuckets;
@@ -81,7 +73,7 @@ protected Supplier<CountAtBucket[]> valueSupplier() {
8173

8274
@Override
8375
protected CountAtBucket[] noValue() {
84-
return getEmptyCounts(buckets);
76+
return getEmptyCounts(this.fixedBoundaryHistogram.getBuckets());
8577
}
8678

8779
private static CountAtBucket[] getEmptyCounts(double[] buckets) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.micrometer.core.instrument.distribution;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.stream.Stream;
6+
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.Arguments;
11+
import org.junit.jupiter.params.provider.MethodSource;
12+
13+
class FixedBoundaryHistogramTest {
14+
15+
private static final double[] BUCKET_BOUNDS = new double[] { 1, 10, 100 };
16+
17+
private FixedBoundaryHistogram fixedBoundaryHistogram;
18+
19+
@BeforeEach
20+
void setup() {
21+
fixedBoundaryHistogram = new FixedBoundaryHistogram(BUCKET_BOUNDS, false);
22+
}
23+
24+
@Test
25+
void testGetBuckets() {
26+
assertThat(fixedBoundaryHistogram.getBuckets()).containsExactly(BUCKET_BOUNDS);
27+
}
28+
29+
@ParameterizedTest
30+
@MethodSource("valuedIndexProvider")
31+
void testLeastLessThanOrEqualTo(long value, int expectedIndex) {
32+
assertThat(fixedBoundaryHistogram.leastLessThanOrEqualTo(value)).isEqualTo(expectedIndex);
33+
}
34+
35+
private static Stream<Arguments> valuedIndexProvider() {
36+
return Stream.of(Arguments.of(0, 0), Arguments.of(1, 0), Arguments.of(2, 1), Arguments.of(5, 1),
37+
Arguments.of(10, 1), Arguments.of(11, 2), Arguments.of(90, 2), Arguments.of(100, 2),
38+
Arguments.of(101, -1), Arguments.of(Long.MAX_VALUE, -1));
39+
}
40+
41+
@Test
42+
void testReset() {
43+
fixedBoundaryHistogram.record(1);
44+
fixedBoundaryHistogram.record(10);
45+
fixedBoundaryHistogram.record(100);
46+
assertThat(fixedBoundaryHistogram.getCountsAtBucket()).allMatch(countAtBucket -> countAtBucket.count() == 1);
47+
fixedBoundaryHistogram.reset();
48+
assertThat(fixedBoundaryHistogram.getCountsAtBucket()).allMatch(countAtBucket -> countAtBucket.count() == 0);
49+
}
50+
51+
@Test
52+
void testCountsAtBucket() {
53+
fixedBoundaryHistogram.record(1);
54+
fixedBoundaryHistogram.record(10);
55+
fixedBoundaryHistogram.record(100);
56+
assertThat(fixedBoundaryHistogram.getCountsAtBucket()).allMatch(countAtBucket -> countAtBucket.count() == 1);
57+
fixedBoundaryHistogram.reset();
58+
assertThat(fixedBoundaryHistogram.getCountsAtBucket()).allMatch(countAtBucket -> countAtBucket.count() == 0);
59+
fixedBoundaryHistogram.record(0);
60+
assertThat(fixedBoundaryHistogram.getCountsAtBucket()).containsExactly(new CountAtBucket(1.0, 1),
61+
new CountAtBucket(10.0, 0), new CountAtBucket(100.0, 0));
62+
}
63+
64+
@Test
65+
void testCumulativeCounts() {
66+
fixedBoundaryHistogram = new FixedBoundaryHistogram(BUCKET_BOUNDS, true);
67+
fixedBoundaryHistogram.record(1);
68+
fixedBoundaryHistogram.record(10);
69+
fixedBoundaryHistogram.record(100);
70+
assertThat(fixedBoundaryHistogram.getCountsAtBucket()).containsExactly(new CountAtBucket(1.0, 1),
71+
new CountAtBucket(10.0, 2), new CountAtBucket(100.0, 3));
72+
}
73+
74+
}

micrometer-core/src/test/java/io/micrometer/core/instrument/distribution/StepHistogramTest.java micrometer-core/src/test/java/io/micrometer/core/instrument/distribution/StepBucketHistogramTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import static org.assertj.core.api.Assertions.assertThat;
2525

26-
class StepHistogramTest {
26+
class StepBucketHistogramTest {
2727

2828
MockClock clock = new MockClock();
2929

0 commit comments

Comments
 (0)