Skip to content

Commit 3bc93a9

Browse files
mwearpichlermarcdyladan
authored
feat: exponential histogram - part 1 - mapping functions (#3504)
* feat: add exponential histogram mapping functions * Apply suggestions from code review Co-authored-by: Marc Pichler <marcpi@edu.aau.at> Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com> * chore: fix compile * refactor: use Number.MAX_VALUE directly * chore: add docs to mapping and ieee754 * chore: move MIN_SCALE and MAX_SCALE to unexported constants * chore: remove currently unused test helper * chore: lint * refactor: build all scales, extract single getMapping function * fix: off by one error when pre-building mappings Co-authored-by: Marc Pichler <marc.pichler@dynatrace.com> Co-authored-by: Marc Pichler <marcpi@edu.aau.at> Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
1 parent 3670071 commit 3bc93a9

File tree

12 files changed

+1032
-0
lines changed

12 files changed

+1032
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/
1111

1212
### :rocket: (Enhancement)
1313

14+
* feat(sdk-metrics): add exponential histogram mapping functions [#3504](https://github.com/open-telemetry/opentelemetry-js/pull/3504) @mwear
15+
1416
### :bug: (Bug Fix)
1517

1618
* fix: avoid grpc types dependency [#3551](https://github.com/open-telemetry/opentelemetry-js/pull/3551) @flarna
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import * as ieee754 from './ieee754';
17+
import * as util from '../util';
18+
import { Mapping, MappingError } from './types';
19+
20+
/**
21+
* ExponentMapping implements exponential mapping functions for
22+
* scales <=0. For scales > 0 LogarithmMapping should be used.
23+
*/
24+
export class ExponentMapping implements Mapping {
25+
private readonly _shift: number;
26+
27+
constructor(scale: number) {
28+
this._shift = -scale;
29+
}
30+
31+
/**
32+
* Maps positive floating point values to indexes corresponding to scale
33+
* @param value
34+
* @returns {number} index for provided value at the current scale
35+
*/
36+
mapToIndex(value: number): number {
37+
if (value < ieee754.MIN_VALUE) {
38+
return this._minNormalLowerBoundaryIndex();
39+
}
40+
41+
const exp = ieee754.getNormalBase2(value);
42+
43+
// In case the value is an exact power of two, compute a
44+
// correction of -1. Note, we are using a custom _rightShift
45+
// to accommodate a 52-bit argument, which the native bitwise
46+
// operators do not support
47+
const correction = this._rightShift(
48+
ieee754.getSignificand(value) - 1,
49+
ieee754.SIGNIFICAND_WIDTH
50+
);
51+
52+
return (exp + correction) >> this._shift;
53+
}
54+
55+
/**
56+
* Returns the lower bucket boundary for the given index for scale
57+
*
58+
* @param index
59+
* @returns {number}
60+
*/
61+
lowerBoundary(index: number): number {
62+
const minIndex = this._minNormalLowerBoundaryIndex();
63+
if (index < minIndex) {
64+
throw new MappingError(
65+
`underflow: ${index} is < minimum lower boundary: ${minIndex}`
66+
);
67+
}
68+
const maxIndex = this._maxNormalLowerBoundaryIndex();
69+
if (index > maxIndex) {
70+
throw new MappingError(
71+
`overflow: ${index} is > maximum lower boundary: ${maxIndex}`
72+
);
73+
}
74+
75+
return util.ldexp(1, index << this._shift);
76+
}
77+
78+
/**
79+
* The scale used by this mapping
80+
* @returns {number}
81+
*/
82+
scale(): number {
83+
if (this._shift === 0) {
84+
return 0;
85+
}
86+
return -this._shift;
87+
}
88+
89+
private _minNormalLowerBoundaryIndex(): number {
90+
let index = ieee754.MIN_NORMAL_EXPONENT >> this._shift;
91+
if (this._shift < 2) {
92+
index--;
93+
}
94+
95+
return index;
96+
}
97+
98+
private _maxNormalLowerBoundaryIndex(): number {
99+
return ieee754.MAX_NORMAL_EXPONENT >> this._shift;
100+
}
101+
102+
private _rightShift(value: number, shift: number): number {
103+
return Math.floor(value * Math.pow(2, -shift));
104+
}
105+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import * as ieee754 from './ieee754';
17+
import * as util from '../util';
18+
import { Mapping, MappingError } from './types';
19+
20+
/**
21+
* LogarithmMapping implements exponential mapping functions for scale > 0.
22+
* For scales <= 0 the exponent mapping should be used.
23+
*/
24+
export class LogarithmMapping implements Mapping {
25+
private readonly _scale: number;
26+
private readonly _scaleFactor: number;
27+
private readonly _inverseFactor: number;
28+
29+
constructor(scale: number) {
30+
this._scale = scale;
31+
this._scaleFactor = util.ldexp(Math.LOG2E, scale);
32+
this._inverseFactor = util.ldexp(Math.LN2, -scale);
33+
}
34+
35+
/**
36+
* Maps positive floating point values to indexes corresponding to scale
37+
* @param value
38+
* @returns {number} index for provided value at the current scale
39+
*/
40+
mapToIndex(value: number): number {
41+
if (value <= ieee754.MIN_VALUE) {
42+
return this._minNormalLowerBoundaryIndex() - 1;
43+
}
44+
45+
// exact power of two special case
46+
if (ieee754.getSignificand(value) === 0) {
47+
const exp = ieee754.getNormalBase2(value);
48+
return (exp << this._scale) - 1;
49+
}
50+
51+
// non-power of two cases. use Math.floor to round the scaled logarithm
52+
const index = Math.floor(Math.log(value) * this._scaleFactor);
53+
const maxIndex = this._maxNormalLowerBoundaryIndex();
54+
if (index >= maxIndex) {
55+
return maxIndex;
56+
}
57+
58+
return index;
59+
}
60+
61+
/**
62+
* Returns the lower bucket boundary for the given index for scale
63+
*
64+
* @param index
65+
* @returns {number}
66+
*/
67+
lowerBoundary(index: number): number {
68+
const maxIndex = this._maxNormalLowerBoundaryIndex();
69+
if (index >= maxIndex) {
70+
if (index === maxIndex) {
71+
return 2 * Math.exp((index - (1 << this._scale)) / this._scaleFactor);
72+
}
73+
throw new MappingError(
74+
`overflow: ${index} is > maximum lower boundary: ${maxIndex}`
75+
);
76+
}
77+
78+
const minIndex = this._minNormalLowerBoundaryIndex();
79+
if (index <= minIndex) {
80+
if (index === minIndex) {
81+
return ieee754.MIN_VALUE;
82+
} else if (index === minIndex - 1) {
83+
return Math.exp((index + (1 << this._scale)) / this._scaleFactor) / 2;
84+
}
85+
throw new MappingError(
86+
`overflow: ${index} is < minimum lower boundary: ${minIndex}`
87+
);
88+
}
89+
90+
return Math.exp(index * this._inverseFactor);
91+
}
92+
93+
/**
94+
* The scale used by this mapping
95+
* @returns {number}
96+
*/
97+
scale(): number {
98+
return this._scale;
99+
}
100+
101+
private _minNormalLowerBoundaryIndex(): number {
102+
return ieee754.MIN_NORMAL_EXPONENT << this._scale;
103+
}
104+
105+
private _maxNormalLowerBoundaryIndex(): number {
106+
return ((ieee754.MAX_NORMAL_EXPONENT + 1) << this._scale) - 1;
107+
}
108+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { ExponentMapping } from './ExponentMapping';
17+
import { LogarithmMapping } from './LogarithmMapping';
18+
import { MappingError, Mapping } from './types';
19+
20+
const MIN_SCALE = -10;
21+
const MAX_SCALE = 20;
22+
const PREBUILT_MAPPINGS = Array.from({ length: 31 }, (_, i) => {
23+
if (i > 10) {
24+
return new LogarithmMapping(i - 10);
25+
}
26+
return new ExponentMapping(i - 10);
27+
});
28+
29+
/**
30+
* getMapping returns an appropriate mapping for the given scale. For scales -10
31+
* to 0 the underlying type will be ExponentMapping. For scales 1 to 20 the
32+
* underlying type will be LogarithmMapping.
33+
* @param scale a number in the range [-10, 20]
34+
* @returns {Mapping}
35+
*/
36+
export function getMapping(scale: number): Mapping {
37+
if (scale > MAX_SCALE || scale < MIN_SCALE) {
38+
throw new MappingError(
39+
`expected scale >= ${MIN_SCALE} && <= ${MAX_SCALE}, got: ${scale}`
40+
);
41+
}
42+
// mappings are offset by 10. scale -10 is at position 0 and scale 20 is at 30
43+
return PREBUILT_MAPPINGS[scale + 10];
44+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* The functions and constants in this file allow us to interact
19+
* with the internal representation of an IEEE 64-bit floating point
20+
* number. We need to work with all 64-bits, thus, care needs to be
21+
* taken when working with Javascript's bitwise operators (<<, >>, &,
22+
* |, etc) as they truncate operands to 32-bits. In order to work around
23+
* this we work with the 64-bits as two 32-bit halves, perform bitwise
24+
* operations on them independently, and combine the results (if needed).
25+
*/
26+
27+
export const SIGNIFICAND_WIDTH = 52;
28+
29+
/**
30+
* EXPONENT_MASK is set to 1 for the hi 32-bits of an IEEE 754
31+
* floating point exponent: 0x7ff00000.
32+
*/
33+
const EXPONENT_MASK = 0x7ff00000;
34+
35+
/**
36+
* SIGNIFICAND_MASK is the mask for the significand portion of the hi 32-bits
37+
* of an IEEE 754 double-precision floating-point value: 0xfffff
38+
*/
39+
const SIGNIFICAND_MASK = 0xfffff;
40+
41+
/**
42+
* EXPONENT_BIAS is the exponent bias specified for encoding
43+
* the IEEE 754 double-precision floating point exponent: 1023
44+
*/
45+
const EXPONENT_BIAS = 1023;
46+
47+
/**
48+
* MIN_NORMAL_EXPONENT is the minimum exponent of a normalized
49+
* floating point: -1022.
50+
*/
51+
export const MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1;
52+
53+
/**
54+
* MAX_NORMAL_EXPONENT is the maximum exponent of a normalized
55+
* floating point: 1023.
56+
*/
57+
export const MAX_NORMAL_EXPONENT = EXPONENT_BIAS;
58+
59+
/**
60+
* MIN_VALUE is the smallest normal number
61+
*/
62+
export const MIN_VALUE = Math.pow(2, -1022);
63+
64+
/**
65+
* getNormalBase2 extracts the normalized base-2 fractional exponent.
66+
* This returns k for the equation f x 2**k where f is
67+
* in the range [1, 2). Note that this function is not called for
68+
* subnormal numbers.
69+
* @param {number} value - the value to determine normalized base-2 fractional
70+
* exponent for
71+
* @returns {number} the normalized base-2 exponent
72+
*/
73+
export function getNormalBase2(value: number): number {
74+
const dv = new DataView(new ArrayBuffer(8));
75+
dv.setFloat64(0, value);
76+
// access the raw 64-bit float as 32-bit uints
77+
const hiBits = dv.getUint32(0);
78+
const expBits = (hiBits & EXPONENT_MASK) >> 20;
79+
return expBits - EXPONENT_BIAS;
80+
}
81+
82+
/**
83+
* GetSignificand returns the 52 bit (unsigned) significand as a signed value.
84+
* @param {number} value - the floating point number to extract the significand from
85+
* @returns {number} The 52-bit significand
86+
*/
87+
export function getSignificand(value: number): number {
88+
const dv = new DataView(new ArrayBuffer(8));
89+
dv.setFloat64(0, value);
90+
// access the raw 64-bit float as two 32-bit uints
91+
const hiBits = dv.getUint32(0);
92+
const loBits = dv.getUint32(4);
93+
// extract the significand bits from the hi bits and left shift 32 places note:
94+
// we can't use the native << operator as it will truncate the result to 32-bits
95+
const significandHiBits = (hiBits & SIGNIFICAND_MASK) * Math.pow(2, 32);
96+
// combine the hi and lo bits and return
97+
return significandHiBits + loBits;
98+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
export class MappingError extends Error {}
17+
18+
/**
19+
* The mapping interface is used by the exponential histogram to determine
20+
* where to bucket values. The interface is implemented by ExponentMapping,
21+
* used for scales [-10, 0] and LogarithmMapping, used for scales [1, 20].
22+
*/
23+
export interface Mapping {
24+
mapToIndex(value: number): number;
25+
lowerBoundary(index: number): number;
26+
scale(): number;
27+
}

0 commit comments

Comments
 (0)