From 7ce50bb05eea60f46a83068e3b48bf1c3c58824c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 9 Aug 2022 19:15:06 +0200 Subject: [PATCH 01/24] Add exponent and logarithm mappings Fixes #2957 --- CHANGELOG.md | 2 + .../exponential_histogram/mapping/__init__.py | 85 ++++ .../exponential_histogram/mapping/errors.py | 26 ++ .../mapping/exponent_mapping.py | 125 ++++++ .../exponential_histogram/mapping/ieee_754.py | 181 +++++++++ .../mapping/logarithm_mapping.py | 140 +++++++ .../test_exponent_mapping.py | 378 ++++++++++++++++++ .../exponential_histogram/test_ieee_754.py | 92 +++++ .../test_logarithm_mapping.py | 226 +++++++++++ 9 files changed, 1255 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py create mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py create mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_ieee_754.py create mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b04c50742..27a8b61a8f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0...HEAD) +- Add logarithm and exponent mappings + ([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960)) - Add and use missing metrics environment variables ([#2968](https://github.com/open-telemetry/opentelemetry-python/pull/2968)) - Enabled custom samplers via entry points diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py new file mode 100644 index 00000000000..7ab61ac8d67 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -0,0 +1,85 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod + + +class Mapping(ABC): + + # pylint: disable=no-member + def __new__(cls, scale: int): + + with cls._mappings_lock: + if scale not in cls._mappings: + cls._mappings[scale] = super().__new__(cls) + + return cls._mappings[scale] + + def __init__(self, scale: int) -> None: + + if scale > self._max_scale: + raise Exception(f"scale is larger than {self._max_scale}") + + if scale < self._min_scale: + raise Exception(f"scale is smaller than {self._min_scale}") + + # The size of the exponential histogram buckets is determined by a + # parameter known as scale, larger values of scale will produce smaller + # buckets. Bucket boundaries of the exponential histogram are located + # at integer powers of the base, where: + + # base = 2 ** (2 ** (-scale)) + self._scale = scale + + @property + @abstractmethod + def _min_scale(self) -> int: + """ + Return the smallest possible value for the mapping scale + """ + + @property + @abstractmethod + def _max_scale(self) -> int: + """ + Return the largest possible value for the mapping scale + """ + + @abstractmethod + def map_to_index(self, value: float) -> int: + """ + Maps positive floating point values to indexes corresponding to + `Mapping.scale`. Implementations are not expected to handle zeros, + +inf, NaN, or negative values. + """ + + @abstractmethod + def get_lower_boundary(self, index: int) -> float: + """ + Returns the lower boundary of a given bucket index. The index is + expected to map onto a range that is at least partially inside the + range of normalized floating point values. If the corresponding + bucket's upper boundary is less than or equal to 2 ** -1022, + `UnderflowError` will be raised. If the corresponding bucket's lower + boundary is greater than `sys.float_info.max`, `OverflowError` will be + raised. + """ + + @property + @abstractmethod + def scale(self) -> int: + """ + Returns the parameter that controls the resolution of this mapping. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale + """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py new file mode 100644 index 00000000000..54edb5fcd0b --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class MappingUnderflowError(Exception): + """ + Raised when computing the lower boundary of an index that maps into a + denormalized floating point value. + """ + + +class MappingOverflowError(Exception): + """ + Raised when computing the lower boundary of an index that maps into +inf. + """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py new file mode 100644 index 00000000000..54eabcfd88e --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -0,0 +1,125 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import ldexp +from threading import Lock + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( + Mapping, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( + MappingOverflowError, + MappingUnderflowError, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MAX_NORMAL_EXPONENT, + MIN_NORMAL_EXPONENT, + MIN_NORMAL_VALUE, + SIGNIFICAND_WIDTH, + get_ieee_754_exponent, + get_ieee_754_significand, +) + + +class ExponentMapping(Mapping): + + _mappings = {} + _mappings_lock = Lock() + # _min_scale defines the point at which the exponential mapping function + # becomes useless for 64-bit floats. With scale -10, ignoring subnormal + # values, bucket indices range from -1 to 1. + _min_scale = -10 + # _max_scale is the largest scale supported by exponential mapping. Use + # a logarithm mapping for larger scales. + _max_scale = 0 + + def __init__(self, scale: int): + super().__init__(scale) + + # self._min_normal_lower_boundary_index is the index such that + # base ** index <= MIN_NORMAL_VALUE. An exponential histogram bucket + # with this index covers the range (base ** index, base (index + 1)], + # including MIN_NORMAL_VALUE. + index = MIN_NORMAL_EXPONENT >> -self._scale + + if -self._scale < 2: + index -= 1 + + self._min_normal_lower_boundary_index = index + + # self._max_normal_lower_boundary_index is the index such that + # base**index equals the greatest representable lower boundary. An + # exponential histogram bucket with this index covers the range + # ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. + # metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. + # This bucket is incomplete, since the upper boundary cannot be + # represented. One greater than this index corresponds with the bucket + # containing values > 2 ** 1024. + self._max_normal_lower_boundary_index = ( + MAX_NORMAL_EXPONENT >> -self._scale + ) + + def map_to_index(self, value: float) -> int: + if value < MIN_NORMAL_VALUE: + return self._min_normal_lower_boundary_index + + exponent = get_ieee_754_exponent(value) + + # Positive integers are represented in binary as having an infinite + # amount of leading zeroes, for example 2 is represented as ...00010. + + # A negative integer -x is represented in binary as the complement of + # (x - 1). For example, -4 is represented as the complement of 4 - 1 + # == 3. 3 is represented as ...00011. Its compliment is ...11100, the + # binary representation of -4. + + # get_ieee_754_significand(value) gets the positive integer made up + # from the rightmost SIGNIFICAND_WIDTH bits (the mantissa) of the IEEE + # 754 representation of value. If value is an exact power of 2, all + # these SIGNIFICAND_WIDTH bits would be all zeroes, and when 1 is + # subtracted the resulting value is -1. The binary representation of + # -1 is ...111, so when these bits are right shifted SIGNIFICAND_WIDTH + # places, the resulting value for correction is -1. If value is not an + # exact power of 2, at least one of the rightmost SIGNIFICAND_WIDTH + # bits would be 1 (even for values whose decimal part is 0, like 5.0 + # since the IEEE 754 of such number is too the product of a power of 2 + # (defined in the exponent part of the IEEE 754 representation) and the + # value defined in the mantissa). Having at least one of the rightmost + # SIGNIFICAND_WIDTH bit being 1 means that get_ieee_754(value) will + # always be greater or equal to 1, and when 1 is subtracted, the + # result will be greater or equal to 0, whose representation in binary + # will be of at most SIGNIFICAND_WIDTH ones that have an infinite + # amount of leading zeroes. When those SIGNIFICAND_WIDTH bits are + # shifted to the right SIGNIFICAND_WIDTH places, the resulting value + # will be 0. + + # In summary, correction will be -1 if value is a power of 2, 0 if not. + + # FIXME Document why we can assume value will not be 0, inf, or NaN. + correction = (get_ieee_754_significand(value) - 1) >> SIGNIFICAND_WIDTH + + return (exponent + correction) >> -self._scale + + def get_lower_boundary(self, index: int) -> float: + if index < self._min_normal_lower_boundary_index: + raise MappingUnderflowError() + + if index > self._max_normal_lower_boundary_index: + raise MappingOverflowError() + + return ldexp(1, index << -self._scale) + + @property + def scale(self) -> int: + return self._scale diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py new file mode 100644 index 00000000000..4d3eb05d67f --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py @@ -0,0 +1,181 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ctypes import c_double, c_uint64 +from decimal import Decimal +from sys import float_info + +# An IEEE 754 double-precision (64 bit) floating point number is represented +# as: 1 bit for sign, 11 bits for exponent and 52 bits for significand. Since +# these numbers are in a normalized form (in scientific notation), the first +# bit of the significand will always be 1. Because of that, that bit is not +# stored but implicit, to make room for one more bit and more precision. + +SIGNIFICAND_WIDTH = 52 +EXPONENT_WIDTH = 11 + +# This mask is equivalent to 52 "1" bits (there are 13 hexadecimal 4-bit "f"s +# in the significand mask, 13 * 4 == 52) or 0xfffffffffffff in hexadecimal. +SIGNIFICAND_MASK = (1 << SIGNIFICAND_WIDTH) - 1 + +# There are 11 bits for the exponent, but the exponent bias values 0 (11 "0" +# bits) and 2047 (11 "1" bits) have special meanings so the exponent range is +# from 1 to 2046. To calculate the exponent value, 1023 is subtracted from the +# exponent, so the exponent value range is from -1022 to +1023. +EXPONENT_BIAS = (2 ** (EXPONENT_WIDTH - 1)) - 1 + +# All the exponent mask bits are set to 1 for the 11 exponent bits. +EXPONENT_MASK = ((1 << EXPONENT_WIDTH) - 1) << SIGNIFICAND_WIDTH + +# The exponent mask bit is to 1 for the sign bit. +SIGN_MASK = 1 << (EXPONENT_WIDTH + SIGNIFICAND_WIDTH) + +MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1 +MAX_NORMAL_EXPONENT = EXPONENT_BIAS + +# Smallest possible normal value (2.2250738585072014e-308) +# This value is the result of using the smallest possible number in the +# mantissa, 1.0000000000000000000000000000000000000000000000000000 (52 "0"s in +# the fractional part) = 1.0000000000000000 and a single "1" in the exponent. +# Finally 1.0000000000000000 * 2 ** -1022 = 2.2250738585072014e-308. +MIN_NORMAL_VALUE = float_info.min + +# Greatest possible normal value (1.7976931348623157e+308) +# The binary representation of a float in scientific notation uses (for the +# significand) one bit for the integer part (which is implicit) and 52 bits for +# the fractional part. Consider a float binary 1.111. It is equal to 1 + 1/2 + +# 1/4 + 1/8. The greatest possible value in the 52-bit significand would be +# then 1.1111111111111111111111111111111111111111111111111111 (52 "1"s in the +# fractional part) = 1.9999999999999998. Finally, +# 1.9999999999999998 * 2 ** 1023 = 1.7976931348623157e+308. +MAX_NORMAL_VALUE = float_info.max + + +def get_ieee_754_64_binary(value: float): + """ + The purpose of this function is to illustrate the IEEE 754 64-bit float + representation. + """ + result = bin(c_uint64.from_buffer(c_double(value)).value)[2:] + + if result == "0": + result = result * 64 + + if value > 0: + result = f"0{result}" + + decimal_exponent = 0 + + exponent = result[1:12] + + for index, bit in enumerate(reversed(exponent)): + if int(bit): + decimal_exponent += 2**index + + # 0 has a special representation in IEE 574, all exponent and mantissa bits + # are 0. The sign bit still represents its sign, so there is 0 (all bits + # are set to 0) and -0 (the first bit is 1, the rest are 0). + if value == 0: + implicit_bit = 0 + else: + implicit_bit = 1 + + decimal_exponent -= 1023 * implicit_bit + + decimal_mantissa = Decimal(implicit_bit) + + mantissa = result[12:] + + for index, bit in enumerate(mantissa): + if int(bit): + decimal_mantissa += Decimal(1) / Decimal(2 ** (index + 1)) + + sign = result[0] + + return { + "sign": sign, + "exponent": exponent, + "mantissa": mantissa, + # IEEE 754 can only exactly represent a discrete series of numbers, the + # intention of this field is to show the actual decimal value that is + # represented. + "decimal": str( + Decimal(-1 if int(sign) else 1) + * Decimal(2**decimal_exponent) + * decimal_mantissa + ), + } + + +def get_ieee_754_exponent(value: float) -> int: + """ + Gets the exponent of the IEEE 754 representation of a float. + """ + + # 0000 -> 0 + # 0001 -> 1 + # 0010 -> 2 + # 0011 -> 3 + + # 0100 -> 4 + # 0101 -> 5 + # 0110 -> 6 + # 0111 -> 7 + + # 1000 -> 8 + # 1001 -> 9 + # 1010 -> 10 + # 1011 -> 11 + + # 1100 -> 12 + # 1101 -> 13 + # 1110 -> 14 + # 1111 -> 15 + + # 0 & 10 == 0 + # 1 & 10 == 0 + # 2 & 10 == 2 + # 3 & 10 == 2 + # 4 & 10 == 0 + # 6 & 10 == 2 + + # 12 >> 2 == 3 + # 1 >> 2 == 0 + + return ( + ( + # This step gives the integer that corresponds to the IEEE 754 + # representation of a float. + c_uint64.from_buffer(c_double(value)).value + # This step isolates the exponent bits, turning every bit + # outside of the exponent field to 0. + & EXPONENT_MASK + ) + # This step moves the exponent bits to the right, removing the + # mantissa bits that were set to 0 by the previous step. This + # leaves the IEEE 754 exponent value, ready for the next step. + >> SIGNIFICAND_WIDTH + # This step subtracts the exponent bias from the IEEE 754 value, + # leaving the actual exponent value. + ) - EXPONENT_BIAS + + +def get_ieee_754_significand(value: float) -> int: + return ( + c_uint64.from_buffer(c_double(value)).value + # This stepe isolates the significand bits. There is no need to do any + # bit shifting as the significand bits are already the rightmost field + # in an IEEE 754 representation. + & SIGNIFICAND_MASK + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py new file mode 100644 index 00000000000..d70b3030b5c --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py @@ -0,0 +1,140 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import exp, floor, ldexp, log +from threading import Lock + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( + Mapping, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( + MappingOverflowError, + MappingUnderflowError, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MAX_NORMAL_EXPONENT, + MIN_NORMAL_EXPONENT, + MIN_NORMAL_VALUE, + get_ieee_754_exponent, + get_ieee_754_significand, +) + + +class LogarithmMapping(Mapping): + + _mappings = {} + _mappings_lock = Lock() + # _min_scale ensures that ExponentMapping is used for zero and negative + # scale values. + _min_scale = 1 + # FIXME Go implementation uses a value of 20 here, find out the right + # value for this implementation, more information here: + # https://github.com/lightstep/otel-launcher-go/blob/c9ca8483be067a39ab306b09060446e7fda65f35/lightstep/sdk/metric/aggregator/histogram/structure/README.md#mapping-function + # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go#L32-L45 + _max_scale = 20 + + def __init__(self, scale: int): + + super().__init__(scale) + + # self._scale_factor is defined as a multiplier because multiplication + # is faster than division. self._scale_factor is defined as: + # index = log(value) * self._scale_factor + # Where: + # index = log(value) / log(base) + # index = log(value) / log(2 ** (2 ** -scale)) + # index = log(value) / ((2 ** -scale) * log(2)) + # index = log(value) * ((1 / log(2)) * (2 ** scale)) + # self._scale_factor = ((1 / log(2)) * (2 ** scale)) + # self._scale_factor = (1 /log(2)) * (2 ** scale) + # self._scale_factor = ldexp(1 / log(2), scale) + # This implementation was copied from a Java prototype. See: + # https://github.com/newrelic-experimental/newrelic-sketch-java/blob/1ce245713603d61ba3a4510f6df930a5479cd3f6/src/main/java/com/newrelic/nrsketch/indexer/LogIndexer.java + # for the equations used here. + self._scale_factor = ldexp(1 / log(2), scale) + + # self._min_normal_lower_boundary_index is the index such that + # base ** index == MIN_NORMAL_VALUE. An exponential histogram bucket + # with this index covers the range + # (MIN_NORMAL_VALUE, MIN_NORMAL_VALUE * base]. One less than this index + # corresponds with the bucket containing values <= MIN_NORMAL_VALUE. + self._min_normal_lower_boundary_index = ( + MIN_NORMAL_EXPONENT << self._scale + ) + + # self._max_normal_lower_boundary_index is the index such that + # base**index equals the greatest representable lower boundary. An + # exponential histogram bucket with this index covers the range + # ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. + # metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. + # This bucket is incomplete, since the upper boundary cannot be + # represented. One greater than this index corresponds with the bucket + # containing values > 2 ** 1024. + self._max_normal_lower_boundary_index = ( + (MAX_NORMAL_EXPONENT + 1) << self._scale + ) - 1 + + def map_to_index(self, value: float) -> int: + """ + MapToIndex maps positive floating point values to indexes + corresponding to Scale(). Implementations are not expected + to handle zeros, +Inf, NaN, or negative values. + """ + + # Note: we can assume not a 0, Inf, or NaN; positive sign bit. + if value <= MIN_NORMAL_VALUE: + return self._min_normal_lower_boundary_index - 1 + + # Exact power-of-two correctness: an optional special case. + if get_ieee_754_significand(value) == 0: + exponent = get_ieee_754_exponent(value) + return (exponent << self._scale) - 1 + + # Non-power of two cases. Use Floor(x) to round the scaled + # logarithm. We could use Ceil(x)-1 to achieve the same + # result, though Ceil() is typically defined as -Floor(-x) + # and typically not performed in hardware, so this is likely + # less code. + index = floor(log(value) * self._scale_factor) + + max_ = self._max_normal_lower_boundary_index + + if index >= max_: + return max_ + + return index + + def get_lower_boundary(self, index: int) -> float: + + if index >= self._max_normal_lower_boundary_index: + if index == self._max_normal_lower_boundary_index: + return 2 * exp( + (index - (1 << self._scale)) / self._scale_factor + ) + raise MappingOverflowError() + + if index <= self._min_normal_lower_boundary_index: + if index == self._min_normal_lower_boundary_index: + return MIN_NORMAL_VALUE + if index == self._min_normal_lower_boundary_index - 1: + return ( + exp((index + (1 << self._scale)) / self._scale_factor) / 2 + ) + raise MappingUnderflowError() + + return exp(index / self._scale_factor) + + @property + def scale(self) -> int: + return self._scale diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py new file mode 100644 index 00000000000..14c820df3f9 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -0,0 +1,378 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import inf +from sys import float_info, version_info +from unittest import TestCase + +from pytest import mark + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( + MappingUnderflowError, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping import ( + ExponentMapping, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MAX_NORMAL_EXPONENT, + MAX_NORMAL_VALUE, + MIN_NORMAL_EXPONENT, + MIN_NORMAL_VALUE, +) + +if version_info >= (3, 9): + from math import nextafter + + +def rounded_boundary(scale: int, index: int) -> float: + result = 2**index + + for _ in range(scale, 0): + result = result * result + + return result + + +class TestExponentMapping(TestCase): + def test_singleton(self): + + self.assertIs(ExponentMapping(-3), ExponentMapping(-3)) + self.assertIsNot(ExponentMapping(-3), ExponentMapping(-5)) + + def test_exponent_mapping_0(self): + + try: + ExponentMapping(0) + + except Exception as error: + self.fail(f"Unexpected exception raised: {error}") + + def test_exponent_mapping_zero(self): + + exponent_mapping = ExponentMapping(0) + + # This is the equivalent to 1.1 in hexadecimal + hex_1_1 = 1 + (1 / 16) + + # Testing with values near +inf + self.assertEqual( + exponent_mapping.map_to_index(MAX_NORMAL_VALUE), + MAX_NORMAL_EXPONENT, + ) + self.assertEqual(exponent_mapping.map_to_index(MAX_NORMAL_VALUE), 1023) + self.assertEqual(exponent_mapping.map_to_index(2**1023), 1022) + self.assertEqual(exponent_mapping.map_to_index(2**1022), 1021) + self.assertEqual( + exponent_mapping.map_to_index(hex_1_1 * (2**1023)), 1023 + ) + self.assertEqual( + exponent_mapping.map_to_index(hex_1_1 * (2**1022)), 1022 + ) + + # Testing with values near 1 + self.assertEqual(exponent_mapping.map_to_index(4), 1) + self.assertEqual(exponent_mapping.map_to_index(3), 1) + self.assertEqual(exponent_mapping.map_to_index(2), 0) + self.assertEqual(exponent_mapping.map_to_index(1), -1) + self.assertEqual(exponent_mapping.map_to_index(0.75), -1) + self.assertEqual(exponent_mapping.map_to_index(0.51), -1) + self.assertEqual(exponent_mapping.map_to_index(0.5), -2) + self.assertEqual(exponent_mapping.map_to_index(0.26), -2) + self.assertEqual(exponent_mapping.map_to_index(0.25), -3) + self.assertEqual(exponent_mapping.map_to_index(0.126), -3) + self.assertEqual(exponent_mapping.map_to_index(0.125), -4) + + # Testing with values near 0 + self.assertEqual(exponent_mapping.map_to_index(2**-1022), -1023) + self.assertEqual( + exponent_mapping.map_to_index(hex_1_1 * (2**-1022)), -1022 + ) + self.assertEqual(exponent_mapping.map_to_index(2**-1021), -1022) + self.assertEqual( + exponent_mapping.map_to_index(hex_1_1 * (2**-1021)), -1021 + ) + self.assertEqual( + exponent_mapping.map_to_index(2**-1022), MIN_NORMAL_EXPONENT - 1 + ) + self.assertEqual( + exponent_mapping.map_to_index(2**-1021), MIN_NORMAL_EXPONENT + ) + # The smallest subnormal value in Python is 2 ** -1074 = 5e-324. + # This value is also the result of: + # s = 1 + # while s / 2: + # s = s / 2 + # s == 5e-324 + self.assertEqual( + exponent_mapping.map_to_index(2**-1074), MIN_NORMAL_EXPONENT - 1 + ) + + def test_exponent_mapping_min_scale(self): + + exponent_mapping = ExponentMapping(ExponentMapping._min_scale) + self.assertEqual(exponent_mapping.map_to_index(1.000001), 0) + self.assertEqual(exponent_mapping.map_to_index(1), -1) + self.assertEqual(exponent_mapping.map_to_index(float_info.max), 0) + self.assertEqual(exponent_mapping.map_to_index(float_info.min), -1) + + def test_invalid_scale(self): + with self.assertRaises(Exception): + ExponentMapping(1) + + with self.assertRaises(Exception): + ExponentMapping(ExponentMapping._min_scale - 1) + + def test_exponent_mapping_neg_one(self): + exponent_mapping = ExponentMapping(-1) + self.assertEqual(exponent_mapping.map_to_index(17), 2) + self.assertEqual(exponent_mapping.map_to_index(16), 1) + self.assertEqual(exponent_mapping.map_to_index(15), 1) + self.assertEqual(exponent_mapping.map_to_index(9), 1) + self.assertEqual(exponent_mapping.map_to_index(8), 1) + self.assertEqual(exponent_mapping.map_to_index(5), 1) + self.assertEqual(exponent_mapping.map_to_index(4), 0) + self.assertEqual(exponent_mapping.map_to_index(3), 0) + self.assertEqual(exponent_mapping.map_to_index(2), 0) + self.assertEqual(exponent_mapping.map_to_index(1.5), 0) + self.assertEqual(exponent_mapping.map_to_index(1), -1) + self.assertEqual(exponent_mapping.map_to_index(0.75), -1) + self.assertEqual(exponent_mapping.map_to_index(0.5), -1) + self.assertEqual(exponent_mapping.map_to_index(0.25), -2) + self.assertEqual(exponent_mapping.map_to_index(0.20), -2) + self.assertEqual(exponent_mapping.map_to_index(0.13), -2) + self.assertEqual(exponent_mapping.map_to_index(0.125), -2) + self.assertEqual(exponent_mapping.map_to_index(0.10), -2) + self.assertEqual(exponent_mapping.map_to_index(0.0625), -3) + self.assertEqual(exponent_mapping.map_to_index(0.06), -3) + + def test_exponent_mapping_neg_four(self): + exponent_mapping = ExponentMapping(-4) + self.assertEqual(exponent_mapping.map_to_index(float(0x1)), -1) + self.assertEqual(exponent_mapping.map_to_index(float(0x10)), 0) + self.assertEqual(exponent_mapping.map_to_index(float(0x100)), 0) + self.assertEqual(exponent_mapping.map_to_index(float(0x1000)), 0) + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000)), 0 + ) # base == 2 ** 16 + self.assertEqual(exponent_mapping.map_to_index(float(0x100000)), 1) + self.assertEqual(exponent_mapping.map_to_index(float(0x1000000)), 1) + self.assertEqual(exponent_mapping.map_to_index(float(0x10000000)), 1) + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000)), 1 + ) # base == 2 ** 32 + + self.assertEqual(exponent_mapping.map_to_index(float(0x1000000000)), 2) + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000000000)), 2 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000000)), 2 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x1000000000000)), 2 + ) # base == 2 ** 48 + + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000000000000)), 3 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000000000)), 3 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x1000000000000000)), 3 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000000000000000)), 3 + ) # base == 2 ** 64 + + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000000000000)), 4 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x1000000000000000000)), 4 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x10000000000000000000)), 4 + ) + self.assertEqual( + exponent_mapping.map_to_index(float(0x100000000000000000000)), 4 + ) # base == 2 ** 80 + self.assertEqual( + exponent_mapping.map_to_index(float(0x1000000000000000000000)), 5 + ) + + self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1)), -1) + self.assertEqual(exponent_mapping.map_to_index(1 / float(0x10)), -1) + self.assertEqual(exponent_mapping.map_to_index(1 / float(0x100)), -1) + self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1000)), -1) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000)), -2 + ) # base == 2 ** -16 + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000)), -2 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x1000000)), -2 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000000)), -2 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000000)), -3 + ) # base == 2 ** -32 + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x1000000000)), -3 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000000000)), -3 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000000000)), -3 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x1000000000000)), -4 + ) # base == 2 ** -32 + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000000000000)), -4 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000000000000)), -4 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x1000000000000000)), -4 + ) + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x10000000000000000)), -5 + ) # base == 2 ** -64 + self.assertEqual( + exponent_mapping.map_to_index(1 / float(0x100000000000000000)), -5 + ) + + self.assertEqual(exponent_mapping.map_to_index(float_info.max), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1023), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1019), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1009), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1008), 62) + self.assertEqual(exponent_mapping.map_to_index(2**1007), 62) + self.assertEqual(exponent_mapping.map_to_index(2**1000), 62) + self.assertEqual(exponent_mapping.map_to_index(2**993), 62) + self.assertEqual(exponent_mapping.map_to_index(2**992), 61) + self.assertEqual(exponent_mapping.map_to_index(2**991), 61) + + self.assertEqual(exponent_mapping.map_to_index(2**-1074), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1073), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1072), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1057), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1056), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1041), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1040), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1025), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1024), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1023), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1022), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1009), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1008), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1007), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-993), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-992), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-991), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-977), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-976), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-975), -61) + + def test_exponent_index_mat(self): + + for scale in range( + ExponentMapping._min_scale, ExponentMapping._max_scale + ): + exponent_mapping = ExponentMapping(scale) + + index = exponent_mapping.map_to_index(MAX_NORMAL_VALUE) + + max_index = ((MAX_NORMAL_EXPONENT + 1) >> -scale) - 1 + + self.assertEqual(index, max_index) + + boundary = exponent_mapping.get_lower_boundary(index) + + self.assertEqual(boundary, rounded_boundary(scale, max_index)) + + with self.assertRaises(Exception): + exponent_mapping.get_lower_boundary(index + 1) + + @mark.skipif( + version_info < (3, 9), + reason="math.nextafter is only available for Python >= 3.9", + ) + def test_exponent_index_mint(self): + for scale in range( + ExponentMapping._min_scale, ExponentMapping._max_scale + 1 + ): + exponent_mapping = ExponentMapping(scale) + + min_index = exponent_mapping.map_to_index(MIN_NORMAL_VALUE) + boundary = exponent_mapping.get_lower_boundary(min_index) + + correct_min_index = MIN_NORMAL_EXPONENT >> -scale + + if MIN_NORMAL_EXPONENT % (1 << -scale) == 0: + correct_min_index -= 1 + + # We do not check for correct_min_index to be greater than the + # smallest integer because the smallest integer in Python is -inf. + + self.assertEqual(correct_min_index, min_index) + + correct_boundary = rounded_boundary(scale, correct_min_index) + + self.assertEqual(correct_boundary, boundary) + self.assertGreater( + rounded_boundary(scale, correct_min_index + 1), boundary + ) + + self.assertEqual( + correct_min_index, + exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 2), + ) + self.assertEqual( + correct_min_index, + exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 3), + ) + self.assertEqual( + correct_min_index, + exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 100), + ) + self.assertEqual( + correct_min_index, exponent_mapping.map_to_index(2**-1050) + ) + self.assertEqual( + correct_min_index, exponent_mapping.map_to_index(2**-1073) + ) + self.assertEqual( + correct_min_index, + exponent_mapping.map_to_index(1.1 * (2**-1073)), + ) + self.assertEqual( + correct_min_index, exponent_mapping.map_to_index(2**-1074) + ) + + with self.assertRaises(MappingUnderflowError): + exponent_mapping.get_lower_boundary(min_index - 1) + + self.assertEqual( + exponent_mapping.map_to_index( + nextafter(MIN_NORMAL_VALUE, inf) + ), + MIN_NORMAL_EXPONENT >> -scale, + ) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_ieee_754.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_ieee_754.py new file mode 100644 index 00000000000..8ac6df8b61b --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_ieee_754.py @@ -0,0 +1,92 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + get_ieee_754_64_binary, +) + + +class TestExponential(TestCase): + def test_get_ieee_754_64_binary(self): + """ + Bit 0: 1 for negative values, 0 for positive values + Bits 1 - 11: exponent, subtract 1023 from it to get the actual value + Bits 12 - 63: mantissa, a leading 1 is implicit + """ + + # 0 + # 10000000001 == 1025 -> 1025 - 1023 == 2 + # 0000000000000000000000000000000000000000000000000000 + + result = get_ieee_754_64_binary(4.0) + + self.assertEqual(result["sign"], "0") + self.assertEqual(result["exponent"], "10000000001") + self.assertEqual( + result["mantissa"], + "0000000000000000000000000000000000000000000000000000", + ) + self.assertEqual(result["decimal"], "4") + + result = get_ieee_754_64_binary(4.5) + + self.assertEqual(result["sign"], "0") + self.assertEqual(result["exponent"], "10000000001") + self.assertEqual( + result["mantissa"], + "0010000000000000000000000000000000000000000000000000", + ) + self.assertEqual(result["decimal"], "4.500") + + result = get_ieee_754_64_binary(-4.5) + + self.assertEqual(result["sign"], "1") + self.assertEqual(result["exponent"], "10000000001") + self.assertEqual( + result["mantissa"], + "0010000000000000000000000000000000000000000000000000", + ) + self.assertEqual(result["decimal"], "-4.500") + + result = get_ieee_754_64_binary(0.0) + + self.assertEqual(result["sign"], "0") + self.assertEqual(result["exponent"], "00000000000") + self.assertEqual( + result["mantissa"], + "0000000000000000000000000000000000000000000000000000", + ) + self.assertEqual(result["decimal"], "0") + + result = get_ieee_754_64_binary(-0.0) + + self.assertEqual(result["sign"], "1") + self.assertEqual(result["exponent"], "00000000000") + self.assertEqual( + result["mantissa"], + "0000000000000000000000000000000000000000000000000000", + ) + self.assertEqual(result["decimal"], "-0") + + result = get_ieee_754_64_binary(4.3) + + self.assertEqual(result["sign"], "0") + self.assertEqual(result["exponent"], "10000000001") + self.assertEqual( + result["mantissa"], + "0001001100110011001100110011001100110011001100110011", + ) + self.assertEqual(result["decimal"], "4.299999999999999822364316064") diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py new file mode 100644 index 00000000000..5230166d3e9 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py @@ -0,0 +1,226 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from math import sqrt +from unittest import TestCase + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( + MappingOverflowError, + MappingUnderflowError, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MAX_NORMAL_EXPONENT, + MAX_NORMAL_VALUE, + MIN_NORMAL_EXPONENT, + MIN_NORMAL_VALUE, +) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.logarithm_mapping import ( + LogarithmMapping, +) + + +def rounded_boundary(scale: int, index: int) -> float: + + # This is implemented in this way to avoid using a third-party bigfloat + # package. The Go implementation uses a bigfloat package that is part of + # their standard library. The assumption here is that the smallest float + # available in Python is 2 ** -1022 (from sys.float_info.min). + while scale > 0: + if index < -1022: + index /= 2 + scale -= 1 + else: + break + + result = 2**index + + for _ in range(scale, 0, -1): + result = sqrt(result) + + return result + + +class TestLogarithmMapping(TestCase): + def assertInEpsilon(self, first, second, epsilon): + self.assertLessEqual(first, (second * (1 + epsilon))) + self.assertGreaterEqual(first, (second * (1 - epsilon))) + + def test_invalid_scale(self): + with self.assertRaises(Exception): + LogarithmMapping(-1) + + def test_logarithm_mapping_scale_one(self): + + # The exponentiation factor for this logarithm exponent histogram + # mapping is square_root(2). + # Scale 1 means 1 division between every power of two, having + # a factor sqare_root(2) times the lower boundary. + logarithm_exponent_histogram_mapping = LogarithmMapping(1) + + self.assertEqual(logarithm_exponent_histogram_mapping.scale, 1) + + # Note: Do not test exact boundaries, with the exception of + # 1, because we expect errors in that case (e.g., + # MapToIndex(8) returns 5, an off-by-one. See the following + # test. + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(15), 7 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(9), 6 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(7), 5 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(5), 4 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(3), 3 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(2.5), 2 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(1.5), 1 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(1.2), 0 + ) + # This one is actually an exact test + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(1), -1 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(0.75), -1 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(0.55), -2 + ) + self.assertEqual( + logarithm_exponent_histogram_mapping.map_to_index(0.45), -3 + ) + + def test_logarithm_boundary(self): + + for scale in [1, 2, 3, 4, 10, 15]: + logarithm_exponent_histogram_mapping = LogarithmMapping(scale) + + for index in [-100, -10, -1, 0, 1, 10, 100]: + + lower_boundary = ( + logarithm_exponent_histogram_mapping.get_lower_boundary( + index + ) + ) + + mapped_index = ( + logarithm_exponent_histogram_mapping.map_to_index( + lower_boundary + ) + ) + + self.assertLessEqual(index - 1, mapped_index) + self.assertGreaterEqual(index, mapped_index) + + self.assertInEpsilon( + lower_boundary, rounded_boundary(scale, index), 1e-9 + ) + + def test_logarithm_index_max(self): + + for scale in range( + LogarithmMapping._min_scale, LogarithmMapping._max_scale + 1 + ): + logarithm_mapping = LogarithmMapping(scale) + + index = logarithm_mapping.map_to_index(MAX_NORMAL_VALUE) + + max_index = ((MAX_NORMAL_EXPONENT + 1) << scale) - 1 + + # We do not check for max_index to be lesser than the + # greatest integer because the greatest integer in Python is inf. + + self.assertEqual(index, max_index) + + boundary = logarithm_mapping.get_lower_boundary(index) + + base = logarithm_mapping.get_lower_boundary(1) + + self.assertLess(boundary, MAX_NORMAL_VALUE) + + self.assertInEpsilon( + (MAX_NORMAL_VALUE - boundary) / boundary, base - 1, 1e-6 + ) + + with self.assertRaises(MappingOverflowError): + logarithm_mapping.get_lower_boundary(index + 1) + + with self.assertRaises(MappingOverflowError): + logarithm_mapping.get_lower_boundary(index + 2) + + def test_logarithm_index_min(self): + for scale in range( + LogarithmMapping._min_scale, LogarithmMapping._max_scale + 1 + ): + logarithm_mapping = LogarithmMapping(scale) + + min_index = logarithm_mapping.map_to_index(MIN_NORMAL_VALUE) + + correct_min_index = (MIN_NORMAL_EXPONENT << scale) - 1 + self.assertEqual(min_index, correct_min_index) + + correct_mapped = rounded_boundary(scale, correct_min_index) + self.assertLess(correct_mapped, MIN_NORMAL_VALUE) + + correct_mapped_upper = rounded_boundary( + scale, correct_min_index + 1 + ) + self.assertEqual(correct_mapped_upper, MIN_NORMAL_VALUE) + + mapped = logarithm_mapping.get_lower_boundary(min_index + 1) + + self.assertInEpsilon(mapped, MIN_NORMAL_VALUE, 1e-6) + + self.assertEqual( + logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 2), + correct_min_index, + ) + self.assertEqual( + logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 3), + correct_min_index, + ) + self.assertEqual( + logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 100), + correct_min_index, + ) + self.assertEqual( + logarithm_mapping.map_to_index(2**-1050), correct_min_index + ) + self.assertEqual( + logarithm_mapping.map_to_index(2**-1073), correct_min_index + ) + self.assertEqual( + logarithm_mapping.map_to_index(1.1 * 2**-1073), + correct_min_index, + ) + self.assertEqual( + logarithm_mapping.map_to_index(2**-1074), correct_min_index + ) + + mapped_lower = logarithm_mapping.get_lower_boundary(min_index) + self.assertInEpsilon(correct_mapped, mapped_lower, 1e-6) + + with self.assertRaises(MappingUnderflowError): + logarithm_mapping.get_lower_boundary(min_index - 1) From d50f0737b81484ba7c91db0f44555926b84da667 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 13 Oct 2022 13:07:51 +0100 Subject: [PATCH 02/24] Fix comments --- .../mapping/exponent_mapping.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py index 54eabcfd88e..76c83bd81bd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -47,13 +47,18 @@ class ExponentMapping(Mapping): def __init__(self, scale: int): super().__init__(scale) - # self._min_normal_lower_boundary_index is the index such that - # base ** index <= MIN_NORMAL_VALUE. An exponential histogram bucket - # with this index covers the range (base ** index, base (index + 1)], - # including MIN_NORMAL_VALUE. + # self._min_normal_lower_boundary_index is the largest index such that + # base ** index < MIN_NORMAL_VALUE and + # base ** (index + 1) >= MIN_NORMAL_VALUE. An exponential histogram + # bucket with this index covers the range + # (base ** index, base (index + 1)], including MIN_NORMAL_VALUE. This + # is the smallest valid index that contains at least one normal value. index = MIN_NORMAL_EXPONENT >> -self._scale if -self._scale < 2: + # For scales -1 and 0, the maximum value 2 ** -1022 is a + # power-of-two multiple, meaning base ** index == MIN_NORMAL_VALUE. + # Substracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE. index -= 1 self._min_normal_lower_boundary_index = index From 5303cc4781161925c4de7a2c4e64924dfc492682 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 13 Oct 2022 16:36:25 +0100 Subject: [PATCH 03/24] Remove example function --- .../exponential_histogram/mapping/ieee_754.py | 56 ----------- .../exponential_histogram/test_ieee_754.py | 92 ------------------- 2 files changed, 148 deletions(-) delete mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_ieee_754.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py index 4d3eb05d67f..4c8ceb9f8d5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py @@ -62,62 +62,6 @@ MAX_NORMAL_VALUE = float_info.max -def get_ieee_754_64_binary(value: float): - """ - The purpose of this function is to illustrate the IEEE 754 64-bit float - representation. - """ - result = bin(c_uint64.from_buffer(c_double(value)).value)[2:] - - if result == "0": - result = result * 64 - - if value > 0: - result = f"0{result}" - - decimal_exponent = 0 - - exponent = result[1:12] - - for index, bit in enumerate(reversed(exponent)): - if int(bit): - decimal_exponent += 2**index - - # 0 has a special representation in IEE 574, all exponent and mantissa bits - # are 0. The sign bit still represents its sign, so there is 0 (all bits - # are set to 0) and -0 (the first bit is 1, the rest are 0). - if value == 0: - implicit_bit = 0 - else: - implicit_bit = 1 - - decimal_exponent -= 1023 * implicit_bit - - decimal_mantissa = Decimal(implicit_bit) - - mantissa = result[12:] - - for index, bit in enumerate(mantissa): - if int(bit): - decimal_mantissa += Decimal(1) / Decimal(2 ** (index + 1)) - - sign = result[0] - - return { - "sign": sign, - "exponent": exponent, - "mantissa": mantissa, - # IEEE 754 can only exactly represent a discrete series of numbers, the - # intention of this field is to show the actual decimal value that is - # represented. - "decimal": str( - Decimal(-1 if int(sign) else 1) - * Decimal(2**decimal_exponent) - * decimal_mantissa - ), - } - - def get_ieee_754_exponent(value: float) -> int: """ Gets the exponent of the IEEE 754 representation of a float. diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_ieee_754.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_ieee_754.py deleted file mode 100644 index 8ac6df8b61b..00000000000 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_ieee_754.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest import TestCase - -from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( - get_ieee_754_64_binary, -) - - -class TestExponential(TestCase): - def test_get_ieee_754_64_binary(self): - """ - Bit 0: 1 for negative values, 0 for positive values - Bits 1 - 11: exponent, subtract 1023 from it to get the actual value - Bits 12 - 63: mantissa, a leading 1 is implicit - """ - - # 0 - # 10000000001 == 1025 -> 1025 - 1023 == 2 - # 0000000000000000000000000000000000000000000000000000 - - result = get_ieee_754_64_binary(4.0) - - self.assertEqual(result["sign"], "0") - self.assertEqual(result["exponent"], "10000000001") - self.assertEqual( - result["mantissa"], - "0000000000000000000000000000000000000000000000000000", - ) - self.assertEqual(result["decimal"], "4") - - result = get_ieee_754_64_binary(4.5) - - self.assertEqual(result["sign"], "0") - self.assertEqual(result["exponent"], "10000000001") - self.assertEqual( - result["mantissa"], - "0010000000000000000000000000000000000000000000000000", - ) - self.assertEqual(result["decimal"], "4.500") - - result = get_ieee_754_64_binary(-4.5) - - self.assertEqual(result["sign"], "1") - self.assertEqual(result["exponent"], "10000000001") - self.assertEqual( - result["mantissa"], - "0010000000000000000000000000000000000000000000000000", - ) - self.assertEqual(result["decimal"], "-4.500") - - result = get_ieee_754_64_binary(0.0) - - self.assertEqual(result["sign"], "0") - self.assertEqual(result["exponent"], "00000000000") - self.assertEqual( - result["mantissa"], - "0000000000000000000000000000000000000000000000000000", - ) - self.assertEqual(result["decimal"], "0") - - result = get_ieee_754_64_binary(-0.0) - - self.assertEqual(result["sign"], "1") - self.assertEqual(result["exponent"], "00000000000") - self.assertEqual( - result["mantissa"], - "0000000000000000000000000000000000000000000000000000", - ) - self.assertEqual(result["decimal"], "-0") - - result = get_ieee_754_64_binary(4.3) - - self.assertEqual(result["sign"], "0") - self.assertEqual(result["exponent"], "10000000001") - self.assertEqual( - result["mantissa"], - "0001001100110011001100110011001100110011001100110011", - ) - self.assertEqual(result["decimal"], "4.299999999999999822364316064") From 755ea9e456b793af3e3e53dd8c049ba55b542aa7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 13 Oct 2022 16:55:59 +0100 Subject: [PATCH 04/24] Fix lint and spelling --- .../_internal/exponential_histogram/mapping/exponent_mapping.py | 2 +- .../metrics/_internal/exponential_histogram/mapping/ieee_754.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py index 76c83bd81bd..eb90f1abaa4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -58,7 +58,7 @@ def __init__(self, scale: int): if -self._scale < 2: # For scales -1 and 0, the maximum value 2 ** -1022 is a # power-of-two multiple, meaning base ** index == MIN_NORMAL_VALUE. - # Substracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE. + # Subtracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE. index -= 1 self._min_normal_lower_boundary_index = index diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py index 4c8ceb9f8d5..7e1bfdb3d13 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py @@ -13,7 +13,6 @@ # limitations under the License. from ctypes import c_double, c_uint64 -from decimal import Decimal from sys import float_info # An IEEE 754 double-precision (64 bit) floating point number is represented From b9eb84990f16b69725430b662b3517e5ad2edf48 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 14 Oct 2022 16:57:54 +0100 Subject: [PATCH 05/24] Add link to spec --- .../_internal/exponential_histogram/mapping/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py index 7ab61ac8d67..6349e8b7919 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -38,8 +38,9 @@ def __init__(self, scale: int) -> None: # parameter known as scale, larger values of scale will produce smaller # buckets. Bucket boundaries of the exponential histogram are located # at integer powers of the base, where: - + # # base = 2 ** (2 ** (-scale)) + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#all-scales-use-the-logarithm-function self._scale = scale @property From 5db30dc319c9ac067ec0f3c8ab85d5aa8bf7dff0 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 24 Oct 2022 18:48:04 +0100 Subject: [PATCH 06/24] Fix documentation to reference the exceptions --- .../src/opentelemetry/sdk/metrics/__init__.py | 6 ++++++ .../_internal/exponential_histogram/mapping/__init__.py | 8 +++++--- .../src/opentelemetry/sdk/metrics/export/__init__.py | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 2219bc35c52..0450f3e561d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -30,6 +30,12 @@ UpDownCounter, ) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( # noqa: F401 + MappingUnderflowError, + MappingOverflowError, +) + + __all__ = [] for key, value in globals().copy().items(): if not key.startswith("_"): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py index 6349e8b7919..cc27ccac7dc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -72,9 +72,11 @@ def get_lower_boundary(self, index: int) -> float: expected to map onto a range that is at least partially inside the range of normalized floating point values. If the corresponding bucket's upper boundary is less than or equal to 2 ** -1022, - `UnderflowError` will be raised. If the corresponding bucket's lower - boundary is greater than `sys.float_info.max`, `OverflowError` will be - raised. + :class:`~opentelemetry.sdk.metrics.MappingUnderflowError` + will be raised. If the corresponding bucket's lower boundary is greater + than ``sys.float_info.max``, + :class:`~opentelemetry.sdk.metrics.MappingOverflowError` + will be raised. """ @property diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 76021bd576d..9d602e2ee9a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -39,6 +39,7 @@ Sum, ) + __all__ = [] for key, value in globals().copy().items(): if not key.startswith("_"): From 385517d7f68d88c5960580c0e85edabe40069fd5 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 25 Oct 2022 15:12:02 +0100 Subject: [PATCH 07/24] Refactor min and max scale --- .../src/opentelemetry/sdk/metrics/__init__.py | 10 ++++------ .../exponential_histogram/mapping/__init__.py | 10 ++++------ .../mapping/exponent_mapping.py | 17 ++++++++++++----- .../mapping/logarithm_mapping.py | 19 +++++++++++++------ .../sdk/metrics/export/__init__.py | 1 - 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 0450f3e561d..5b979a1ae54 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -21,6 +21,10 @@ from opentelemetry.sdk.metrics._internal.exceptions import ( # noqa: F401 MetricsTimeoutError, ) +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( # noqa: F401 + MappingOverflowError, + MappingUnderflowError, +) from opentelemetry.sdk.metrics._internal.instrument import ( # noqa: F401 Counter, Histogram, @@ -30,12 +34,6 @@ UpDownCounter, ) -from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( # noqa: F401 - MappingUnderflowError, - MappingOverflowError, -) - - __all__ = [] for key, value in globals().copy().items(): if not key.startswith("_"): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py index cc27ccac7dc..ee6507cfeb9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -28,10 +28,10 @@ def __new__(cls, scale: int): def __init__(self, scale: int) -> None: - if scale > self._max_scale: + if scale > self._get_max_scale(): raise Exception(f"scale is larger than {self._max_scale}") - if scale < self._min_scale: + if scale < self._get_min_scale(): raise Exception(f"scale is smaller than {self._min_scale}") # The size of the exponential histogram buckets is determined by a @@ -43,16 +43,14 @@ def __init__(self, scale: int) -> None: # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#all-scales-use-the-logarithm-function self._scale = scale - @property @abstractmethod - def _min_scale(self) -> int: + def _get_min_scale(self) -> int: """ Return the smallest possible value for the mapping scale """ - @property @abstractmethod - def _max_scale(self) -> int: + def _get_max_scale(self) -> int: """ Return the largest possible value for the mapping scale """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py index eb90f1abaa4..5045004d75e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -36,14 +36,21 @@ class ExponentMapping(Mapping): _mappings = {} _mappings_lock = Lock() - # _min_scale defines the point at which the exponential mapping function - # becomes useless for 64-bit floats. With scale -10, ignoring subnormal - # values, bucket indices range from -1 to 1. + _min_scale = -10 - # _max_scale is the largest scale supported by exponential mapping. Use - # a logarithm mapping for larger scales. _max_scale = 0 + def _get_min_scale(self): + # _min_scale defines the point at which the exponential mapping + # function becomes useless for 64-bit floats. With scale -10, ignoring + # subnormal values, bucket indices range from -1 to 1. + return -10 + + def _get_max_scale(self): + # _max_scale is the largest scale supported by exponential mapping. Use + # a logarithm mapping for larger scales. + return 0 + def __init__(self, scale: int): super().__init__(scale) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py index d70b3030b5c..78b5c9c5e6e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py @@ -35,15 +35,22 @@ class LogarithmMapping(Mapping): _mappings = {} _mappings_lock = Lock() - # _min_scale ensures that ExponentMapping is used for zero and negative - # scale values. + _min_scale = 1 - # FIXME Go implementation uses a value of 20 here, find out the right - # value for this implementation, more information here: - # https://github.com/lightstep/otel-launcher-go/blob/c9ca8483be067a39ab306b09060446e7fda65f35/lightstep/sdk/metric/aggregator/histogram/structure/README.md#mapping-function - # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go#L32-L45 _max_scale = 20 + def _get_min_scale(self): + # _min_scale ensures that ExponentMapping is used for zero and negative + # scale values. + return self._min_scale + + def _get_max_scale(self): + # FIXME The Go implementation uses a value of 20 here, find out the + # right value for this implementation, more information here: + # https://github.com/lightstep/otel-launcher-go/blob/c9ca8483be067a39ab306b09060446e7fda65f35/lightstep/sdk/metric/aggregator/histogram/structure/README.md#mapping-function + # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go#L32-L45 + return self._max_scale + def __init__(self, scale: int): super().__init__(scale) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 9d602e2ee9a..76021bd576d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -39,7 +39,6 @@ Sum, ) - __all__ = [] for key, value in globals().copy().items(): if not key.startswith("_"): From edd32d79cb30279790a332200b24d50fa4dfe64a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 25 Oct 2022 15:15:09 +0100 Subject: [PATCH 08/24] Set self._scale in parent only --- .../metrics/_internal/exponential_histogram/mapping/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py index ee6507cfeb9..27d88f66bff 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -78,9 +78,9 @@ def get_lower_boundary(self, index: int) -> float: """ @property - @abstractmethod def scale(self) -> int: """ Returns the parameter that controls the resolution of this mapping. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale """ + return self._scale From 6871471d1bd7b15562d566d7510e45e3440cf76f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 26 Oct 2022 15:46:38 +0100 Subject: [PATCH 09/24] Add explanation for IEEE 754 --- .../exponential_histogram/mapping/ieee_754.md | 175 ++++++++++++++++++ .../exponential_histogram/mapping/ieee_754.py | 98 ++++++---- 2 files changed, 237 insertions(+), 36 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md new file mode 100644 index 00000000000..d1fea7467c9 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md @@ -0,0 +1,175 @@ +# IEEE 754 Explained + +IEEE 754 is a standard that defines a way to represent certain mathematical +objects using binary numbers. + +## Binary Number Fields + +The binary numbers used in IEEE 754 can have different lenghts, the length that +is interesting for the purposes of this project is 64 bits. These binary +numbers are made up of 3 contiguous fields of bits, from left to right: + +1. 1 sign bit +2. 11 exponent bits +3. 52 mantissa bits + +Depending on the values these fields have, the represented mathematical object +can be one of: + +* Floating point number +* Zero +* NaN +* Infinite + +## Floating Point Numbers + +IEEE 754 represents a floating point number $f$ using an exponential +notation with 4 components: $sign$, $mantissa$, $base$ and $exponent$: + +$$f = sign \times mantissa \times base ^ {exponent}$$ + +There are two possible representations of floating point numbers: +_normalized_ and _denormalized_, which have different valid values for +their $mantissa$ and $exponent$ fields. + +### Binary Representation + +$sign$, $mantissa$, and $exponent$ are represented in binary, the +representation of each component has certain details explained next. + +$base$ is always $2$ and it is not represented in binary. + +#### Sign + +$sign$ can have 2 values: + +1. $1$ if the `sign` bit is `0` +2. $-1$ if the `sign` bit is `1`. + +#### Mantissa + +##### Normalized Floating Point Numbers + +$mantissa$ is a positive fractional number whose integer part is $1$, for example +$1.2345...$. The `mantissa` bits represent only the fractional part and the +$mantissa$ value can be calculated as: + +$$mantissa = 1 + \sum_{i=1}^{52} b_{i} \times 2^{-i} = 1 + \frac{b_{1}}{2^{1}} + \frac{b_{2}}{2^{2}} + \dots + \frac{b_{63}}{2^{63}} + \frac{b_{52}}{2^{52}}$$ + +Where $b_{i}$ is: + +1. $0$ if the bit at the position `i - 1` is `0`. +2. $1$ if the bit at the position `i - 1` is `1`. + +##### Denormalized Floating Point Numbers + +$mantissa$ is a positive fractional number whose integer part is $0$, for example +$0.12345...$. The `mantissa` bits represent only the fractional part and the +$mantissa$ value can be calculated as: + +$$mantissa = \sum_{i=1}^{52} b_{i} \times 2^{-i} = \frac{b_{1}}{2^{1}} + \frac{b_{2}}{2^{2}} + \dots + \frac{b_{63}}{2^{63}} + \frac{b_{52}}{2^{52}}$$ + +Where $b_{i}$ is: + +1. $0$ if the bit at the position `i - 1` is `0`. +2. $1$ if the bit at the position `i - 1` is `1`. + +#### Exponent + +##### Normalized Floating Point Numbers + +Only the following bit sequences are allowed: `00000000001` to `11111111110`. +That is, there must be at least one `0` and one `1` in the exponent bits. + +The actual value of the $exponent$ can be calculated as: + +$$exponent = v - bias$$ + +where $v$ is the value of the binary number in the exponent bits and $bias$ is $1023$. +Considering the restrictions above, the respective minimum and maximum values for the +exponent are: + +1. `00000000001` = $1$, $1 - 1023 = 1022$ +2. `11111111110` = $2046$, $2046 - 1023 = 1023$ + +So, $exponent$ is an integer in the range $\left[-1022, 1023\right]$. + + +##### Denormalized Floating Point Numbers + +$exponent$ is always $-1022$. Nevertheless, it is always represented as `00000000000`. + +### Normalized and Denormalized Floating Point Numbers + +The smallest absolute value a normalized floating point number can have is calculated +like this: + +$$1 \times 1.0\dots0 \times 2^{-1022} = 2.2250738585072014 \times 10^{-308}$$ + +Since normalized floating point numbers always have a $1$ as the integer part of the +$mantissa$, then smaller values can be achieved by using the smallest possible exponent +($-1022$) and a $0$ in the integer part of the $mantissa$, but significant digits are lost. + +The smallest absolute value a denormalized floating point number can have is calculated +like this: + +$$1 \times 2^{-52} \times 2^{-1022} = 5 \times 10^{-324}$$ + +## Zero + +Zero is represented like this: + +* Sign bit: `X` +* Exponent bits: `00000000000` +* Mantissa bits: `0000000000000000000000000000000000000000000000000000` + +where `X` means `0` or `1`. + +## NaN + +There are 2 kinds of NaNs that are represented: + +1. QNaNs (Quiet NaNs): represent the result of indeterminate operations. +2. SNaNs (Signalling NaNs): represent the result of invalid operations. + +### QNaNs + +QNaNs are represented like this: + +* Sign bit: `X` +* Exponent bits: `11111111111` +* Mantissa bits: `1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` + +where `X` means `0` or `1`. + +### SNaNs + +SNaNs are represented like this: + +* Sign bit: `X` +* Exponent bits: `11111111111` +* Mantissa bits: `0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1` + +where `X` means `0` or `1`. + +## Infinite + +### Positive Infinite + +Positive infinite is represented like this: + +* Sign bit: `0` +* Exponent bits: `11111111111` +* Mantissa bits: `0000000000000000000000000000000000000000000000000000` + +where `X` means `0` or `1`. + +### Negative Infinite + +Negative infinite is represented like this: + +* Sign bit: `1` +* Exponent bits: `11111111111` +* Mantissa bits: `0000000000000000000000000000000000000000000000000000` + +where `X` means `0` or `1`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py index 7e1bfdb3d13..09c633bd081 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py @@ -15,6 +15,37 @@ from ctypes import c_double, c_uint64 from sys import float_info +# IEEE 754 is a standard that defines a way to represent floating point numbers +# (normalized and denormalized), and also special numbers like zero, infinite +# and NaN using 64-bit binary numbers. First we'll explain how floating point +# numbers are represented and special numbers will be explained later. +# +# IEEE 754 represents floating point numbers using an exponential notation with +# 4 components: sign, mantissa, base and exponent: +# +# floating_point_number = sign * mantissa * (base ** exponent) +# +# Here: +# 1. sign can be 1 or -1. +# 2. mantissa is a positive fractional number whose integer part is 1 for +# normalized floating point numbers and 0 for denormalized floating point +# numbers. +# 3. base is always 2. +# 4. exponent is an integer in the range [-1022, 1023] for normalized floating +# point numbers and -1022 for denormalized floating point numbers. +# +# The smallest value a normalized floating point number can have is +# -1 * 1.0 * (2 ** -1022) == 2.2250738585072014e-308. +# As mentioned before, IEEE 754 defines how floating point numbers are +# represented using a 64-bit binary number, for this number: +# +# 1. The first bit represents the sign. +# 2. The next 11 bits represent the exponent. +# 3. The next 52 bits represent the mantissa. +# +# The sign is positive if the sign bit is 0 and negative if the sign bit is 1. +# +# There are 11 bits for the exponent # An IEEE 754 double-precision (64 bit) floating point number is represented # as: 1 bit for sign, 11 bits for exponent and 52 bits for significand. Since # these numbers are in a normalized form (in scientific notation), the first @@ -66,58 +97,53 @@ def get_ieee_754_exponent(value: float) -> int: Gets the exponent of the IEEE 754 representation of a float. """ - # 0000 -> 0 - # 0001 -> 1 - # 0010 -> 2 - # 0011 -> 3 - - # 0100 -> 4 - # 0101 -> 5 - # 0110 -> 6 - # 0111 -> 7 - - # 1000 -> 8 - # 1001 -> 9 - # 1010 -> 10 - # 1011 -> 11 - - # 1100 -> 12 - # 1101 -> 13 - # 1110 -> 14 - # 1111 -> 15 - - # 0 & 10 == 0 - # 1 & 10 == 0 - # 2 & 10 == 2 - # 3 & 10 == 2 - # 4 & 10 == 0 - # 6 & 10 == 2 - - # 12 >> 2 == 3 - # 1 >> 2 == 0 - return ( ( # This step gives the integer that corresponds to the IEEE 754 - # representation of a float. - c_uint64.from_buffer(c_double(value)).value - # This step isolates the exponent bits, turning every bit - # outside of the exponent field to 0. + # representation of a float. For example, consider + # -MAX_NORMAL_VALUE for an example. We choose this value because + # of its binary representation which makes easy to understand the + # subsequent operations. + # + # c_uint64.from_buffer(c_double(-MAX_NORMAL_VALUE)).value == 18442240474082181119 + # bin(18442240474082181119) == '0b1111111111101111111111111111111111111111111111111111111111111111' + # + # The first bit of the previous binary number is the sign bit: 1 (1 means negative, 0 means positive) + # The next 11 bits are the exponent bits: 11111111110 + # The next 52 bits are the significand bits: 1111111111111111111111111111111111111111111111111111 + # + # This step isolates the exponent bits, turning every bit outside + # of the exponent field (sign and significand bits) to 0. + c_uint64.from_buffer(c_double(-MAX_NORMAL_VALUE)).value & EXPONENT_MASK + # For the example this means: + # 18442240474082181119 & EXPONENT_MASK == 9214364837600034816 + # bin(9214364837600034816) == '0b111111111100000000000000000000000000000000000000000000000000000' + # Notice that the previous binary representation does not include + # leading zeroes, so the sign bit is not included since it is a + # zero. ) # This step moves the exponent bits to the right, removing the - # mantissa bits that were set to 0 by the previous step. This + # significand bits that were set to 0 by the previous step. This # leaves the IEEE 754 exponent value, ready for the next step. >> SIGNIFICAND_WIDTH + # For the example this means: + # 9214364837600034816 >> SIGNIFICAND_WIDTH == 2046 + # bin(2046) == '0b11111111110' + # As shown above, these are the original 11 bits that correspond to the + # exponent. # This step subtracts the exponent bias from the IEEE 754 value, # leaving the actual exponent value. ) - EXPONENT_BIAS + # For the example this means: + # 2046 - EXPONENT_BIAS == 1023 + # As mentioned in a comment above, the largest value for the exponent is def get_ieee_754_significand(value: float) -> int: return ( c_uint64.from_buffer(c_double(value)).value - # This stepe isolates the significand bits. There is no need to do any + # This step isolates the significand bits. There is no need to do any # bit shifting as the significand bits are already the rightmost field # in an IEEE 754 representation. & SIGNIFICAND_MASK From fda116f4001adad6dddd7da742b04674355d484d Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 27 Oct 2022 16:34:50 +0100 Subject: [PATCH 10/24] Use mantissa consistently --- .../mapping/exponent_mapping.py | 24 ++--- .../exponential_histogram/mapping/ieee_754.md | 2 +- .../exponential_histogram/mapping/ieee_754.py | 92 ++++++------------- .../mapping/logarithm_mapping.py | 4 +- 4 files changed, 45 insertions(+), 77 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py index 5045004d75e..a7aa2e2650a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -23,12 +23,12 @@ MappingUnderflowError, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( + MANTISSA_WIDTH, MAX_NORMAL_EXPONENT, MIN_NORMAL_EXPONENT, MIN_NORMAL_VALUE, - SIGNIFICAND_WIDTH, get_ieee_754_exponent, - get_ieee_754_significand, + get_ieee_754_mantissa, ) @@ -96,30 +96,30 @@ def map_to_index(self, value: float) -> int: # == 3. 3 is represented as ...00011. Its compliment is ...11100, the # binary representation of -4. - # get_ieee_754_significand(value) gets the positive integer made up - # from the rightmost SIGNIFICAND_WIDTH bits (the mantissa) of the IEEE + # get_ieee_754_mantissa(value) gets the positive integer made up + # from the rightmost MANTISSA_WIDTH bits (the mantissa) of the IEEE # 754 representation of value. If value is an exact power of 2, all - # these SIGNIFICAND_WIDTH bits would be all zeroes, and when 1 is + # these MANTISSA_WIDTH bits would be all zeroes, and when 1 is # subtracted the resulting value is -1. The binary representation of - # -1 is ...111, so when these bits are right shifted SIGNIFICAND_WIDTH + # -1 is ...111, so when these bits are right shifted MANTISSA_WIDTH # places, the resulting value for correction is -1. If value is not an - # exact power of 2, at least one of the rightmost SIGNIFICAND_WIDTH + # exact power of 2, at least one of the rightmost MANTISSA_WIDTH # bits would be 1 (even for values whose decimal part is 0, like 5.0 # since the IEEE 754 of such number is too the product of a power of 2 # (defined in the exponent part of the IEEE 754 representation) and the # value defined in the mantissa). Having at least one of the rightmost - # SIGNIFICAND_WIDTH bit being 1 means that get_ieee_754(value) will + # MANTISSA_WIDTH bit being 1 means that get_ieee_754(value) will # always be greater or equal to 1, and when 1 is subtracted, the # result will be greater or equal to 0, whose representation in binary - # will be of at most SIGNIFICAND_WIDTH ones that have an infinite - # amount of leading zeroes. When those SIGNIFICAND_WIDTH bits are - # shifted to the right SIGNIFICAND_WIDTH places, the resulting value + # will be of at most MANTISSA_WIDTH ones that have an infinite + # amount of leading zeroes. When those MANTISSA_WIDTH bits are + # shifted to the right MANTISSA_WIDTH places, the resulting value # will be 0. # In summary, correction will be -1 if value is a power of 2, 0 if not. # FIXME Document why we can assume value will not be 0, inf, or NaN. - correction = (get_ieee_754_significand(value) - 1) >> SIGNIFICAND_WIDTH + correction = (get_ieee_754_mantissa(value) - 1) >> MANTISSA_WIDTH return (exponent + correction) >> -self._scale diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md index d1fea7467c9..cf69a197686 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md @@ -89,7 +89,7 @@ where $v$ is the value of the binary number in the exponent bits and $bias$ is $ Considering the restrictions above, the respective minimum and maximum values for the exponent are: -1. `00000000001` = $1$, $1 - 1023 = 1022$ +1. `00000000001` = $1$, $1 - 1023 = -1022$ 2. `11111111110` = $2046$, $2046 - 1023 = 1023$ So, $exponent$ is an integer in the range $\left[-1022, 1023\right]$. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py index 09c633bd081..cc1266d2b8e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py @@ -15,80 +15,48 @@ from ctypes import c_double, c_uint64 from sys import float_info -# IEEE 754 is a standard that defines a way to represent floating point numbers -# (normalized and denormalized), and also special numbers like zero, infinite -# and NaN using 64-bit binary numbers. First we'll explain how floating point -# numbers are represented and special numbers will be explained later. -# -# IEEE 754 represents floating point numbers using an exponential notation with -# 4 components: sign, mantissa, base and exponent: -# -# floating_point_number = sign * mantissa * (base ** exponent) -# -# Here: -# 1. sign can be 1 or -1. -# 2. mantissa is a positive fractional number whose integer part is 1 for -# normalized floating point numbers and 0 for denormalized floating point -# numbers. -# 3. base is always 2. -# 4. exponent is an integer in the range [-1022, 1023] for normalized floating -# point numbers and -1022 for denormalized floating point numbers. -# -# The smallest value a normalized floating point number can have is -# -1 * 1.0 * (2 ** -1022) == 2.2250738585072014e-308. -# As mentioned before, IEEE 754 defines how floating point numbers are -# represented using a 64-bit binary number, for this number: -# -# 1. The first bit represents the sign. -# 2. The next 11 bits represent the exponent. -# 3. The next 52 bits represent the mantissa. -# -# The sign is positive if the sign bit is 0 and negative if the sign bit is 1. -# -# There are 11 bits for the exponent -# An IEEE 754 double-precision (64 bit) floating point number is represented -# as: 1 bit for sign, 11 bits for exponent and 52 bits for significand. Since -# these numbers are in a normalized form (in scientific notation), the first -# bit of the significand will always be 1. Because of that, that bit is not -# stored but implicit, to make room for one more bit and more precision. - -SIGNIFICAND_WIDTH = 52 +# IEEE 754 64-bit floating point numbers use 11 bits for the exponent and 52 +# bits for the mantissa. +MANTISSA_WIDTH = 52 EXPONENT_WIDTH = 11 # This mask is equivalent to 52 "1" bits (there are 13 hexadecimal 4-bit "f"s -# in the significand mask, 13 * 4 == 52) or 0xfffffffffffff in hexadecimal. -SIGNIFICAND_MASK = (1 << SIGNIFICAND_WIDTH) - 1 +# in the mantissa mask, 13 * 4 == 52) or 0xfffffffffffff in hexadecimal. +MANTISSA_MASK = (1 << MANTISSA_WIDTH) - 1 -# There are 11 bits for the exponent, but the exponent bias values 0 (11 "0" +# There are 11 bits for the exponent, but the exponent values 0 (11 "0" # bits) and 2047 (11 "1" bits) have special meanings so the exponent range is -# from 1 to 2046. To calculate the exponent value, 1023 is subtracted from the -# exponent, so the exponent value range is from -1022 to +1023. +# from 1 to 2046. To calculate the exponent value, 1023 (the bias) is +# subtracted from the exponent, so the exponent value range is from -1022 to +# +1023. EXPONENT_BIAS = (2 ** (EXPONENT_WIDTH - 1)) - 1 # All the exponent mask bits are set to 1 for the 11 exponent bits. -EXPONENT_MASK = ((1 << EXPONENT_WIDTH) - 1) << SIGNIFICAND_WIDTH +EXPONENT_MASK = ((1 << EXPONENT_WIDTH) - 1) << MANTISSA_WIDTH -# The exponent mask bit is to 1 for the sign bit. -SIGN_MASK = 1 << (EXPONENT_WIDTH + SIGNIFICAND_WIDTH) +# The sign mask has the first bit set to 1 and the rest to 0. +SIGN_MASK = 1 << (EXPONENT_WIDTH + MANTISSA_WIDTH) +# For normalized floating point numbers, the exponent can have a value in the +# range [-1022, 1023]. MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1 MAX_NORMAL_EXPONENT = EXPONENT_BIAS -# Smallest possible normal value (2.2250738585072014e-308) +# The smallest possible normal value is 2.2250738585072014e-308. # This value is the result of using the smallest possible number in the # mantissa, 1.0000000000000000000000000000000000000000000000000000 (52 "0"s in -# the fractional part) = 1.0000000000000000 and a single "1" in the exponent. -# Finally 1.0000000000000000 * 2 ** -1022 = 2.2250738585072014e-308. +# the fractional part) and a single "1" in the exponent. +# Finally 1 * (2 ** -1022) = 2.2250738585072014e-308. MIN_NORMAL_VALUE = float_info.min # Greatest possible normal value (1.7976931348623157e+308) # The binary representation of a float in scientific notation uses (for the -# significand) one bit for the integer part (which is implicit) and 52 bits for +# mantissa) one bit for the integer part (which is implicit) and 52 bits for # the fractional part. Consider a float binary 1.111. It is equal to 1 + 1/2 + -# 1/4 + 1/8. The greatest possible value in the 52-bit significand would be +# 1/4 + 1/8. The greatest possible value in the 52-bit binary mantissa would be # then 1.1111111111111111111111111111111111111111111111111111 (52 "1"s in the -# fractional part) = 1.9999999999999998. Finally, -# 1.9999999999999998 * 2 ** 1023 = 1.7976931348623157e+308. +# fractional part) whose decimal value is 1.9999999999999998. Finally, +# 1.9999999999999998 * (2 ** 1023) = 1.7976931348623157e+308. MAX_NORMAL_VALUE = float_info.max @@ -110,10 +78,10 @@ def get_ieee_754_exponent(value: float) -> int: # # The first bit of the previous binary number is the sign bit: 1 (1 means negative, 0 means positive) # The next 11 bits are the exponent bits: 11111111110 - # The next 52 bits are the significand bits: 1111111111111111111111111111111111111111111111111111 + # The next 52 bits are the mantissa bits: 1111111111111111111111111111111111111111111111111111 # # This step isolates the exponent bits, turning every bit outside - # of the exponent field (sign and significand bits) to 0. + # of the exponent field (sign and mantissa bits) to 0. c_uint64.from_buffer(c_double(-MAX_NORMAL_VALUE)).value & EXPONENT_MASK # For the example this means: @@ -124,11 +92,11 @@ def get_ieee_754_exponent(value: float) -> int: # zero. ) # This step moves the exponent bits to the right, removing the - # significand bits that were set to 0 by the previous step. This + # mantissa bits that were set to 0 by the previous step. This # leaves the IEEE 754 exponent value, ready for the next step. - >> SIGNIFICAND_WIDTH + >> MANTISSA_WIDTH # For the example this means: - # 9214364837600034816 >> SIGNIFICAND_WIDTH == 2046 + # 9214364837600034816 >> MANTISSA_WIDTH == 2046 # bin(2046) == '0b11111111110' # As shown above, these are the original 11 bits that correspond to the # exponent. @@ -140,11 +108,11 @@ def get_ieee_754_exponent(value: float) -> int: # As mentioned in a comment above, the largest value for the exponent is -def get_ieee_754_significand(value: float) -> int: +def get_ieee_754_mantissa(value: float) -> int: return ( c_uint64.from_buffer(c_double(value)).value - # This step isolates the significand bits. There is no need to do any - # bit shifting as the significand bits are already the rightmost field + # This step isolates the mantissa bits. There is no need to do any + # bit shifting as the mantissa bits are already the rightmost field # in an IEEE 754 representation. - & SIGNIFICAND_MASK + & MANTISSA_MASK ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py index 78b5c9c5e6e..d93bc120fe5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py @@ -27,7 +27,7 @@ MIN_NORMAL_EXPONENT, MIN_NORMAL_VALUE, get_ieee_754_exponent, - get_ieee_754_significand, + get_ieee_754_mantissa, ) @@ -104,7 +104,7 @@ def map_to_index(self, value: float) -> int: return self._min_normal_lower_boundary_index - 1 # Exact power-of-two correctness: an optional special case. - if get_ieee_754_significand(value) == 0: + if get_ieee_754_mantissa(value) == 0: exponent = get_ieee_754_exponent(value) return (exponent << self._scale) - 1 From 064bb2b7d7f04f93891580c9cce4a6ee135bacd1 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 15:49:34 +0000 Subject: [PATCH 11/24] Refactor lock definition --- .../exponential_histogram/mapping/__init__.py | 7 +++ .../mapping/exponent_mapping.py | 3 - .../exponential_histogram/test_mapping.py | 57 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py index 27d88f66bff..df617a134a0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. from abc import ABC, abstractmethod +from threading import Lock class Mapping(ABC): @@ -20,6 +21,12 @@ class Mapping(ABC): # pylint: disable=no-member def __new__(cls, scale: int): + if not hasattr(cls, "_mappings"): + cls._mappings = {} + + if not hasattr(cls, "_mappings_lock"): + cls._mappings_lock = Lock() + with cls._mappings_lock: if scale not in cls._mappings: cls._mappings[scale] = super().__new__(cls) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py index a7aa2e2650a..8e975bf857b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -34,9 +34,6 @@ class ExponentMapping(Mapping): - _mappings = {} - _mappings_lock = Lock() - _min_scale = -10 _max_scale = 0 diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py new file mode 100644 index 00000000000..d4542965d10 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py @@ -0,0 +1,57 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase +from math import inf + +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( + Mapping, +) + + +class TestMapping(TestCase): + + def test_lock(self): + + class Child0(Mapping): + def _get_max_scale(self) -> int: + return inf + + def _get_min_scale(self) -> int: + return -inf + + def map_to_index(self, value: float) -> int: + pass + + def get_lower_boundary(self, index: int) -> float: + pass + + class Child1(Mapping): + def _get_max_scale(self) -> int: + return inf + + def _get_min_scale(self) -> int: + return -inf + + def map_to_index(self, value: float) -> int: + pass + + def get_lower_boundary(self, index: int) -> float: + pass + + child_0 = Child0(0) + child_1 = Child1(1) + + self.assertIsNot(child_0._mappings, child_1._mappings) + self.assertIsNot(child_0._mappings_lock, child_1._mappings_lock) From dadd36d81b105ec53ee7a66111f3b0202701dc97 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 21:59:25 +0000 Subject: [PATCH 12/24] Fix wrong fixed value --- .../metrics/_internal/exponential_histogram/mapping/ieee_754.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py index cc1266d2b8e..1a6907dd77b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py @@ -82,7 +82,7 @@ def get_ieee_754_exponent(value: float) -> int: # # This step isolates the exponent bits, turning every bit outside # of the exponent field (sign and mantissa bits) to 0. - c_uint64.from_buffer(c_double(-MAX_NORMAL_VALUE)).value + c_uint64.from_buffer(c_double(value)).value & EXPONENT_MASK # For the example this means: # 18442240474082181119 & EXPONENT_MASK == 9214364837600034816 From b8b99f1b8fa873cb284b0b2d4734edfbb57833bb Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 22:01:30 +0000 Subject: [PATCH 13/24] Fix lint --- .../mapping/logarithm_mapping.py | 27 ++++++------------- .../exponential_histogram/test_mapping.py | 4 +-- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py index d93bc120fe5..85eaf21cd27 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py @@ -81,7 +81,7 @@ def __init__(self, scale: int): ) # self._max_normal_lower_boundary_index is the index such that - # base**index equals the greatest representable lower boundary. An + # base ** index equals the greatest representable lower boundary. An # exponential histogram bucket with this index covers the range # ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. # metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. @@ -94,33 +94,22 @@ def __init__(self, scale: int): def map_to_index(self, value: float) -> int: """ - MapToIndex maps positive floating point values to indexes - corresponding to Scale(). Implementations are not expected - to handle zeros, +Inf, NaN, or negative values. + Maps positive floating point values to indexes corresponding to scale. """ - # Note: we can assume not a 0, Inf, or NaN; positive sign bit. + # value is subnormal if value <= MIN_NORMAL_VALUE: return self._min_normal_lower_boundary_index - 1 - # Exact power-of-two correctness: an optional special case. + # value is an exact power of two. if get_ieee_754_mantissa(value) == 0: exponent = get_ieee_754_exponent(value) return (exponent << self._scale) - 1 - # Non-power of two cases. Use Floor(x) to round the scaled - # logarithm. We could use Ceil(x)-1 to achieve the same - # result, though Ceil() is typically defined as -Floor(-x) - # and typically not performed in hardware, so this is likely - # less code. - index = floor(log(value) * self._scale_factor) - - max_ = self._max_normal_lower_boundary_index - - if index >= max_: - return max_ - - return index + return min( + floor(log(value) * self._scale_factor), + self._max_normal_lower_boundary_index, + ) def get_lower_boundary(self, index: int) -> float: diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py index d4542965d10..22466b4155c 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest import TestCase from math import inf +from unittest import TestCase from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( Mapping, @@ -21,9 +21,7 @@ class TestMapping(TestCase): - def test_lock(self): - class Child0(Mapping): def _get_max_scale(self) -> int: return inf From 572f2d282497855a3b86a55f9d55275147482c5e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 22:20:06 +0000 Subject: [PATCH 14/24] Fix test name --- .../metrics/exponential_histogram/test_exponent_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py index 14c820df3f9..d8ddeabb2a2 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -291,7 +291,7 @@ def test_exponent_mapping_neg_four(self): self.assertEqual(exponent_mapping.map_to_index(2**-976), -62) self.assertEqual(exponent_mapping.map_to_index(2**-975), -61) - def test_exponent_index_mat(self): + def test_exponent_index_max(self): for scale in range( ExponentMapping._min_scale, ExponentMapping._max_scale From adacc0745610a55880dd062ceeb558711a91e13f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 22:21:30 +0000 Subject: [PATCH 15/24] Update opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py Co-authored-by: Aaron Abbott --- .../metrics/exponential_histogram/test_exponent_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py index d8ddeabb2a2..c2f40b3fb10 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -315,7 +315,7 @@ def test_exponent_index_max(self): version_info < (3, 9), reason="math.nextafter is only available for Python >= 3.9", ) - def test_exponent_index_mint(self): + def test_exponent_index_min(self): for scale in range( ExponentMapping._min_scale, ExponentMapping._max_scale + 1 ): From b81c556193b8c0f7e46cbfe8c671eef1fb1fbd50 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 22:24:37 +0000 Subject: [PATCH 16/24] Fix operator separator --- .../test_exponent_mapping.py | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py index c2f40b3fb10..d4eff914e32 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -36,7 +36,7 @@ def rounded_boundary(scale: int, index: int) -> float: - result = 2**index + result = 2 ** index for _ in range(scale, 0): result = result * result @@ -71,13 +71,13 @@ def test_exponent_mapping_zero(self): MAX_NORMAL_EXPONENT, ) self.assertEqual(exponent_mapping.map_to_index(MAX_NORMAL_VALUE), 1023) - self.assertEqual(exponent_mapping.map_to_index(2**1023), 1022) - self.assertEqual(exponent_mapping.map_to_index(2**1022), 1021) + self.assertEqual(exponent_mapping.map_to_index(2 ** 1023), 1022) + self.assertEqual(exponent_mapping.map_to_index(2 ** 1022), 1021) self.assertEqual( - exponent_mapping.map_to_index(hex_1_1 * (2**1023)), 1023 + exponent_mapping.map_to_index(hex_1_1 * (2 ** 1023)), 1023 ) self.assertEqual( - exponent_mapping.map_to_index(hex_1_1 * (2**1022)), 1022 + exponent_mapping.map_to_index(hex_1_1 * (2 ** 1022)), 1022 ) # Testing with values near 1 @@ -94,28 +94,28 @@ def test_exponent_mapping_zero(self): self.assertEqual(exponent_mapping.map_to_index(0.125), -4) # Testing with values near 0 - self.assertEqual(exponent_mapping.map_to_index(2**-1022), -1023) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1022), -1023) self.assertEqual( - exponent_mapping.map_to_index(hex_1_1 * (2**-1022)), -1022 + exponent_mapping.map_to_index(hex_1_1 * (2 ** -1022)), -1022 ) - self.assertEqual(exponent_mapping.map_to_index(2**-1021), -1022) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1021), -1022) self.assertEqual( - exponent_mapping.map_to_index(hex_1_1 * (2**-1021)), -1021 + exponent_mapping.map_to_index(hex_1_1 * (2 ** -1021)), -1021 ) self.assertEqual( - exponent_mapping.map_to_index(2**-1022), MIN_NORMAL_EXPONENT - 1 + exponent_mapping.map_to_index(2 ** -1022), MIN_NORMAL_EXPONENT - 1 ) self.assertEqual( - exponent_mapping.map_to_index(2**-1021), MIN_NORMAL_EXPONENT + exponent_mapping.map_to_index(2 ** -1021), MIN_NORMAL_EXPONENT ) - # The smallest subnormal value in Python is 2 ** -1074 = 5e-324. + # The smallest subnormal value is 2 ** -1074 = 5e-324. # This value is also the result of: # s = 1 # while s / 2: # s = s / 2 # s == 5e-324 self.assertEqual( - exponent_mapping.map_to_index(2**-1074), MIN_NORMAL_EXPONENT - 1 + exponent_mapping.map_to_index(2 ** -1074), MIN_NORMAL_EXPONENT - 1 ) def test_exponent_mapping_min_scale(self): @@ -164,13 +164,13 @@ def test_exponent_mapping_neg_four(self): self.assertEqual(exponent_mapping.map_to_index(float(0x1000)), 0) self.assertEqual( exponent_mapping.map_to_index(float(0x10000)), 0 - ) # base == 2 ** 16 + ) # base == 2 ** 16 self.assertEqual(exponent_mapping.map_to_index(float(0x100000)), 1) self.assertEqual(exponent_mapping.map_to_index(float(0x1000000)), 1) self.assertEqual(exponent_mapping.map_to_index(float(0x10000000)), 1) self.assertEqual( exponent_mapping.map_to_index(float(0x100000000)), 1 - ) # base == 2 ** 32 + ) # base == 2 ** 32 self.assertEqual(exponent_mapping.map_to_index(float(0x1000000000)), 2) self.assertEqual( @@ -181,7 +181,7 @@ def test_exponent_mapping_neg_four(self): ) self.assertEqual( exponent_mapping.map_to_index(float(0x1000000000000)), 2 - ) # base == 2 ** 48 + ) # base == 2 ** 48 self.assertEqual( exponent_mapping.map_to_index(float(0x10000000000000)), 3 @@ -194,7 +194,7 @@ def test_exponent_mapping_neg_four(self): ) self.assertEqual( exponent_mapping.map_to_index(float(0x10000000000000000)), 3 - ) # base == 2 ** 64 + ) # base == 2 ** 64 self.assertEqual( exponent_mapping.map_to_index(float(0x100000000000000000)), 4 @@ -207,7 +207,7 @@ def test_exponent_mapping_neg_four(self): ) self.assertEqual( exponent_mapping.map_to_index(float(0x100000000000000000000)), 4 - ) # base == 2 ** 80 + ) # base == 2 ** 80 self.assertEqual( exponent_mapping.map_to_index(float(0x1000000000000000000000)), 5 ) @@ -218,7 +218,7 @@ def test_exponent_mapping_neg_four(self): self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1000)), -1) self.assertEqual( exponent_mapping.map_to_index(1 / float(0x10000)), -2 - ) # base == 2 ** -16 + ) # base == 2 ** -16 self.assertEqual( exponent_mapping.map_to_index(1 / float(0x100000)), -2 ) @@ -230,7 +230,7 @@ def test_exponent_mapping_neg_four(self): ) self.assertEqual( exponent_mapping.map_to_index(1 / float(0x100000000)), -3 - ) # base == 2 ** -32 + ) # base == 2 ** -32 self.assertEqual( exponent_mapping.map_to_index(1 / float(0x1000000000)), -3 ) @@ -242,7 +242,7 @@ def test_exponent_mapping_neg_four(self): ) self.assertEqual( exponent_mapping.map_to_index(1 / float(0x1000000000000)), -4 - ) # base == 2 ** -32 + ) # base == 2 ** -32 self.assertEqual( exponent_mapping.map_to_index(1 / float(0x10000000000000)), -4 ) @@ -254,42 +254,42 @@ def test_exponent_mapping_neg_four(self): ) self.assertEqual( exponent_mapping.map_to_index(1 / float(0x10000000000000000)), -5 - ) # base == 2 ** -64 + ) # base == 2 ** -64 self.assertEqual( exponent_mapping.map_to_index(1 / float(0x100000000000000000)), -5 ) self.assertEqual(exponent_mapping.map_to_index(float_info.max), 63) - self.assertEqual(exponent_mapping.map_to_index(2**1023), 63) - self.assertEqual(exponent_mapping.map_to_index(2**1019), 63) - self.assertEqual(exponent_mapping.map_to_index(2**1009), 63) - self.assertEqual(exponent_mapping.map_to_index(2**1008), 62) - self.assertEqual(exponent_mapping.map_to_index(2**1007), 62) - self.assertEqual(exponent_mapping.map_to_index(2**1000), 62) - self.assertEqual(exponent_mapping.map_to_index(2**993), 62) - self.assertEqual(exponent_mapping.map_to_index(2**992), 61) - self.assertEqual(exponent_mapping.map_to_index(2**991), 61) - - self.assertEqual(exponent_mapping.map_to_index(2**-1074), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1073), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1072), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1057), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1056), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1041), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1040), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1025), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1024), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1023), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1022), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1009), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1008), -64) - self.assertEqual(exponent_mapping.map_to_index(2**-1007), -63) - self.assertEqual(exponent_mapping.map_to_index(2**-993), -63) - self.assertEqual(exponent_mapping.map_to_index(2**-992), -63) - self.assertEqual(exponent_mapping.map_to_index(2**-991), -62) - self.assertEqual(exponent_mapping.map_to_index(2**-977), -62) - self.assertEqual(exponent_mapping.map_to_index(2**-976), -62) - self.assertEqual(exponent_mapping.map_to_index(2**-975), -61) + self.assertEqual(exponent_mapping.map_to_index(2 ** 1023), 63) + self.assertEqual(exponent_mapping.map_to_index(2 ** 1019), 63) + self.assertEqual(exponent_mapping.map_to_index(2 ** 1009), 63) + self.assertEqual(exponent_mapping.map_to_index(2 ** 1008), 62) + self.assertEqual(exponent_mapping.map_to_index(2 ** 1007), 62) + self.assertEqual(exponent_mapping.map_to_index(2 ** 1000), 62) + self.assertEqual(exponent_mapping.map_to_index(2 ** 993), 62) + self.assertEqual(exponent_mapping.map_to_index(2 ** 992), 61) + self.assertEqual(exponent_mapping.map_to_index(2 ** 991), 61) + + self.assertEqual(exponent_mapping.map_to_index(2 ** -1074), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1073), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1072), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1057), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1056), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1041), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1040), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1025), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1024), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1023), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1022), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1009), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1008), -64) + self.assertEqual(exponent_mapping.map_to_index(2 ** -1007), -63) + self.assertEqual(exponent_mapping.map_to_index(2 ** -993), -63) + self.assertEqual(exponent_mapping.map_to_index(2 ** -992), -63) + self.assertEqual(exponent_mapping.map_to_index(2 ** -991), -62) + self.assertEqual(exponent_mapping.map_to_index(2 ** -977), -62) + self.assertEqual(exponent_mapping.map_to_index(2 ** -976), -62) + self.assertEqual(exponent_mapping.map_to_index(2 ** -975), -61) def test_exponent_index_max(self): @@ -354,17 +354,17 @@ def test_exponent_index_min(self): exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 100), ) self.assertEqual( - correct_min_index, exponent_mapping.map_to_index(2**-1050) + correct_min_index, exponent_mapping.map_to_index(2 ** -1050) ) self.assertEqual( - correct_min_index, exponent_mapping.map_to_index(2**-1073) + correct_min_index, exponent_mapping.map_to_index(2 ** -1073) ) self.assertEqual( correct_min_index, - exponent_mapping.map_to_index(1.1 * (2**-1073)), + exponent_mapping.map_to_index(1.1 * (2 ** -1073)), ) self.assertEqual( - correct_min_index, exponent_mapping.map_to_index(2**-1074) + correct_min_index, exponent_mapping.map_to_index(2 ** -1074) ) with self.assertRaises(MappingUnderflowError): From 82b8d65b82a95927d24e1fd734f6466f281ea35d Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 22:39:38 +0000 Subject: [PATCH 17/24] Rename boundary functions --- .../test_exponent_mapping.py | 8 ++++---- .../test_logarithm_mapping.py | 20 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py index d4eff914e32..d83bf826fd9 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -35,7 +35,7 @@ from math import nextafter -def rounded_boundary(scale: int, index: int) -> float: +def right_boundary(scale: int, index: int) -> float: result = 2 ** index for _ in range(scale, 0): @@ -306,7 +306,7 @@ def test_exponent_index_max(self): boundary = exponent_mapping.get_lower_boundary(index) - self.assertEqual(boundary, rounded_boundary(scale, max_index)) + self.assertEqual(boundary, right_boundary(scale, max_index)) with self.assertRaises(Exception): exponent_mapping.get_lower_boundary(index + 1) @@ -334,11 +334,11 @@ def test_exponent_index_min(self): self.assertEqual(correct_min_index, min_index) - correct_boundary = rounded_boundary(scale, correct_min_index) + correct_boundary = right_boundary(scale, correct_min_index) self.assertEqual(correct_boundary, boundary) self.assertGreater( - rounded_boundary(scale, correct_min_index + 1), boundary + right_boundary(scale, correct_min_index + 1), boundary ) self.assertEqual( diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py index 5230166d3e9..5732f02ea45 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py @@ -30,12 +30,12 @@ ) -def rounded_boundary(scale: int, index: int) -> float: +def left_boundary(scale: int, index: int) -> float: # This is implemented in this way to avoid using a third-party bigfloat # package. The Go implementation uses a bigfloat package that is part of # their standard library. The assumption here is that the smallest float - # available in Python is 2 ** -1022 (from sys.float_info.min). + # available in Python is 2 ** -1022 (from sys.float_info.min). while scale > 0: if index < -1022: index /= 2 @@ -43,7 +43,7 @@ def rounded_boundary(scale: int, index: int) -> float: else: break - result = 2**index + result = 2 ** index for _ in range(scale, 0, -1): result = sqrt(result) @@ -135,7 +135,7 @@ def test_logarithm_boundary(self): self.assertGreaterEqual(index, mapped_index) self.assertInEpsilon( - lower_boundary, rounded_boundary(scale, index), 1e-9 + lower_boundary, left_boundary(scale, index), 1e-9 ) def test_logarithm_index_max(self): @@ -181,10 +181,10 @@ def test_logarithm_index_min(self): correct_min_index = (MIN_NORMAL_EXPONENT << scale) - 1 self.assertEqual(min_index, correct_min_index) - correct_mapped = rounded_boundary(scale, correct_min_index) + correct_mapped = left_boundary(scale, correct_min_index) self.assertLess(correct_mapped, MIN_NORMAL_VALUE) - correct_mapped_upper = rounded_boundary( + correct_mapped_upper = left_boundary( scale, correct_min_index + 1 ) self.assertEqual(correct_mapped_upper, MIN_NORMAL_VALUE) @@ -206,17 +206,17 @@ def test_logarithm_index_min(self): correct_min_index, ) self.assertEqual( - logarithm_mapping.map_to_index(2**-1050), correct_min_index + logarithm_mapping.map_to_index(2 ** -1050), correct_min_index ) self.assertEqual( - logarithm_mapping.map_to_index(2**-1073), correct_min_index + logarithm_mapping.map_to_index(2 ** -1073), correct_min_index ) self.assertEqual( - logarithm_mapping.map_to_index(1.1 * 2**-1073), + logarithm_mapping.map_to_index(1.1 * 2 ** -1073), correct_min_index, ) self.assertEqual( - logarithm_mapping.map_to_index(2**-1074), correct_min_index + logarithm_mapping.map_to_index(2 ** -1074), correct_min_index ) mapped_lower = logarithm_mapping.get_lower_boundary(min_index) From 5c1a89c08b9c3817064d2cd4f124a9ca61dafd08 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 31 Oct 2022 22:54:28 +0000 Subject: [PATCH 18/24] Add links to reference implementation --- .../exponential_histogram/mapping/exponent_mapping.py | 3 ++- .../exponential_histogram/mapping/logarithm_mapping.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py index 8e975bf857b..d033c641a3a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -13,7 +13,6 @@ # limitations under the License. from math import ldexp -from threading import Lock from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( Mapping, @@ -33,6 +32,8 @@ class ExponentMapping(Mapping): + # Reference implementation here: + # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/exponent/exponent.go _min_scale = -10 _max_scale = 0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py index 85eaf21cd27..d9e67d05f20 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py @@ -32,6 +32,8 @@ class LogarithmMapping(Mapping): + # Reference implementation here: + # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go _mappings = {} _mappings_lock = Lock() From 25db75e0fece11d0ef56bc907ff27367059d92e6 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 1 Nov 2022 10:15:46 +0000 Subject: [PATCH 19/24] Fix lint and spelling --- .../exponential_histogram/mapping/ieee_754.md | 2 +- .../test_exponent_mapping.py | 92 +++++++++---------- .../test_logarithm_mapping.py | 14 ++- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md index cf69a197686..a903430a3e8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md @@ -5,7 +5,7 @@ objects using binary numbers. ## Binary Number Fields -The binary numbers used in IEEE 754 can have different lenghts, the length that +The binary numbers used in IEEE 754 can have different lengths, the length that is interesting for the purposes of this project is 64 bits. These binary numbers are made up of 3 contiguous fields of bits, from left to right: diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py index d83bf826fd9..634c6b9e979 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -36,7 +36,7 @@ def right_boundary(scale: int, index: int) -> float: - result = 2 ** index + result = 2**index for _ in range(scale, 0): result = result * result @@ -71,13 +71,13 @@ def test_exponent_mapping_zero(self): MAX_NORMAL_EXPONENT, ) self.assertEqual(exponent_mapping.map_to_index(MAX_NORMAL_VALUE), 1023) - self.assertEqual(exponent_mapping.map_to_index(2 ** 1023), 1022) - self.assertEqual(exponent_mapping.map_to_index(2 ** 1022), 1021) + self.assertEqual(exponent_mapping.map_to_index(2**1023), 1022) + self.assertEqual(exponent_mapping.map_to_index(2**1022), 1021) self.assertEqual( - exponent_mapping.map_to_index(hex_1_1 * (2 ** 1023)), 1023 + exponent_mapping.map_to_index(hex_1_1 * (2**1023)), 1023 ) self.assertEqual( - exponent_mapping.map_to_index(hex_1_1 * (2 ** 1022)), 1022 + exponent_mapping.map_to_index(hex_1_1 * (2**1022)), 1022 ) # Testing with values near 1 @@ -94,19 +94,19 @@ def test_exponent_mapping_zero(self): self.assertEqual(exponent_mapping.map_to_index(0.125), -4) # Testing with values near 0 - self.assertEqual(exponent_mapping.map_to_index(2 ** -1022), -1023) + self.assertEqual(exponent_mapping.map_to_index(2**-1022), -1023) self.assertEqual( - exponent_mapping.map_to_index(hex_1_1 * (2 ** -1022)), -1022 + exponent_mapping.map_to_index(hex_1_1 * (2**-1022)), -1022 ) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1021), -1022) + self.assertEqual(exponent_mapping.map_to_index(2**-1021), -1022) self.assertEqual( - exponent_mapping.map_to_index(hex_1_1 * (2 ** -1021)), -1021 + exponent_mapping.map_to_index(hex_1_1 * (2**-1021)), -1021 ) self.assertEqual( - exponent_mapping.map_to_index(2 ** -1022), MIN_NORMAL_EXPONENT - 1 + exponent_mapping.map_to_index(2**-1022), MIN_NORMAL_EXPONENT - 1 ) self.assertEqual( - exponent_mapping.map_to_index(2 ** -1021), MIN_NORMAL_EXPONENT + exponent_mapping.map_to_index(2**-1021), MIN_NORMAL_EXPONENT ) # The smallest subnormal value is 2 ** -1074 = 5e-324. # This value is also the result of: @@ -115,7 +115,7 @@ def test_exponent_mapping_zero(self): # s = s / 2 # s == 5e-324 self.assertEqual( - exponent_mapping.map_to_index(2 ** -1074), MIN_NORMAL_EXPONENT - 1 + exponent_mapping.map_to_index(2**-1074), MIN_NORMAL_EXPONENT - 1 ) def test_exponent_mapping_min_scale(self): @@ -260,36 +260,36 @@ def test_exponent_mapping_neg_four(self): ) self.assertEqual(exponent_mapping.map_to_index(float_info.max), 63) - self.assertEqual(exponent_mapping.map_to_index(2 ** 1023), 63) - self.assertEqual(exponent_mapping.map_to_index(2 ** 1019), 63) - self.assertEqual(exponent_mapping.map_to_index(2 ** 1009), 63) - self.assertEqual(exponent_mapping.map_to_index(2 ** 1008), 62) - self.assertEqual(exponent_mapping.map_to_index(2 ** 1007), 62) - self.assertEqual(exponent_mapping.map_to_index(2 ** 1000), 62) - self.assertEqual(exponent_mapping.map_to_index(2 ** 993), 62) - self.assertEqual(exponent_mapping.map_to_index(2 ** 992), 61) - self.assertEqual(exponent_mapping.map_to_index(2 ** 991), 61) - - self.assertEqual(exponent_mapping.map_to_index(2 ** -1074), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1073), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1072), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1057), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1056), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1041), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1040), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1025), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1024), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1023), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1022), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1009), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1008), -64) - self.assertEqual(exponent_mapping.map_to_index(2 ** -1007), -63) - self.assertEqual(exponent_mapping.map_to_index(2 ** -993), -63) - self.assertEqual(exponent_mapping.map_to_index(2 ** -992), -63) - self.assertEqual(exponent_mapping.map_to_index(2 ** -991), -62) - self.assertEqual(exponent_mapping.map_to_index(2 ** -977), -62) - self.assertEqual(exponent_mapping.map_to_index(2 ** -976), -62) - self.assertEqual(exponent_mapping.map_to_index(2 ** -975), -61) + self.assertEqual(exponent_mapping.map_to_index(2**1023), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1019), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1009), 63) + self.assertEqual(exponent_mapping.map_to_index(2**1008), 62) + self.assertEqual(exponent_mapping.map_to_index(2**1007), 62) + self.assertEqual(exponent_mapping.map_to_index(2**1000), 62) + self.assertEqual(exponent_mapping.map_to_index(2**993), 62) + self.assertEqual(exponent_mapping.map_to_index(2**992), 61) + self.assertEqual(exponent_mapping.map_to_index(2**991), 61) + + self.assertEqual(exponent_mapping.map_to_index(2**-1074), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1073), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1072), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1057), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1056), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1041), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1040), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1025), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1024), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1023), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1022), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1009), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1008), -64) + self.assertEqual(exponent_mapping.map_to_index(2**-1007), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-993), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-992), -63) + self.assertEqual(exponent_mapping.map_to_index(2**-991), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-977), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-976), -62) + self.assertEqual(exponent_mapping.map_to_index(2**-975), -61) def test_exponent_index_max(self): @@ -354,17 +354,17 @@ def test_exponent_index_min(self): exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 100), ) self.assertEqual( - correct_min_index, exponent_mapping.map_to_index(2 ** -1050) + correct_min_index, exponent_mapping.map_to_index(2**-1050) ) self.assertEqual( - correct_min_index, exponent_mapping.map_to_index(2 ** -1073) + correct_min_index, exponent_mapping.map_to_index(2**-1073) ) self.assertEqual( correct_min_index, - exponent_mapping.map_to_index(1.1 * (2 ** -1073)), + exponent_mapping.map_to_index(1.1 * (2**-1073)), ) self.assertEqual( - correct_min_index, exponent_mapping.map_to_index(2 ** -1074) + correct_min_index, exponent_mapping.map_to_index(2**-1074) ) with self.assertRaises(MappingUnderflowError): diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py index 5732f02ea45..2051263eddc 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py @@ -43,7 +43,7 @@ def left_boundary(scale: int, index: int) -> float: else: break - result = 2 ** index + result = 2**index for _ in range(scale, 0, -1): result = sqrt(result) @@ -184,9 +184,7 @@ def test_logarithm_index_min(self): correct_mapped = left_boundary(scale, correct_min_index) self.assertLess(correct_mapped, MIN_NORMAL_VALUE) - correct_mapped_upper = left_boundary( - scale, correct_min_index + 1 - ) + correct_mapped_upper = left_boundary(scale, correct_min_index + 1) self.assertEqual(correct_mapped_upper, MIN_NORMAL_VALUE) mapped = logarithm_mapping.get_lower_boundary(min_index + 1) @@ -206,17 +204,17 @@ def test_logarithm_index_min(self): correct_min_index, ) self.assertEqual( - logarithm_mapping.map_to_index(2 ** -1050), correct_min_index + logarithm_mapping.map_to_index(2**-1050), correct_min_index ) self.assertEqual( - logarithm_mapping.map_to_index(2 ** -1073), correct_min_index + logarithm_mapping.map_to_index(2**-1073), correct_min_index ) self.assertEqual( - logarithm_mapping.map_to_index(1.1 * 2 ** -1073), + logarithm_mapping.map_to_index(1.1 * 2**-1073), correct_min_index, ) self.assertEqual( - logarithm_mapping.map_to_index(2 ** -1074), correct_min_index + logarithm_mapping.map_to_index(2**-1074), correct_min_index ) mapped_lower = logarithm_mapping.get_lower_boundary(min_index) From acd1d39a760853eaab31670f2a7b4596a5c7f5d9 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 1 Nov 2022 11:09:33 +0000 Subject: [PATCH 20/24] Revert "Refactor lock definition" This reverts commit 064bb2b7d7f04f93891580c9cce4a6ee135bacd1. --- .../exponential_histogram/mapping/__init__.py | 15 ++--- .../mapping/exponent_mapping.py | 4 ++ .../exponential_histogram/test_mapping.py | 55 ------------------- 3 files changed, 12 insertions(+), 62 deletions(-) delete mode 100644 opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py index df617a134a0..7b7eb4a3214 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -13,21 +13,22 @@ # limitations under the License. from abc import ABC, abstractmethod -from threading import Lock class Mapping(ABC): + """ + Parent class for `LogarithmMapping` and `ExponentialMapping`. + """ # pylint: disable=no-member def __new__(cls, scale: int): - if not hasattr(cls, "_mappings"): - cls._mappings = {} - - if not hasattr(cls, "_mappings_lock"): - cls._mappings_lock = Lock() - with cls._mappings_lock: + # cls._mappings and cls._mappings_lock are implemented in each of + # the child classes as a dictionary and a lock, respectively. They + # are not instantiated here because that would lead to both child + # classes having the same instance of cls._mappings and + # cls._mappings_lock. if scale not in cls._mappings: cls._mappings[scale] = super().__new__(cls) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py index d033c641a3a..116dd710c50 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -13,6 +13,7 @@ # limitations under the License. from math import ldexp +from threading import Lock from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( Mapping, @@ -35,6 +36,9 @@ class ExponentMapping(Mapping): # Reference implementation here: # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/exponent/exponent.go + _mappings = {} + _mappings_lock = Lock() + _min_scale = -10 _max_scale = 0 diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py deleted file mode 100644 index 22466b4155c..00000000000 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_mapping.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from math import inf -from unittest import TestCase - -from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( - Mapping, -) - - -class TestMapping(TestCase): - def test_lock(self): - class Child0(Mapping): - def _get_max_scale(self) -> int: - return inf - - def _get_min_scale(self) -> int: - return -inf - - def map_to_index(self, value: float) -> int: - pass - - def get_lower_boundary(self, index: int) -> float: - pass - - class Child1(Mapping): - def _get_max_scale(self) -> int: - return inf - - def _get_min_scale(self) -> int: - return -inf - - def map_to_index(self, value: float) -> int: - pass - - def get_lower_boundary(self, index: int) -> float: - pass - - child_0 = Child0(0) - child_1 = Child1(1) - - self.assertIsNot(child_0._mappings, child_1._mappings) - self.assertIsNot(child_0._mappings_lock, child_1._mappings_lock) From 82dcdf60e1f4f44fa6bb7cec053b53fea680cc76 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 1 Nov 2022 13:45:20 +0000 Subject: [PATCH 21/24] Refactor initialization --- .../exponential_histogram/mapping/__init__.py | 5 ++++- .../mapping/exponent_mapping.py | 6 ++++-- .../mapping/logarithm_mapping.py | 5 +++-- .../test_exponent_mapping.py | 17 +++++++++++++++++ .../test_logarithm_mapping.py | 17 +++++++++++++++++ 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py index 7b7eb4a3214..ff2d7beb3b0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -31,10 +31,13 @@ def __new__(cls, scale: int): # cls._mappings_lock. if scale not in cls._mappings: cls._mappings[scale] = super().__new__(cls) + cls._mappings[scale]._init(scale) return cls._mappings[scale] - def __init__(self, scale: int) -> None: + @abstractmethod + def _init(self, scale: int) -> None: + # pylint: disable=attribute-defined-outside-init if scale > self._get_max_scale(): raise Exception(f"scale is larger than {self._max_scale}") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py index 116dd710c50..297bb7a4831 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py @@ -53,8 +53,10 @@ def _get_max_scale(self): # a logarithm mapping for larger scales. return 0 - def __init__(self, scale: int): - super().__init__(scale) + def _init(self, scale: int): + # pylint: disable=attribute-defined-outside-init + + super()._init(scale) # self._min_normal_lower_boundary_index is the largest index such that # base ** index < MIN_NORMAL_VALUE and diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py index d9e67d05f20..5abf9238b9b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py @@ -53,9 +53,10 @@ def _get_max_scale(self): # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go#L32-L45 return self._max_scale - def __init__(self, scale: int): + def _init(self, scale: int): + # pylint: disable=attribute-defined-outside-init - super().__init__(scale) + super()._init(scale) # self._scale_factor is defined as a multiplier because multiplication # is faster than division. self._scale_factor is defined as: diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py index 634c6b9e979..ae06d963abd 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py @@ -15,6 +15,7 @@ from math import inf from sys import float_info, version_info from unittest import TestCase +from unittest.mock import patch from pytest import mark @@ -50,6 +51,22 @@ def test_singleton(self): self.assertIs(ExponentMapping(-3), ExponentMapping(-3)) self.assertIsNot(ExponentMapping(-3), ExponentMapping(-5)) + @patch( + "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." + "exponent_mapping.ExponentMapping._mappings", + new={}, + ) + @patch( + "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." + "exponent_mapping.ExponentMapping._init" + ) + def test_init_called_once(self, mock_init): + + ExponentMapping(-3) + ExponentMapping(-3) + + mock_init.assert_called_once() + def test_exponent_mapping_0(self): try: diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py index 2051263eddc..1fd18845bb6 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_logarithm_mapping.py @@ -14,6 +14,7 @@ from math import sqrt from unittest import TestCase +from unittest.mock import patch from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( MappingOverflowError, @@ -56,6 +57,22 @@ def assertInEpsilon(self, first, second, epsilon): self.assertLessEqual(first, (second * (1 + epsilon))) self.assertGreaterEqual(first, (second * (1 - epsilon))) + @patch( + "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." + "logarithm_mapping.LogarithmMapping._mappings", + new={}, + ) + @patch( + "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." + "logarithm_mapping.LogarithmMapping._init" + ) + def test_init_called_once(self, mock_init): + + LogarithmMapping(3) + LogarithmMapping(3) + + mock_init.assert_called_once() + def test_invalid_scale(self): with self.assertRaises(Exception): LogarithmMapping(-1) From 375cd75fcbc26dc9e2fa6bded55b7679193ea12e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 1 Nov 2022 14:12:52 +0000 Subject: [PATCH 22/24] Fix math format --- .../_internal/exponential_histogram/mapping/ieee_754.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md index a903430a3e8..48142029ff7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md @@ -51,7 +51,7 @@ $sign$ can have 2 values: ##### Normalized Floating Point Numbers $mantissa$ is a positive fractional number whose integer part is $1$, for example -$1.2345...$. The `mantissa` bits represent only the fractional part and the +$1.2345 \dots$. The `mantissa` bits represent only the fractional part and the $mantissa$ value can be calculated as: $$mantissa = 1 + \sum_{i=1}^{52} b_{i} \times 2^{-i} = 1 + \frac{b_{1}}{2^{1}} + \frac{b_{2}}{2^{2}} + \dots + \frac{b_{63}}{2^{63}} + \frac{b_{52}}{2^{52}}$$ @@ -64,7 +64,7 @@ Where $b_{i}$ is: ##### Denormalized Floating Point Numbers $mantissa$ is a positive fractional number whose integer part is $0$, for example -$0.12345...$. The `mantissa` bits represent only the fractional part and the +$0.12345 \dots$. The `mantissa` bits represent only the fractional part and the $mantissa$ value can be calculated as: $$mantissa = \sum_{i=1}^{52} b_{i} \times 2^{-i} = \frac{b_{1}}{2^{1}} + \frac{b_{2}}{2^{2}} + \dots + \frac{b_{63}}{2^{63}} + \frac{b_{52}}{2^{52}}$$ @@ -108,7 +108,7 @@ $$1 \times 1.0\dots0 \times 2^{-1022} = 2.2250738585072014 \times 10^{-308}$$ Since normalized floating point numbers always have a $1$ as the integer part of the $mantissa$, then smaller values can be achieved by using the smallest possible exponent -($-1022$) and a $0$ in the integer part of the $mantissa$, but significant digits are lost. +( $-1022$ ) and a $0$ in the integer part of the $mantissa$, but significant digits are lost. The smallest absolute value a denormalized floating point number can have is calculated like this: From e463890a311d00ec0558f6614eeea90a14733d4e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 1 Nov 2022 14:12:52 +0000 Subject: [PATCH 23/24] Rename to normal and denormal --- .../exponential_histogram/mapping/__init__.py | 2 +- .../exponential_histogram/mapping/errors.py | 2 +- .../exponential_histogram/mapping/ieee_754.md | 18 +++++++++--------- .../exponential_histogram/mapping/ieee_754.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py index ff2d7beb3b0..d8c780cf404 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py @@ -79,7 +79,7 @@ def get_lower_boundary(self, index: int) -> float: """ Returns the lower boundary of a given bucket index. The index is expected to map onto a range that is at least partially inside the - range of normalized floating point values. If the corresponding + range of normal floating point values. If the corresponding bucket's upper boundary is less than or equal to 2 ** -1022, :class:`~opentelemetry.sdk.metrics.MappingUnderflowError` will be raised. If the corresponding bucket's lower boundary is greater diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py index 54edb5fcd0b..477ed6f0f51 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py @@ -16,7 +16,7 @@ class MappingUnderflowError(Exception): """ Raised when computing the lower boundary of an index that maps into a - denormalized floating point value. + denormal floating point value. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md index 48142029ff7..ba9601bdf99 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md @@ -29,7 +29,7 @@ notation with 4 components: $sign$, $mantissa$, $base$ and $exponent$: $$f = sign \times mantissa \times base ^ {exponent}$$ There are two possible representations of floating point numbers: -_normalized_ and _denormalized_, which have different valid values for +_normal_ and _denormal_, which have different valid values for their $mantissa$ and $exponent$ fields. ### Binary Representation @@ -48,7 +48,7 @@ $sign$ can have 2 values: #### Mantissa -##### Normalized Floating Point Numbers +##### Normal Floating Point Numbers $mantissa$ is a positive fractional number whose integer part is $1$, for example $1.2345 \dots$. The `mantissa` bits represent only the fractional part and the @@ -61,7 +61,7 @@ Where $b_{i}$ is: 1. $0$ if the bit at the position `i - 1` is `0`. 2. $1$ if the bit at the position `i - 1` is `1`. -##### Denormalized Floating Point Numbers +##### Denormal Floating Point Numbers $mantissa$ is a positive fractional number whose integer part is $0$, for example $0.12345 \dots$. The `mantissa` bits represent only the fractional part and the @@ -76,7 +76,7 @@ Where $b_{i}$ is: #### Exponent -##### Normalized Floating Point Numbers +##### Normal Floating Point Numbers Only the following bit sequences are allowed: `00000000001` to `11111111110`. That is, there must be at least one `0` and one `1` in the exponent bits. @@ -95,22 +95,22 @@ exponent are: So, $exponent$ is an integer in the range $\left[-1022, 1023\right]$. -##### Denormalized Floating Point Numbers +##### Denormal Floating Point Numbers $exponent$ is always $-1022$. Nevertheless, it is always represented as `00000000000`. -### Normalized and Denormalized Floating Point Numbers +### Normal and Denormal Floating Point Numbers -The smallest absolute value a normalized floating point number can have is calculated +The smallest absolute value a normal floating point number can have is calculated like this: $$1 \times 1.0\dots0 \times 2^{-1022} = 2.2250738585072014 \times 10^{-308}$$ -Since normalized floating point numbers always have a $1$ as the integer part of the +Since normal floating point numbers always have a $1$ as the integer part of the $mantissa$, then smaller values can be achieved by using the smallest possible exponent ( $-1022$ ) and a $0$ in the integer part of the $mantissa$, but significant digits are lost. -The smallest absolute value a denormalized floating point number can have is calculated +The smallest absolute value a denormal floating point number can have is calculated like this: $$1 \times 2^{-52} \times 2^{-1022} = 5 \times 10^{-324}$$ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py index 1a6907dd77b..9503b57c0e0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py @@ -37,7 +37,7 @@ # The sign mask has the first bit set to 1 and the rest to 0. SIGN_MASK = 1 << (EXPONENT_WIDTH + MANTISSA_WIDTH) -# For normalized floating point numbers, the exponent can have a value in the +# For normal floating point numbers, the exponent can have a value in the # range [-1022, 1023]. MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1 MAX_NORMAL_EXPONENT = EXPONENT_BIAS From 9855df6f3424353e7a00932eeba6dea44c770eb8 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 1 Nov 2022 17:06:36 +0000 Subject: [PATCH 24/24] Remove errors from public interface --- opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 5b979a1ae54..2219bc35c52 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -21,10 +21,6 @@ from opentelemetry.sdk.metrics._internal.exceptions import ( # noqa: F401 MetricsTimeoutError, ) -from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( # noqa: F401 - MappingOverflowError, - MappingUnderflowError, -) from opentelemetry.sdk.metrics._internal.instrument import ( # noqa: F401 Counter, Histogram,