Skip to content

Commit

Permalink
Add exponent and logarithm mappings
Browse files Browse the repository at this point in the history
Fixes #2957
  • Loading branch information
ocelotl committed Oct 6, 2022
1 parent cd4ccab commit 18aa125
Show file tree
Hide file tree
Showing 10 changed files with 1,378 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-0.34b0...HEAD)

- Add logarithm and exponent mappings
([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960))
- Update explicit histogram bucket boundaries
([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947))

Expand Down
Original file line number Diff line number Diff line change
@@ -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
"""
Original file line number Diff line number Diff line change
@@ -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.
"""
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 18aa125

Please sign in to comment.