|
| 1 | +# Copyright The OpenTelemetry Authors |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +from math import ldexp |
| 16 | +from threading import Lock |
| 17 | + |
| 18 | +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( |
| 19 | + Mapping, |
| 20 | +) |
| 21 | +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( |
| 22 | + MappingOverflowError, |
| 23 | + MappingUnderflowError, |
| 24 | +) |
| 25 | +from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( |
| 26 | + MANTISSA_WIDTH, |
| 27 | + MAX_NORMAL_EXPONENT, |
| 28 | + MIN_NORMAL_EXPONENT, |
| 29 | + MIN_NORMAL_VALUE, |
| 30 | + get_ieee_754_exponent, |
| 31 | + get_ieee_754_mantissa, |
| 32 | +) |
| 33 | + |
| 34 | + |
| 35 | +class ExponentMapping(Mapping): |
| 36 | + # Reference implementation here: |
| 37 | + # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/exponent/exponent.go |
| 38 | + |
| 39 | + _mappings = {} |
| 40 | + _mappings_lock = Lock() |
| 41 | + |
| 42 | + _min_scale = -10 |
| 43 | + _max_scale = 0 |
| 44 | + |
| 45 | + def _get_min_scale(self): |
| 46 | + # _min_scale defines the point at which the exponential mapping |
| 47 | + # function becomes useless for 64-bit floats. With scale -10, ignoring |
| 48 | + # subnormal values, bucket indices range from -1 to 1. |
| 49 | + return -10 |
| 50 | + |
| 51 | + def _get_max_scale(self): |
| 52 | + # _max_scale is the largest scale supported by exponential mapping. Use |
| 53 | + # a logarithm mapping for larger scales. |
| 54 | + return 0 |
| 55 | + |
| 56 | + def _init(self, scale: int): |
| 57 | + # pylint: disable=attribute-defined-outside-init |
| 58 | + |
| 59 | + super()._init(scale) |
| 60 | + |
| 61 | + # self._min_normal_lower_boundary_index is the largest index such that |
| 62 | + # base ** index < MIN_NORMAL_VALUE and |
| 63 | + # base ** (index + 1) >= MIN_NORMAL_VALUE. An exponential histogram |
| 64 | + # bucket with this index covers the range |
| 65 | + # (base ** index, base (index + 1)], including MIN_NORMAL_VALUE. This |
| 66 | + # is the smallest valid index that contains at least one normal value. |
| 67 | + index = MIN_NORMAL_EXPONENT >> -self._scale |
| 68 | + |
| 69 | + if -self._scale < 2: |
| 70 | + # For scales -1 and 0, the maximum value 2 ** -1022 is a |
| 71 | + # power-of-two multiple, meaning base ** index == MIN_NORMAL_VALUE. |
| 72 | + # Subtracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE. |
| 73 | + index -= 1 |
| 74 | + |
| 75 | + self._min_normal_lower_boundary_index = index |
| 76 | + |
| 77 | + # self._max_normal_lower_boundary_index is the index such that |
| 78 | + # base**index equals the greatest representable lower boundary. An |
| 79 | + # exponential histogram bucket with this index covers the range |
| 80 | + # ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. |
| 81 | + # metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. |
| 82 | + # This bucket is incomplete, since the upper boundary cannot be |
| 83 | + # represented. One greater than this index corresponds with the bucket |
| 84 | + # containing values > 2 ** 1024. |
| 85 | + self._max_normal_lower_boundary_index = ( |
| 86 | + MAX_NORMAL_EXPONENT >> -self._scale |
| 87 | + ) |
| 88 | + |
| 89 | + def map_to_index(self, value: float) -> int: |
| 90 | + if value < MIN_NORMAL_VALUE: |
| 91 | + return self._min_normal_lower_boundary_index |
| 92 | + |
| 93 | + exponent = get_ieee_754_exponent(value) |
| 94 | + |
| 95 | + # Positive integers are represented in binary as having an infinite |
| 96 | + # amount of leading zeroes, for example 2 is represented as ...00010. |
| 97 | + |
| 98 | + # A negative integer -x is represented in binary as the complement of |
| 99 | + # (x - 1). For example, -4 is represented as the complement of 4 - 1 |
| 100 | + # == 3. 3 is represented as ...00011. Its compliment is ...11100, the |
| 101 | + # binary representation of -4. |
| 102 | + |
| 103 | + # get_ieee_754_mantissa(value) gets the positive integer made up |
| 104 | + # from the rightmost MANTISSA_WIDTH bits (the mantissa) of the IEEE |
| 105 | + # 754 representation of value. If value is an exact power of 2, all |
| 106 | + # these MANTISSA_WIDTH bits would be all zeroes, and when 1 is |
| 107 | + # subtracted the resulting value is -1. The binary representation of |
| 108 | + # -1 is ...111, so when these bits are right shifted MANTISSA_WIDTH |
| 109 | + # places, the resulting value for correction is -1. If value is not an |
| 110 | + # exact power of 2, at least one of the rightmost MANTISSA_WIDTH |
| 111 | + # bits would be 1 (even for values whose decimal part is 0, like 5.0 |
| 112 | + # since the IEEE 754 of such number is too the product of a power of 2 |
| 113 | + # (defined in the exponent part of the IEEE 754 representation) and the |
| 114 | + # value defined in the mantissa). Having at least one of the rightmost |
| 115 | + # MANTISSA_WIDTH bit being 1 means that get_ieee_754(value) will |
| 116 | + # always be greater or equal to 1, and when 1 is subtracted, the |
| 117 | + # result will be greater or equal to 0, whose representation in binary |
| 118 | + # will be of at most MANTISSA_WIDTH ones that have an infinite |
| 119 | + # amount of leading zeroes. When those MANTISSA_WIDTH bits are |
| 120 | + # shifted to the right MANTISSA_WIDTH places, the resulting value |
| 121 | + # will be 0. |
| 122 | + |
| 123 | + # In summary, correction will be -1 if value is a power of 2, 0 if not. |
| 124 | + |
| 125 | + # FIXME Document why we can assume value will not be 0, inf, or NaN. |
| 126 | + correction = (get_ieee_754_mantissa(value) - 1) >> MANTISSA_WIDTH |
| 127 | + |
| 128 | + return (exponent + correction) >> -self._scale |
| 129 | + |
| 130 | + def get_lower_boundary(self, index: int) -> float: |
| 131 | + if index < self._min_normal_lower_boundary_index: |
| 132 | + raise MappingUnderflowError() |
| 133 | + |
| 134 | + if index > self._max_normal_lower_boundary_index: |
| 135 | + raise MappingOverflowError() |
| 136 | + |
| 137 | + return ldexp(1, index << -self._scale) |
| 138 | + |
| 139 | + @property |
| 140 | + def scale(self) -> int: |
| 141 | + return self._scale |
0 commit comments