diff --git a/sdk/include/opentelemetry/sdk/metrics/aggregation/base2_exponential_histogram_indexer.h b/sdk/include/opentelemetry/sdk/metrics/aggregation/base2_exponential_histogram_indexer.h new file mode 100644 index 0000000000..14c00070b1 --- /dev/null +++ b/sdk/include/opentelemetry/sdk/metrics/aggregation/base2_exponential_histogram_indexer.h @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "opentelemetry/version.h" + +#include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace metrics +{ + +/* + * An indexer for base2 exponential histograms. It is used to calculate index for a given value and + * scale. + */ +class Base2ExponentialHistogramIndexer +{ +public: + /* + * Construct a new indexer for a given scale. + */ + explicit Base2ExponentialHistogramIndexer(int32_t scale = 0); + Base2ExponentialHistogramIndexer(const Base2ExponentialHistogramIndexer &other) = default; + Base2ExponentialHistogramIndexer &operator=(const Base2ExponentialHistogramIndexer &other) = + default; + + /** + * Compute the index for the given value. + * + * @param value Measured value (must be non-zero). + * @return the index of the bucket which the value maps to. + */ + int32_t ComputeIndex(double value) const; + +private: + int32_t scale_; + double scale_factor_; +}; + +} // namespace metrics +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/src/metrics/CMakeLists.txt b/sdk/src/metrics/CMakeLists.txt index db8283fc43..cd831eaac8 100644 --- a/sdk/src/metrics/CMakeLists.txt +++ b/sdk/src/metrics/CMakeLists.txt @@ -14,6 +14,7 @@ add_library( state/observable_registry.cc state/sync_metric_storage.cc state/temporal_metric_storage.cc + aggregation/base2_exponential_histogram_indexer.cc aggregation/histogram_aggregation.cc aggregation/lastvalue_aggregation.cc aggregation/sum_aggregation.cc diff --git a/sdk/src/metrics/aggregation/base2_exponential_histogram_indexer.cc b/sdk/src/metrics/aggregation/base2_exponential_histogram_indexer.cc new file mode 100644 index 0000000000..a46a2775e5 --- /dev/null +++ b/sdk/src/metrics/aggregation/base2_exponential_histogram_indexer.cc @@ -0,0 +1,71 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/sdk/metrics/aggregation/base2_exponential_histogram_indexer.h" + +#include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace metrics +{ + +namespace +{ + +const double kLogBase2E = 1.0 / std::log(2.0); + +double ComputeScaleFactor(int32_t scale) +{ + return std::scalbn(kLogBase2E, scale); +} + +// Compute the bucket index using a logarithm based approach. +int32_t GetIndexByLogarithm(double value, double scale_factor) +{ + return static_cast(std::ceil(std::log(value) * scale_factor)) - 1; +} + +// Compute the bucket index at scale 0. +int32_t MapToIndexScaleZero(double value) +{ + // Note: std::frexp() rounds submnormal values to the smallest normal value and returns an + // exponent corresponding to fractions in the range [0.5, 1), whereas an exponent for the range + // [1, 2), so subtract 1 from the exponent immediately. + int exp; + double frac = std::frexp(value, &exp); + exp--; + + if (frac == 0.5) + { + // Special case for powers of two: they fall into the bucket numbered one less. + exp--; + } + return exp; +} + +} // namespace + +Base2ExponentialHistogramIndexer::Base2ExponentialHistogramIndexer(int32_t scale) + : scale_(scale), scale_factor_(scale > 0 ? ComputeScaleFactor(scale) : 0) +{} + +int32_t Base2ExponentialHistogramIndexer::ComputeIndex(double value) const +{ + const double abs_value = std::fabs(value); + // For positive scales, compute the index by logarithm, which is simpler but may be + // inaccurate near bucket boundaries + if (scale_ > 0) + { + return GetIndexByLogarithm(abs_value, scale_factor_); + } + // For scale zero, compute the exact index by extracting the exponent. + // For negative scales, compute the exact index by extracting the exponent and shifting it to + // the right by -scale + return MapToIndexScaleZero(abs_value) >> -scale_; +} + +} // namespace metrics +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/test/metrics/BUILD b/sdk/test/metrics/BUILD index df56f9c67d..24b426fdf5 100644 --- a/sdk/test/metrics/BUILD +++ b/sdk/test/metrics/BUILD @@ -285,6 +285,21 @@ cc_test( ], ) +cc_test( + name = "base2_exponential_histogram_indexer_test", + srcs = [ + "base2_exponential_histogram_indexer_test.cc", + ], + tags = [ + "metrics", + "test", + ], + deps = [ + "metrics_common_test_utils", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "histogram_aggregation_test", srcs = [ diff --git a/sdk/test/metrics/CMakeLists.txt b/sdk/test/metrics/CMakeLists.txt index 47598fbbb8..8e60e3b342 100644 --- a/sdk/test/metrics/CMakeLists.txt +++ b/sdk/test/metrics/CMakeLists.txt @@ -14,6 +14,7 @@ foreach( histogram_aggregation_test attributes_processor_test attributes_hashmap_test + base2_exponential_histogram_indexer_test circular_buffer_counter_test histogram_test sync_metric_storage_counter_test diff --git a/sdk/test/metrics/base2_exponential_histogram_indexer_test.cc b/sdk/test/metrics/base2_exponential_histogram_indexer_test.cc new file mode 100644 index 0000000000..85f6fe72e9 --- /dev/null +++ b/sdk/test/metrics/base2_exponential_histogram_indexer_test.cc @@ -0,0 +1,178 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/sdk/metrics/aggregation/base2_exponential_histogram_indexer.h" + +#include + +using namespace opentelemetry::sdk::metrics; + +TEST(Base2ExponentialHistogramIndexerTest, ScaleOne) +{ + const Base2ExponentialHistogramIndexer indexer{1}; + auto compute_index = [indexer](double value) { return indexer.ComputeIndex(value); }; + + EXPECT_EQ(compute_index(std::numeric_limits::max()), 2047); + EXPECT_EQ(compute_index(strtod("0x1p1023", nullptr)), 2045); + EXPECT_EQ(compute_index(strtod("0x1.1p1023", nullptr)), 2046); + EXPECT_EQ(compute_index(strtod("0x1p1022", nullptr)), 2043); + EXPECT_EQ(compute_index(strtod("0x1.1p1022", nullptr)), 2044); + EXPECT_EQ(compute_index(strtod("0x1p-1022", nullptr)), -2045); + EXPECT_EQ(compute_index(strtod("0x1.1p-1022", nullptr)), -2044); + EXPECT_EQ(compute_index(strtod("0x1p-1021", nullptr)), -2043); + EXPECT_EQ(compute_index(strtod("0x1.1p-1021", nullptr)), -2042); + EXPECT_EQ(compute_index(std::numeric_limits::min()), -2045); + EXPECT_EQ(compute_index(std::numeric_limits::denorm_min()), -2149); + EXPECT_EQ(compute_index(15.0), 7); + EXPECT_EQ(compute_index(9.0), 6); + EXPECT_EQ(compute_index(7.0), 5); + EXPECT_EQ(compute_index(5.0), 4); + EXPECT_EQ(compute_index(3.0), 3); + EXPECT_EQ(compute_index(2.5), 2); + EXPECT_EQ(compute_index(1.5), 1); + EXPECT_EQ(compute_index(1.2), 0); + EXPECT_EQ(compute_index(1.0), -1); + EXPECT_EQ(compute_index(0.75), -1); + EXPECT_EQ(compute_index(0.55), -2); + EXPECT_EQ(compute_index(0.45), -3); +} + +TEST(Base2ExponentialHistogramIndexerTest, ScaleZero) +{ + const Base2ExponentialHistogramIndexer indexer{0}; + auto compute_index = [indexer](double value) { return indexer.ComputeIndex(value); }; + + // Near +Inf. + // Use constant for exp, as max_exponent constant includes bias. + EXPECT_EQ(compute_index(std::numeric_limits::max()), 1023); + EXPECT_EQ(compute_index(strtod("0x1p1023", nullptr)), 1022); + EXPECT_EQ(compute_index(strtod("0x1.1p1023", nullptr)), 1023); + EXPECT_EQ(compute_index(strtod("0x1p1022", nullptr)), 1021); + EXPECT_EQ(compute_index(strtod("0x1.1p1022", nullptr)), 1022); + // Near 0. + EXPECT_EQ(compute_index(strtod("0x1p-1022", nullptr)), -1023); + EXPECT_EQ(compute_index(strtod("0x1.1p-1022", nullptr)), -1022); + EXPECT_EQ(compute_index(strtod("0x1p-1021", nullptr)), -1022); + EXPECT_EQ(compute_index(strtod("0x1.1p-1021", nullptr)), -1021); + EXPECT_EQ(compute_index(std::numeric_limits::min()), -1023); + EXPECT_EQ(compute_index(std::numeric_limits::denorm_min()), -1075); + // Near 1. + EXPECT_EQ(compute_index(4.0), 1); + EXPECT_EQ(compute_index(3.0), 1); + EXPECT_EQ(compute_index(2.0), 0); + EXPECT_EQ(compute_index(1.5), 0); + EXPECT_EQ(compute_index(1.0), -1); + EXPECT_EQ(compute_index(0.75), -1); + EXPECT_EQ(compute_index(0.51), -1); + EXPECT_EQ(compute_index(0.5), -2); + EXPECT_EQ(compute_index(0.26), -2); + EXPECT_EQ(compute_index(0.25), -3); + EXPECT_EQ(compute_index(0.126), -3); + EXPECT_EQ(compute_index(0.125), -4); +} + +TEST(Base2ExponentialHistogramIndexerTest, ScaleNegativeOne) +{ + const Base2ExponentialHistogramIndexer indexer{-1}; + auto compute_index = [indexer](double value) { return indexer.ComputeIndex(value); }; + + EXPECT_EQ(compute_index(17.0), 2); + EXPECT_EQ(compute_index(16.0), 1); + EXPECT_EQ(compute_index(15.0), 1); + EXPECT_EQ(compute_index(9.0), 1); + EXPECT_EQ(compute_index(8.0), 1); + EXPECT_EQ(compute_index(5.0), 1); + EXPECT_EQ(compute_index(4.0), 0); + EXPECT_EQ(compute_index(3.0), 0); + EXPECT_EQ(compute_index(2.0), 0); + EXPECT_EQ(compute_index(1.5), 0); + EXPECT_EQ(compute_index(1.0), -1); + EXPECT_EQ(compute_index(0.75), -1); + EXPECT_EQ(compute_index(0.5), -1); + EXPECT_EQ(compute_index(0.25), -2); + EXPECT_EQ(compute_index(0.20), -2); + EXPECT_EQ(compute_index(0.13), -2); + EXPECT_EQ(compute_index(0.125), -2); + EXPECT_EQ(compute_index(0.10), -2); + EXPECT_EQ(compute_index(0.0625), -3); + EXPECT_EQ(compute_index(0.06), -3); +} + +TEST(Base2ExponentialHistogramIndexerTest, ScaleNegativeFour) +{ + const Base2ExponentialHistogramIndexer indexer{-4}; + auto compute_index = [indexer](double value) { return indexer.ComputeIndex(value); }; + + EXPECT_EQ(compute_index(strtod("0x1p0", nullptr)), -1); + EXPECT_EQ(compute_index(strtod("0x10p0", nullptr)), 0); + EXPECT_EQ(compute_index(strtod("0x100p0", nullptr)), 0); + EXPECT_EQ(compute_index(strtod("0x1000p0", nullptr)), 0); + EXPECT_EQ(compute_index(strtod("0x10000p0", nullptr)), 0); // Base == 2**16 + EXPECT_EQ(compute_index(strtod("0x100000p0", nullptr)), 1); + EXPECT_EQ(compute_index(strtod("0x1000000p0", nullptr)), 1); + EXPECT_EQ(compute_index(strtod("0x10000000p0", nullptr)), 1); + EXPECT_EQ(compute_index(strtod("0x100000000p0", nullptr)), 1); // == 2**32 + EXPECT_EQ(compute_index(strtod("0x1000000000p0", nullptr)), 2); + EXPECT_EQ(compute_index(strtod("0x10000000000p0", nullptr)), 2); + EXPECT_EQ(compute_index(strtod("0x100000000000p0", nullptr)), 2); + EXPECT_EQ(compute_index(strtod("0x1000000000000p0", nullptr)), 2); // 2**48 + EXPECT_EQ(compute_index(strtod("0x10000000000000p0", nullptr)), 3); + EXPECT_EQ(compute_index(strtod("0x100000000000000p0", nullptr)), 3); + EXPECT_EQ(compute_index(strtod("0x1000000000000000p0", nullptr)), 3); + EXPECT_EQ(compute_index(strtod("0x10000000000000000p0", nullptr)), 3); // 2**64 + EXPECT_EQ(compute_index(strtod("0x100000000000000000p0", nullptr)), 4); + EXPECT_EQ(compute_index(strtod("0x1000000000000000000p0", nullptr)), 4); + EXPECT_EQ(compute_index(strtod("0x10000000000000000000p0", nullptr)), 4); + EXPECT_EQ(compute_index(strtod("0x100000000000000000000p0", nullptr)), 4); + EXPECT_EQ(compute_index(strtod("0x1000000000000000000000p0", nullptr)), 5); + EXPECT_EQ(compute_index(1 / strtod("0x1p0", nullptr)), -1); + EXPECT_EQ(compute_index(1 / strtod("0x10p0", nullptr)), -1); + EXPECT_EQ(compute_index(1 / strtod("0x100p0", nullptr)), -1); + EXPECT_EQ(compute_index(1 / strtod("0x1000p0", nullptr)), -1); + EXPECT_EQ(compute_index(1 / strtod("0x10000p0", nullptr)), -2); // 2**-16 + EXPECT_EQ(compute_index(1 / strtod("0x100000p0", nullptr)), -2); + EXPECT_EQ(compute_index(1 / strtod("0x1000000p0", nullptr)), -2); + EXPECT_EQ(compute_index(1 / strtod("0x10000000p0", nullptr)), -2); + EXPECT_EQ(compute_index(1 / strtod("0x100000000p0", nullptr)), -3); // 2**-32 + EXPECT_EQ(compute_index(1 / strtod("0x1000000000p0", nullptr)), -3); + EXPECT_EQ(compute_index(1 / strtod("0x10000000000p0", nullptr)), -3); + EXPECT_EQ(compute_index(1 / strtod("0x100000000000p0", nullptr)), -3); + EXPECT_EQ(compute_index(1 / strtod("0x1000000000000p0", nullptr)), -4); // 2**-48 + EXPECT_EQ(compute_index(1 / strtod("0x10000000000000p0", nullptr)), -4); + EXPECT_EQ(compute_index(1 / strtod("0x100000000000000p0", nullptr)), -4); + EXPECT_EQ(compute_index(1 / strtod("0x1000000000000000p0", nullptr)), -4); + EXPECT_EQ(compute_index(1 / strtod("0x10000000000000000p0", nullptr)), -5); // 2**-64 + EXPECT_EQ(compute_index(1 / strtod("0x100000000000000000p0", nullptr)), -5); + // Max values. + EXPECT_EQ(compute_index(0x1.FFFFFFFFFFFFFp1023), 63); + EXPECT_EQ(compute_index(strtod("0x1p1023", nullptr)), 63); + EXPECT_EQ(compute_index(strtod("0x1p1019", nullptr)), 63); + EXPECT_EQ(compute_index(strtod("0x1p1009", nullptr)), 63); + EXPECT_EQ(compute_index(strtod("0x1p1008", nullptr)), 62); + EXPECT_EQ(compute_index(strtod("0x1p1007", nullptr)), 62); + EXPECT_EQ(compute_index(strtod("0x1p1000", nullptr)), 62); + EXPECT_EQ(compute_index(strtod("0x1p0993", nullptr)), 62); + EXPECT_EQ(compute_index(strtod("0x1p0992", nullptr)), 61); + EXPECT_EQ(compute_index(strtod("0x1p0991", nullptr)), 61); + // Min and subnormal values. + EXPECT_EQ(compute_index(strtod("0x1p-1074", nullptr)), -68); + EXPECT_EQ(compute_index(strtod("0x1p-1073", nullptr)), -68); + EXPECT_EQ(compute_index(strtod("0x1p-1072", nullptr)), -68); + EXPECT_EQ(compute_index(strtod("0x1p-1057", nullptr)), -67); + EXPECT_EQ(compute_index(strtod("0x1p-1056", nullptr)), -67); + EXPECT_EQ(compute_index(strtod("0x1p-1041", nullptr)), -66); + EXPECT_EQ(compute_index(strtod("0x1p-1040", nullptr)), -66); + EXPECT_EQ(compute_index(strtod("0x1p-1025", nullptr)), -65); + EXPECT_EQ(compute_index(strtod("0x1p-1024", nullptr)), -65); + EXPECT_EQ(compute_index(strtod("0x1p-1023", nullptr)), -64); + EXPECT_EQ(compute_index(strtod("0x1p-1022", nullptr)), -64); + EXPECT_EQ(compute_index(strtod("0x1p-1009", nullptr)), -64); + EXPECT_EQ(compute_index(strtod("0x1p-1008", nullptr)), -64); + EXPECT_EQ(compute_index(strtod("0x1p-1007", nullptr)), -63); + EXPECT_EQ(compute_index(strtod("0x1p-0993", nullptr)), -63); + EXPECT_EQ(compute_index(strtod("0x1p-0992", nullptr)), -63); + EXPECT_EQ(compute_index(strtod("0x1p-0991", nullptr)), -62); + EXPECT_EQ(compute_index(strtod("0x1p-0977", nullptr)), -62); + EXPECT_EQ(compute_index(strtod("0x1p-0976", nullptr)), -62); + EXPECT_EQ(compute_index(strtod("0x1p-0975", nullptr)), -61); +}