Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore the exponential mapping functions #3243

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions sdk/metric/metricdata/exponential/README.md
@@ -0,0 +1,32 @@
# Base-2 Exponential Histogram

## Design

This document is a placeholder for the future when this directory
contains both the mapping and data structure of the OpenTelemetry
Exponential Histogram. The complete prototype for this data structure
and the SDK integration has been seen in draft PRs
[2393](https://github.com/open-telemetry/opentelemetry-go/pull/2393),
[3022](https://github.com/open-telemetry/opentelemetry-go/pull/3022),
and [3174](https://github.com/open-telemetry/opentelemetry-go/pull/3174).

Only the mapping functions have been made available at this time. The
equations tested here are specified in the [data model for Exponential
Histogram data points](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#exponentialhistogram).

### Mapping function

There are two mapping functions used, depending on the sign of the
scale. Negative and zero scales use the `mapping/exponent` mapping
function, which computes the bucket index directly from the bits of
the `float64` exponent. This mapping function is used with scale `-10
<= scale <= 0`. Scales smaller than -10 map the entire normal
`float64` number range into a single bucket, thus are not considered
useful.

The `mapping/logarithm` mapping function uses `math.Log(value)` times
the scaling factor `math.Ldexp(math.Log2E, scale)`. This mapping
function is used with `0 < scale <= 20`. The maximum scale is
selected because at scale 21, simply, it becomes difficult to test
correctness--at this point `math.MaxFloat64` maps to index
`math.MaxInt32` and the `math/big` logic used in testing breaks down.
127 changes: 127 additions & 0 deletions sdk/metric/metricdata/exponential/mapping/exponent/exponent.go
@@ -0,0 +1,127 @@
// 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.

package exponent // import "go.opentelemetry.io/otel/sdk/metric/metricdata/exponential/mapping/exponent"

import (
"fmt"
"math"

"go.opentelemetry.io/otel/sdk/metric/metricdata/exponential/mapping"
"go.opentelemetry.io/otel/sdk/metric/metricdata/exponential/mapping/internal"
)

const (
// MinScale defines the point at which the exponential mapping
// function becomes useless for float64. With scale -10, ignoring
// subnormal values, bucket indices range from -1 to 1.
MinScale int32 = -10

// MaxScale is the largest scale supported in this code. Use
// ../logarithm for larger scales.
MaxScale int32 = 0
)

type exponentMapping struct {
shift uint8 // equals negative scale
}

// exponentMapping is used for negative scales, effectively a
// mapping of the base-2 logarithm of the exponent.
var prebuiltMappings = [-MinScale + 1]exponentMapping{
{10},
{9},
{8},
{7},
{6},
{5},
{4},
{3},
{2},
{1},
{0},
}

// NewMapping constructs an exponential mapping function, used for scales <= 0.
func NewMapping(scale int32) (mapping.Mapping, error) {
if scale > MaxScale {
return nil, fmt.Errorf("exponent mapping requires scale <= 0")
}
if scale < MinScale {
return nil, fmt.Errorf("scale too low")
}
return &prebuiltMappings[scale-MinScale], nil
}

// minNormalLowerBoundaryIndex is the largest index such that
// base**index is <= MinValue. A histogram bucket with this index
// covers the range (base**index, base**(index+1)], including
// MinValue.
func (e *exponentMapping) minNormalLowerBoundaryIndex() int32 {
idx := int32(internal.MinNormalExponent) >> e.shift
if e.shift < 2 {
// For scales -1 and 0 the minimum value 2**-1022
// is a power-of-two multiple, meaning it belongs
// to the index one less.
idx--
}
return idx
}

// maxNormalLowerBoundaryIndex is the index such that base**index
// equals the largest representable boundary. A histogram bucket with this
// index covers the range (0x1p+1024/base, 0x1p+1024], which includes
// MaxValue; note that this bucket is incomplete, since the upper
// boundary cannot be represented. One greater than this index
// corresponds with the bucket containing values > 0x1p1024.
func (e *exponentMapping) maxNormalLowerBoundaryIndex() int32 {
return int32(internal.MaxNormalExponent) >> e.shift
}

// MapToIndex implements mapping.Mapping.
func (e *exponentMapping) MapToIndex(value float64) int32 {
// Note: we can assume not a 0, Inf, or NaN; positive sign bit.
if value < internal.MinValue {
return e.minNormalLowerBoundaryIndex()
}

// Extract the raw exponent.
rawExp := internal.GetNormalBase2(value)

// In case the value is an exact power of two, compute a
// correction of -1:
correction := int32((internal.GetSignificand(value) - 1) >> internal.SignificandWidth)

// Note: bit-shifting does the right thing for negative
// exponents, e.g., -1 >> 1 == -1.
return (rawExp + correction) >> e.shift
}

// LowerBoundary implements mapping.Mapping.
func (e *exponentMapping) LowerBoundary(index int32) (float64, error) {
if min := e.minNormalLowerBoundaryIndex(); index < min {
return 0, mapping.ErrUnderflow
}

if max := e.maxNormalLowerBoundaryIndex(); index > max {
return 0, mapping.ErrOverflow
}

return math.Ldexp(1, int(index<<e.shift)), nil
}

// Scale implements mapping.Mapping.
func (e *exponentMapping) Scale() int32 {
return -int32(e.shift)
}