diff --git a/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageGarbageCollectionBenchmark.java b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentGarbageCollectionBenchmark.java
similarity index 77%
rename from sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageGarbageCollectionBenchmark.java
rename to sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentGarbageCollectionBenchmark.java
index b1d845773dd..f9056a6e6e0 100644
--- a/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageGarbageCollectionBenchmark.java
+++ b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentGarbageCollectionBenchmark.java
@@ -7,13 +7,14 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.common.export.MemoryMode;
-import io.opentelemetry.sdk.metrics.Aggregation;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil;
import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter;
+import io.opentelemetry.sdk.metrics.internal.state.TestInstrumentType.InstrumentTester;
+import io.opentelemetry.sdk.metrics.internal.state.TestInstrumentType.TestInstrumentsState;
import java.time.Duration;
import java.util.List;
import java.util.Random;
@@ -33,8 +34,8 @@
import org.openjdk.jmh.annotations.Warmup;
/**
- * Run this through {@link AsynchronousMetricStorageGarbageCollectionBenchmarkTest}, as it runs it
- * embedded with the GC profiler which what this test designed for (No need for command line run)
+ * Run this through {@link InstrumentGarbageCollectionBenchmarkTest}, as it runs it embedded with
+ * the GC profiler which what this test designed for (No need for command line run)
*
*
This test creates 10 asynchronous counters (any asynchronous instrument will do as the code
* path is almost the same for all async instrument types), and 1000 attribute sets. Each time the
@@ -51,37 +52,41 @@
*/
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
-@Measurement(iterations = 20, batchSize = 100)
+@Measurement(iterations = 10, batchSize = 10)
@Warmup(iterations = 10, batchSize = 10)
@Fork(1)
-public class AsynchronousMetricStorageGarbageCollectionBenchmark {
+public class InstrumentGarbageCollectionBenchmark {
@State(value = Scope.Benchmark)
- @SuppressWarnings("SystemOut")
public static class ThreadState {
private final int cardinality;
- private final int countersCount;
+ private final int instrumentCount;
+ @Param public TestInstrumentType testInstrumentType;
@Param public AggregationTemporality aggregationTemporality;
@Param public MemoryMode memoryMode;
SdkMeterProvider sdkMeterProvider;
private final Random random = new Random();
List attributesList;
+ private TestInstrumentsState testInstrumentsState;
+ private InstrumentTester instrumentTester;
/** Creates a ThreadState. */
@SuppressWarnings("unused")
public ThreadState() {
cardinality = 1000;
- countersCount = 10;
+ instrumentCount = 10;
}
@SuppressWarnings("SpellCheckingInspection")
@Setup
public void setup() {
+ instrumentTester = testInstrumentType.createInstrumentTester();
PeriodicMetricReader metricReader =
PeriodicMetricReader.builder(
// Configure an exporter that configures the temporality and aggregation
// for the test case, but otherwise drops the data on export
- new NoopMetricExporter(aggregationTemporality, Aggregation.sum(), memoryMode))
+ new NoopMetricExporter(
+ aggregationTemporality, instrumentTester.testedAggregation(), memoryMode))
// Effectively disable periodic reading so reading is only done on #flush()
.setInterval(Duration.ofSeconds(Integer.MAX_VALUE))
.build();
@@ -95,18 +100,9 @@ public void setup() {
SdkMeterProviderUtil.setExemplarFilter(builder, ExemplarFilter.alwaysOff());
sdkMeterProvider = builder.build();
- for (int i = 0; i < countersCount; i++) {
- sdkMeterProvider
- .get("meter")
- .counterBuilder("counter" + i)
- .buildWithCallback(
- observableLongMeasurement -> {
- for (int j = 0; j < attributesList.size(); j++) {
- Attributes attributes = attributesList.get(j);
- observableLongMeasurement.record(random.nextInt(10_000), attributes);
- }
- });
- }
+ testInstrumentsState =
+ instrumentTester.buildInstruments(
+ instrumentCount, sdkMeterProvider, attributesList, random);
}
@TearDown
@@ -123,6 +119,8 @@ public void tearDown() {
@Benchmark
@Threads(value = 1)
public void recordAndCollect(ThreadState threadState) {
+ threadState.instrumentTester.recordValuesInInstruments(
+ threadState.testInstrumentsState, threadState.attributesList, threadState.random);
threadState.sdkMeterProvider.forceFlush().join(10, TimeUnit.SECONDS);
}
}
diff --git a/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageGarbageCollectionBenchmarkTest.java b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentGarbageCollectionBenchmarkTest.java
similarity index 51%
rename from sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageGarbageCollectionBenchmarkTest.java
rename to sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentGarbageCollectionBenchmarkTest.java
index a5b5f5cc5d2..de1733b0ad8 100644
--- a/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/AsynchronousMetricStorageGarbageCollectionBenchmarkTest.java
+++ b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/InstrumentGarbageCollectionBenchmarkTest.java
@@ -26,17 +26,17 @@
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
-public class AsynchronousMetricStorageGarbageCollectionBenchmarkTest {
+public class InstrumentGarbageCollectionBenchmarkTest {
/**
- * This test validates that in {@link MemoryMode#REUSABLE_DATA}, {@link
- * AsynchronousMetricStorage#collect(Resource, InstrumentationScopeInfo, long, long)} barely
- * allocates memory which is then subsequently garbage collected. It is done so comparatively to
- * {@link MemoryMode#IMMUTABLE_DATA},
+ * This test validates that in {@link MemoryMode#REUSABLE_DATA}, any {@link
+ * MetricStorage#collect(Resource, InstrumentationScopeInfo, long, long)} barely allocates memory
+ * which is then subsequently garbage collected. It is done so comparatively to {@link
+ * MemoryMode#IMMUTABLE_DATA},
*
- * It runs the JMH test {@link AsynchronousMetricStorageGarbageCollectionBenchmark} with GC
- * profiler, and measures for each parameter combination the garbage collector normalized rate
- * (bytes allocated per Operation).
+ *
It runs the JMH test {@link InstrumentGarbageCollectionBenchmark} with GC profiler, and
+ * measures for each parameter combination the garbage collector normalized rate (bytes allocated
+ * per Operation).
*
*
Memory allocations can be hidden even at an innocent foreach loop on a collection, which
* under the hood allocates an internal object O(N) times. Someone can accidentally refactor such
@@ -52,11 +52,11 @@ public void normalizedAllocationRateTest() throws RunnerException {
"true".equals(System.getenv("CI")),
"This test should only run in GitHub CI since it's long");
- // Runs AsynchronousMetricStorageMemoryProfilingBenchmark
+ // Runs InstrumentGarbageCollectionBenchmark
// with garbage collection profiler
Options opt =
new OptionsBuilder()
- .include(AsynchronousMetricStorageGarbageCollectionBenchmark.class.getSimpleName())
+ .include(InstrumentGarbageCollectionBenchmark.class.getSimpleName())
.addProfiler("gc")
.shouldFailOnError(true)
.jvmArgs("-Xmx1500m")
@@ -64,15 +64,17 @@ public void normalizedAllocationRateTest() throws RunnerException {
Collection results = new Runner(opt).run();
// Collect the normalized GC allocation rate per parameters combination
- Map> resultMap = new HashMap<>();
+ Map testInstrumentTypeResultsMap = new HashMap<>();
for (RunResult result : results) {
for (BenchmarkResult benchmarkResult : result.getBenchmarkResults()) {
BenchmarkParams benchmarkParams = benchmarkResult.getParams();
String memoryMode = benchmarkParams.getParam("memoryMode");
String aggregationTemporality = benchmarkParams.getParam("aggregationTemporality");
+ String testInstrumentType = benchmarkParams.getParam("testInstrumentType");
assertThat(memoryMode).isNotNull();
assertThat(aggregationTemporality).isNotNull();
+ assertThat(testInstrumentType).isNotNull();
Map secondaryResults = benchmarkResult.getSecondaryResults();
Result allocRateNorm = secondaryResults.get("gc.alloc.rate.norm");
@@ -80,27 +82,46 @@ public void normalizedAllocationRateTest() throws RunnerException {
.describedAs("Allocation rate in secondary results: %s", secondaryResults)
.isNotNull();
- resultMap
+ testInstrumentTypeResultsMap
+ .computeIfAbsent(testInstrumentType, k -> new TestInstrumentTypeResults())
+ .aggregationTemporalityToMemoryModeResult
.computeIfAbsent(aggregationTemporality, k -> new HashMap<>())
.put(memoryMode, allocRateNorm.getScore());
}
}
- assertThat(resultMap).hasSameSizeAs(AggregationTemporality.values());
+ testInstrumentTypeResultsMap.forEach(
+ (testInstrumentType, testInstrumentTypeResults) -> {
+ Map> resultMap =
+ testInstrumentTypeResults.aggregationTemporalityToMemoryModeResult;
+ assertThat(resultMap).hasSameSizeAs(AggregationTemporality.values());
- // Asserts that reusable data GC allocation rate is a tiny fraction of immutable data
- // GC allocation rate
- resultMap.forEach(
- (aggregationTemporality, memoryModeToAllocRateMap) -> {
- Double immutableDataAllocRate =
- memoryModeToAllocRateMap.get(MemoryMode.IMMUTABLE_DATA.toString());
- Double reusableDataAllocRate =
- memoryModeToAllocRateMap.get(MemoryMode.REUSABLE_DATA.toString());
+ // Asserts that reusable data GC allocation rate is a tiny fraction of immutable data
+ // GC allocation rate
+ resultMap.forEach(
+ (aggregationTemporality, memoryModeToAllocRateMap) -> {
+ Double immutableDataAllocRate =
+ memoryModeToAllocRateMap.get(MemoryMode.IMMUTABLE_DATA.toString());
+ Double reusableDataAllocRate =
+ memoryModeToAllocRateMap.get(MemoryMode.REUSABLE_DATA.toString());
- assertThat(immutableDataAllocRate).isNotNull().isNotZero();
- assertThat(reusableDataAllocRate).isNotNull().isNotZero();
- assertThat(100 - (reusableDataAllocRate / immutableDataAllocRate) * 100)
- .isCloseTo(99.8, Offset.offset(2.0));
+ assertThat(immutableDataAllocRate).isNotNull().isNotZero();
+ assertThat(reusableDataAllocRate).isNotNull().isNotZero();
+
+ // If this test suddenly fails for you this means you have changed the code in a way
+ // that allocates more memory than before. You can find out where, by running
+ // ProfileBenchmark class and looking at the flame graph. Make sure to
+ // set the parameters according to where it failed for.
+ assertThat(100 - (reusableDataAllocRate / immutableDataAllocRate) * 100)
+ .describedAs(
+ "Aggregation temporality = %s, testInstrumentType = %s",
+ aggregationTemporality, testInstrumentType)
+ .isCloseTo(99.8, Offset.offset(2.0));
+ });
});
}
+
+ static class TestInstrumentTypeResults {
+ Map> aggregationTemporalityToMemoryModeResult = new HashMap<>();
+ }
}
diff --git a/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/ProfileBenchmark.java b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/ProfileBenchmark.java
new file mode 100644
index 00000000000..b79fbc36e09
--- /dev/null
+++ b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/ProfileBenchmark.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.metrics.internal.state;
+
+import io.opentelemetry.sdk.common.export.MemoryMode;
+import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
+
+/**
+ * This benchmark class is used to see memory allocation flame graphs for a single run.
+ *
+ * Steps:
+ *
+ *
+ * - Follow download instructions for async-profiler, located at this location
+ *
- Assuming you have extracted it at /tmp/async-profiler-2.9-macos, add the following to your
+ * JVM arguments of your run configuration:
+ *
+ * -agentpath:/tmp/async-profiler-2.9-macos/build/libasyncProfiler.so=start,event=alloc,flamegraph,file=/tmp/profiled_data.html
+ *
+ * - Tune the parameters as you see fit (They are marked below with "Parameters")
+ *
- Run the class (its main function)
+ *
- Open /tmp/profiled_data.html with your browser
+ *
- Use the flame graph to see where the allocations are happening the most and fix
+ *
- Run {@link InstrumentGarbageCollectionBenchmark} and see if it passes now
+ *
- If not, repeat
+ *
+ */
+public class ProfileBenchmark {
+
+ private ProfileBenchmark() {}
+
+ public static void main(String[] args) {
+ // Parameters
+ AggregationTemporality aggregationTemporality = AggregationTemporality.DELTA;
+ MemoryMode memoryMode = MemoryMode.REUSABLE_DATA;
+ TestInstrumentType testInstrumentType = TestInstrumentType.EXPONENTIAL_HISTOGRAM;
+
+ InstrumentGarbageCollectionBenchmark.ThreadState benchmarkSetup =
+ new InstrumentGarbageCollectionBenchmark.ThreadState();
+
+ benchmarkSetup.aggregationTemporality = aggregationTemporality;
+ benchmarkSetup.memoryMode = memoryMode;
+ benchmarkSetup.testInstrumentType = testInstrumentType;
+
+ InstrumentGarbageCollectionBenchmark benchmark = new InstrumentGarbageCollectionBenchmark();
+
+ benchmarkSetup.setup();
+
+ warmup(benchmark, benchmarkSetup);
+
+ // This is divided explicitly to two methods so you can focus on `measure` in the flame graph
+ // when trying to decrease the allocations
+ measure(benchmark, benchmarkSetup);
+ }
+
+ public static void warmup(
+ InstrumentGarbageCollectionBenchmark benchmark,
+ InstrumentGarbageCollectionBenchmark.ThreadState benchmarkSetup) {
+ for (int i = 0; i < 10; i++) {
+ benchmark.recordAndCollect(benchmarkSetup);
+ }
+ }
+
+ public static void measure(
+ InstrumentGarbageCollectionBenchmark benchmark,
+ InstrumentGarbageCollectionBenchmark.ThreadState benchmarkSetup) {
+ for (int i = 0; i < 200; i++) {
+ benchmark.recordAndCollect(benchmarkSetup);
+ }
+ }
+}
diff --git a/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/TestInstrumentType.java b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/TestInstrumentType.java
new file mode 100644
index 00000000000..6514146a6cb
--- /dev/null
+++ b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/TestInstrumentType.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.metrics.internal.state;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.metrics.Aggregation;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.internal.state.tester.AsyncCounterTester;
+import io.opentelemetry.sdk.metrics.internal.state.tester.ExponentialHistogramTester;
+import java.util.List;
+import java.util.Random;
+
+public enum TestInstrumentType {
+ ASYNC_COUNTER() {
+ @Override
+ InstrumentTester createInstrumentTester() {
+ return new AsyncCounterTester();
+ }
+ },
+ EXPONENTIAL_HISTOGRAM() {
+ @Override
+ InstrumentTester createInstrumentTester() {
+ return new ExponentialHistogramTester();
+ }
+ };
+
+ abstract InstrumentTester createInstrumentTester();
+
+ TestInstrumentType() {}
+
+ public interface InstrumentTester {
+ Aggregation testedAggregation();
+
+ TestInstrumentsState buildInstruments(
+ double instrumentCount,
+ SdkMeterProvider sdkMeterProvider,
+ List attributesList,
+ Random random);
+
+ void recordValuesInInstruments(
+ TestInstrumentsState testInstrumentsState, List attributesList, Random random);
+ }
+
+ public interface TestInstrumentsState {}
+
+ public static class EmptyInstrumentsState implements TestInstrumentsState {}
+}
diff --git a/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/tester/AsyncCounterTester.java b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/tester/AsyncCounterTester.java
new file mode 100644
index 00000000000..f926fb343b6
--- /dev/null
+++ b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/tester/AsyncCounterTester.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.metrics.internal.state.tester;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.metrics.Aggregation;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.internal.state.TestInstrumentType;
+import io.opentelemetry.sdk.metrics.internal.state.TestInstrumentType.EmptyInstrumentsState;
+import java.util.List;
+import java.util.Random;
+
+public class AsyncCounterTester implements TestInstrumentType.InstrumentTester {
+ @Override
+ public Aggregation testedAggregation() {
+ return Aggregation.sum();
+ }
+
+ @SuppressWarnings("ForLoopReplaceableByForEach") // This is for GC sensitivity testing: no streams
+ @Override
+ public TestInstrumentType.TestInstrumentsState buildInstruments(
+ double instrumentCount,
+ SdkMeterProvider sdkMeterProvider,
+ List attributesList,
+ Random random) {
+ for (int i = 0; i < instrumentCount; i++) {
+ sdkMeterProvider
+ .get("meter")
+ .counterBuilder("counter" + i)
+ .buildWithCallback(
+ observableLongMeasurement -> {
+ for (int j = 0; j < attributesList.size(); j++) {
+ Attributes attributes = attributesList.get(j);
+ observableLongMeasurement.record(random.nextInt(10_000), attributes);
+ }
+ });
+ }
+ return new EmptyInstrumentsState();
+ }
+
+ @Override
+ public void recordValuesInInstruments(
+ TestInstrumentType.TestInstrumentsState testInstrumentsState,
+ List attributesList,
+ Random random) {
+ // No need, all done via the callbacks
+ }
+}
diff --git a/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/tester/ExponentialHistogramTester.java b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/tester/ExponentialHistogramTester.java
new file mode 100644
index 00000000000..6cca5a35bd4
--- /dev/null
+++ b/sdk/metrics/src/jmhBasedTest/java/io/opentelemetry/sdk/metrics/internal/state/tester/ExponentialHistogramTester.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.metrics.internal.state.tester;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.sdk.metrics.Aggregation;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.internal.state.TestInstrumentType;
+import io.opentelemetry.sdk.metrics.internal.state.TestInstrumentType.InstrumentTester;
+import java.util.List;
+import java.util.Random;
+
+public class ExponentialHistogramTester implements InstrumentTester {
+
+ static class ExponentialHistogramState implements TestInstrumentType.TestInstrumentsState {
+ DoubleHistogram doubleHistogram;
+ }
+
+ private static final int measurementsPerAttributeSet = 1_000;
+
+ @Override
+ public Aggregation testedAggregation() {
+ return Aggregation.base2ExponentialBucketHistogram();
+ }
+
+ @Override
+ public TestInstrumentType.TestInstrumentsState buildInstruments(
+ double instrumentCount,
+ SdkMeterProvider sdkMeterProvider,
+ List attributesList,
+ Random random) {
+ ExponentialHistogramState state = new ExponentialHistogramState();
+ state.doubleHistogram = sdkMeterProvider.get("meter").histogramBuilder("testhistogram").build();
+ return state;
+ }
+
+ @SuppressWarnings("ForLoopReplaceableByForEach") // This is for GC sensitivity testing: no streams
+ @Override
+ public void recordValuesInInstruments(
+ TestInstrumentType.TestInstrumentsState testInstrumentsState,
+ List attributesList,
+ Random random) {
+
+ ExponentialHistogramState state = (ExponentialHistogramState) testInstrumentsState;
+
+ for (int j = 0; j < attributesList.size(); j++) {
+ Attributes attributes = attributesList.get(j);
+ for (int i = 0; i < measurementsPerAttributeSet; i++) {
+ state.doubleHistogram.record(random.nextInt(10_000), attributes);
+ }
+ }
+ }
+}
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/Base2ExponentialHistogramIndexer.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/Base2ExponentialHistogramIndexer.java
index 749cba883ae..a0b1acb52a4 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/Base2ExponentialHistogramIndexer.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/Base2ExponentialHistogramIndexer.java
@@ -43,7 +43,7 @@ private Base2ExponentialHistogramIndexer(int scale) {
/** Get an indexer for the given scale. Indexers are cached and reused for performance. */
static Base2ExponentialHistogramIndexer get(int scale) {
- return cache.computeIfAbsent(scale, unused -> new Base2ExponentialHistogramIndexer(scale));
+ return cache.computeIfAbsent(scale, Base2ExponentialHistogramIndexer::new);
}
/**