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

Change the inclusivity of exponential histogram bounds #2633

Merged
merged 23 commits into from
Aug 12, 2022
Merged
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a90cbd7
Change the exponential histogram boundary condition
Jun 28, 2022
ca0b140
Merge branch 'main' of github.com:open-telemetry/opentelemetry-specif…
Jul 5, 2022
409ba17
Merge branch 'main' of github.com:open-telemetry/opentelemetry-specif…
Jul 7, 2022
f9f1b17
remove lookup table; untabify; derive the scaling factor for positive…
Jul 7, 2022
e982a80
typo
Jul 7, 2022
76125e7
remove mention of table lookup
Jul 7, 2022
53d26d6
format subscripts
Jul 7, 2022
d65a076
deformat
Jul 7, 2022
969d0b6
moreformat
Jul 7, 2022
abd5362
typo
Jul 7, 2022
8a37196
Update specification/metrics/data-model.md
jmacd Jul 18, 2022
e68e7bf
Merge branch 'main' of github.com:open-telemetry/opentelemetry-specif…
Jul 27, 2022
8215aaf
reiley's fixes
Jul 27, 2022
b5d24a7
Merge branch 'jmacd/histobounds' of github.com:jmacd/opentelemetry-sp…
Jul 27, 2022
87577de
lengthen the explanation
Jul 27, 2022
68c75e2
lint
Jul 27, 2022
aa45c6d
Merge branch 'main' of github.com:open-telemetry/opentelemetry-specif…
Jul 27, 2022
b0014b0
Update specification/metrics/data-model.md
jmacd Jul 28, 2022
5af9404
Merge branch 'main' into jmacd/histobounds
reyang Jul 30, 2022
b9b24a6
Merge branch 'main' into jmacd/histobounds
jmacd Aug 5, 2022
cc95f75
Merge branch 'main' of github.com:open-telemetry/opentelemetry-specif…
Aug 9, 2022
bc00417
Merge branch 'jmacd/histobounds' of github.com:jmacd/opentelemetry-sp…
Aug 9, 2022
9e47e1f
Merge branch 'main' into jmacd/histobounds
reyang Aug 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 80 additions & 23 deletions specification/metrics/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,14 +599,14 @@ downscale) without introducing error.
#### Exponential Buckets

The ExponentialHistogram bucket identified by `index`, a signed
integer, represents values in the population that are greater than or
equal to `base**index` and less than `base**(index+1)`. Note that the
ExponentialHistogram specifies a lower-inclusive bound while the
explicit-boundary Histogram specifies an upper-inclusive bound.
integer, represents values in the population that are greater than
`base**index` and less than or equal to `base**(index+1)`.
jmacd marked this conversation as resolved.
Show resolved Hide resolved

The positive and negative ranges of the histogram are expressed
separately. Negative values are mapped by their absolute value
into the negative range using the same scale as the positive range.
separately. Negative values are mapped by their absolute value into
the negative range using the same scale as the positive range. Note
that in the negative range, therefore, histogram buckets use
lower-inclusive boundaries.

Each range of the ExponentialHistogram data point uses a dense
representation of the buckets, where a range of buckets is expressed
Expand All @@ -616,9 +616,9 @@ index `offset+i`.

For a given range, positive or negative:

- Bucket index `0` counts measurements in the range `[1, base)`
- Positive indexes correspond with absolute values greater or equal to `base`
- Negative indexes correspond with absolute values less than 1
- Bucket index `0` counts measurements in the range `(1, base]`
- Positive indexes correspond with absolute values greater than `base`
- Negative indexes correspond with absolute values less than or equal to 1
- There are `2**scale` buckets between successive powers of 2.

For example, with `scale=3` there are `2**3` buckets between 1 and 2.
Expand Down Expand Up @@ -650,6 +650,18 @@ that have been rounded to zero.

#### Producer Expectations

Producers MAY use an inexect mapping function because in the general
jmacd marked this conversation as resolved.
Show resolved Hide resolved
case:

1. Exact mapping functions are substantially more complex to implement.
2. Boundaries cannot be exactly represented as floating point numbers for all scales.

The statement that a histogram bucket includes values exactly equal to
its upper boundary is for the benefit of table-lookup based
implementations, where exactness is a desired outcome. Generally,
producers SHOULD use a mapping function with a difference of at most 1
from the correct result for all inputs.

The ExponentialHistogram design makes it possible to express values
that are too large or small to be represented in the 64 bit "double"
floating point format. Certain values for `scale`, while meaningful,
Expand Down Expand Up @@ -704,10 +716,8 @@ double-width floating point values have indices in the range
`[-1074, -1023]`. This may be written as:
jmacd marked this conversation as resolved.
Show resolved Hide resolved

```golang
// GetExponent extracts the normalized base-2 fractional exponent.
// Let the value be represented as `1.significand x 2**exponent`,
// this returns `exponent`. Not defined for 0, Inf, or NaN values.
func GetExponent(value float64) int32 {
// MapToIndexScale0 computes a bucket index at scale 0.
func MapToIndexScale0(value float64) int32 {
rawBits := math.Float64bits(value)
rawExponent := (int64(rawBits) & ExponentMask) >> SignificandWidth
rawSignificand := rawBits & SignificandMask
jmacd marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -716,21 +726,30 @@ func GetExponent(value float64) int32 {
// unless value is zero.
rawExponent -= int64(bits.LeadingZeros64(rawSignificand) - 12)
jmacd marked this conversation as resolved.
Show resolved Hide resolved
}
return int32(rawExponent - ExponentBias)
ieeeExponent := int32(rawExponent - ExponentBias)
if rawSignificand == 0 {
// Special case for power-of-two boundary: subtract one.
return ieeeExponent - 1
}
return ieeeExponent
}
```

Implementations are permitted to round subnormal values up to the
smallest normal value, which may permit the use of a built-in function:

```golang

func GetExponent(value float64) int {
// MapToIndexScale0 computes a bucket index at scale 0.
func MapToIndexScale0(value float64) int {
// Note: Frexp() rounds submnormal values to the smallest normal
// value and returns an exponent corresponding to fractions in the
// range [0.5, 1), whereas we want [1, 2), so subtract 1 from the
// range [0.5, 1), whereas we want (1, 2], so subtract 1 from the
// exponent.
_, exp := math.Frexp(value)
frac, exp := math.Frexp(value)
if frac == 0.5 {
// Special case for power-of-two boundary: subtract one.
exp--
jmacd marked this conversation as resolved.
Show resolved Hide resolved
jmacd marked this conversation as resolved.
Show resolved Hide resolved
}
return exp - 1
}
```
Expand All @@ -743,13 +762,20 @@ by `-scale`. Note that because of sign extension, this shift performs
correct rounding for the negative indices. This may be written as:
jmacd marked this conversation as resolved.
Show resolved Hide resolved

```golang
return GetExponent(value) >> -scale
// MapToIndexNegativeScale computes a bucket index for scale 0.
func MapToIndexNegativeScale(value float64) int {
return MapToIndexScale0(value) >> -scale
}
```

The reverse mapping function is:

```golang
// LowerBoundaryNegativeScale computes the lower boundary for index
// with scale < 0.
func LowerBoundaryNegativeScale(index int) {
return math.Ldexp(1, index << -scale)
}
```

Note that the reverse mapping function is expected to produce
Expand All @@ -767,18 +793,42 @@ compute the bucket index. A multiplicative factor equal to `2**scale
example:

```golang
// MapToIndex for any scale.
func MapToIndex(value float64) int {
scaleFactor := math.Ldexp(math.Log2E, scale)
return math.Floor(math.Log(value) * scaleFactor)
return math.Ceil(math.Log(value) * scaleFactor) - 1
jmacd marked this conversation as resolved.
Show resolved Hide resolved
}
```

Note that in the example Golang code above, the built-in `math.Log2E`
is defined as the inverse of the natural logarithm of 2, i.e., `1 / ln(2)`.
The expression `math.Ceil(expr) - 1` rounds the calculated index up
and subtracts 1 to ensure the correct boundary inclusivity. Note thatl
jmacd marked this conversation as resolved.
Show resolved Hide resolved
in the example Golang code above, the built-in `math.Log2E` is defined
as the inverse of the natural logarithm of 2, i.e., `1 / ln(2)`.

The reverse mapping function is:
The use of `math.Ceil(expr) - 1` is not guaranteed to be exact, even
for power-of-two inputs. Since it is relatively simple to check for
exact powers of two, implementations SHOULD consider such a special
case:

```
jmacd marked this conversation as resolved.
Show resolved Hide resolved
// MapToIndex for any scale, exact for powers of two.
func MapToIndex(value float64) int {
if getSignificand(value) == 0 {
return (MapToIndexScale0(value) << scale) - 1
}
scaleFactor := math.Ldexp(math.Log2E, scale)
return math.Ceil(math.Log(value) * scaleFactor) - 1
}
```

The reverse mapping function for positive scales is:

```golang
// LowerBoundary computes the bucket boundary for positive scales.
func LowerBoundary(index int) float64 {
inverseFactor := math.Ldexp(math.Ln2, -scale)
return math.Exp(index * inverseFactor), nil
}
```

Implementations are expected to verify that their mapping function and
Expand All @@ -791,12 +841,19 @@ reference implementation, for example, the above formula computes
to subtract `1<<scale` from the index and multiply the result by `2`.

```golang
func LowerBoundaryNegativeScale(index int) float64 {
// Use this form in case the equation above computes +Inf
// as the lower boundary of a valid bucket.
inverseFactor := math.Ldexp(math.Ln2, -scale)
return 2.0 * math.Exp((index - (1 << scale)) * inverseFactor), nil
}
```

In the Golang reference implementation, for example, the above formula
does not accurately compute the lower boundary of the minimum-index
bucket (it is a subnormal value). In this case, it is appropriate to
add `1<<scale` to the index and divide the result by `2`.

*Note that floating-point to integer type conversions have been
omitted from the code fragments above, to improve readability.*
jmacd marked this conversation as resolved.
Show resolved Hide resolved

Expand Down