-
Notifications
You must be signed in to change notification settings - Fork 804
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
3670071
commit 3bc93a9
Showing
12 changed files
with
1,032 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* 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 | ||
* | ||
* https://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. | ||
*/ | ||
import * as ieee754 from './ieee754'; | ||
import * as util from '../util'; | ||
import { Mapping, MappingError } from './types'; | ||
|
||
/** | ||
* ExponentMapping implements exponential mapping functions for | ||
* scales <=0. For scales > 0 LogarithmMapping should be used. | ||
*/ | ||
export class ExponentMapping implements Mapping { | ||
private readonly _shift: number; | ||
|
||
constructor(scale: number) { | ||
this._shift = -scale; | ||
} | ||
|
||
/** | ||
* Maps positive floating point values to indexes corresponding to scale | ||
* @param value | ||
* @returns {number} index for provided value at the current scale | ||
*/ | ||
mapToIndex(value: number): number { | ||
if (value < ieee754.MIN_VALUE) { | ||
return this._minNormalLowerBoundaryIndex(); | ||
} | ||
|
||
const exp = ieee754.getNormalBase2(value); | ||
|
||
// In case the value is an exact power of two, compute a | ||
// correction of -1. Note, we are using a custom _rightShift | ||
// to accommodate a 52-bit argument, which the native bitwise | ||
// operators do not support | ||
const correction = this._rightShift( | ||
ieee754.getSignificand(value) - 1, | ||
ieee754.SIGNIFICAND_WIDTH | ||
); | ||
|
||
return (exp + correction) >> this._shift; | ||
} | ||
|
||
/** | ||
* Returns the lower bucket boundary for the given index for scale | ||
* | ||
* @param index | ||
* @returns {number} | ||
*/ | ||
lowerBoundary(index: number): number { | ||
const minIndex = this._minNormalLowerBoundaryIndex(); | ||
if (index < minIndex) { | ||
throw new MappingError( | ||
`underflow: ${index} is < minimum lower boundary: ${minIndex}` | ||
); | ||
} | ||
const maxIndex = this._maxNormalLowerBoundaryIndex(); | ||
if (index > maxIndex) { | ||
throw new MappingError( | ||
`overflow: ${index} is > maximum lower boundary: ${maxIndex}` | ||
); | ||
} | ||
|
||
return util.ldexp(1, index << this._shift); | ||
} | ||
|
||
/** | ||
* The scale used by this mapping | ||
* @returns {number} | ||
*/ | ||
scale(): number { | ||
if (this._shift === 0) { | ||
return 0; | ||
} | ||
return -this._shift; | ||
} | ||
|
||
private _minNormalLowerBoundaryIndex(): number { | ||
let index = ieee754.MIN_NORMAL_EXPONENT >> this._shift; | ||
if (this._shift < 2) { | ||
index--; | ||
} | ||
|
||
return index; | ||
} | ||
|
||
private _maxNormalLowerBoundaryIndex(): number { | ||
return ieee754.MAX_NORMAL_EXPONENT >> this._shift; | ||
} | ||
|
||
private _rightShift(value: number, shift: number): number { | ||
return Math.floor(value * Math.pow(2, -shift)); | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
/* | ||
* 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 | ||
* | ||
* https://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. | ||
*/ | ||
import * as ieee754 from './ieee754'; | ||
import * as util from '../util'; | ||
import { Mapping, MappingError } from './types'; | ||
|
||
/** | ||
* LogarithmMapping implements exponential mapping functions for scale > 0. | ||
* For scales <= 0 the exponent mapping should be used. | ||
*/ | ||
export class LogarithmMapping implements Mapping { | ||
private readonly _scale: number; | ||
private readonly _scaleFactor: number; | ||
private readonly _inverseFactor: number; | ||
|
||
constructor(scale: number) { | ||
this._scale = scale; | ||
this._scaleFactor = util.ldexp(Math.LOG2E, scale); | ||
this._inverseFactor = util.ldexp(Math.LN2, -scale); | ||
} | ||
|
||
/** | ||
* Maps positive floating point values to indexes corresponding to scale | ||
* @param value | ||
* @returns {number} index for provided value at the current scale | ||
*/ | ||
mapToIndex(value: number): number { | ||
if (value <= ieee754.MIN_VALUE) { | ||
return this._minNormalLowerBoundaryIndex() - 1; | ||
} | ||
|
||
// exact power of two special case | ||
if (ieee754.getSignificand(value) === 0) { | ||
const exp = ieee754.getNormalBase2(value); | ||
return (exp << this._scale) - 1; | ||
} | ||
|
||
// non-power of two cases. use Math.floor to round the scaled logarithm | ||
const index = Math.floor(Math.log(value) * this._scaleFactor); | ||
const maxIndex = this._maxNormalLowerBoundaryIndex(); | ||
if (index >= maxIndex) { | ||
return maxIndex; | ||
} | ||
|
||
return index; | ||
} | ||
|
||
/** | ||
* Returns the lower bucket boundary for the given index for scale | ||
* | ||
* @param index | ||
* @returns {number} | ||
*/ | ||
lowerBoundary(index: number): number { | ||
const maxIndex = this._maxNormalLowerBoundaryIndex(); | ||
if (index >= maxIndex) { | ||
if (index === maxIndex) { | ||
return 2 * Math.exp((index - (1 << this._scale)) / this._scaleFactor); | ||
} | ||
throw new MappingError( | ||
`overflow: ${index} is > maximum lower boundary: ${maxIndex}` | ||
); | ||
} | ||
|
||
const minIndex = this._minNormalLowerBoundaryIndex(); | ||
if (index <= minIndex) { | ||
if (index === minIndex) { | ||
return ieee754.MIN_VALUE; | ||
} else if (index === minIndex - 1) { | ||
return Math.exp((index + (1 << this._scale)) / this._scaleFactor) / 2; | ||
} | ||
throw new MappingError( | ||
`overflow: ${index} is < minimum lower boundary: ${minIndex}` | ||
); | ||
} | ||
|
||
return Math.exp(index * this._inverseFactor); | ||
} | ||
|
||
/** | ||
* The scale used by this mapping | ||
* @returns {number} | ||
*/ | ||
scale(): number { | ||
return this._scale; | ||
} | ||
|
||
private _minNormalLowerBoundaryIndex(): number { | ||
return ieee754.MIN_NORMAL_EXPONENT << this._scale; | ||
} | ||
|
||
private _maxNormalLowerBoundaryIndex(): number { | ||
return ((ieee754.MAX_NORMAL_EXPONENT + 1) << this._scale) - 1; | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* 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 | ||
* | ||
* https://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. | ||
*/ | ||
import { ExponentMapping } from './ExponentMapping'; | ||
import { LogarithmMapping } from './LogarithmMapping'; | ||
import { MappingError, Mapping } from './types'; | ||
|
||
const MIN_SCALE = -10; | ||
const MAX_SCALE = 20; | ||
const PREBUILT_MAPPINGS = Array.from({ length: 31 }, (_, i) => { | ||
if (i > 10) { | ||
return new LogarithmMapping(i - 10); | ||
} | ||
return new ExponentMapping(i - 10); | ||
}); | ||
|
||
/** | ||
* getMapping returns an appropriate mapping for the given scale. For scales -10 | ||
* to 0 the underlying type will be ExponentMapping. For scales 1 to 20 the | ||
* underlying type will be LogarithmMapping. | ||
* @param scale a number in the range [-10, 20] | ||
* @returns {Mapping} | ||
*/ | ||
export function getMapping(scale: number): Mapping { | ||
if (scale > MAX_SCALE || scale < MIN_SCALE) { | ||
throw new MappingError( | ||
`expected scale >= ${MIN_SCALE} && <= ${MAX_SCALE}, got: ${scale}` | ||
); | ||
} | ||
// mappings are offset by 10. scale -10 is at position 0 and scale 20 is at 30 | ||
return PREBUILT_MAPPINGS[scale + 10]; | ||
} |
98 changes: 98 additions & 0 deletions
98
packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* 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 | ||
* | ||
* https://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. | ||
*/ | ||
|
||
/** | ||
* The functions and constants in this file allow us to interact | ||
* with the internal representation of an IEEE 64-bit floating point | ||
* number. We need to work with all 64-bits, thus, care needs to be | ||
* taken when working with Javascript's bitwise operators (<<, >>, &, | ||
* |, etc) as they truncate operands to 32-bits. In order to work around | ||
* this we work with the 64-bits as two 32-bit halves, perform bitwise | ||
* operations on them independently, and combine the results (if needed). | ||
*/ | ||
|
||
export const SIGNIFICAND_WIDTH = 52; | ||
|
||
/** | ||
* EXPONENT_MASK is set to 1 for the hi 32-bits of an IEEE 754 | ||
* floating point exponent: 0x7ff00000. | ||
*/ | ||
const EXPONENT_MASK = 0x7ff00000; | ||
|
||
/** | ||
* SIGNIFICAND_MASK is the mask for the significand portion of the hi 32-bits | ||
* of an IEEE 754 double-precision floating-point value: 0xfffff | ||
*/ | ||
const SIGNIFICAND_MASK = 0xfffff; | ||
|
||
/** | ||
* EXPONENT_BIAS is the exponent bias specified for encoding | ||
* the IEEE 754 double-precision floating point exponent: 1023 | ||
*/ | ||
const EXPONENT_BIAS = 1023; | ||
|
||
/** | ||
* MIN_NORMAL_EXPONENT is the minimum exponent of a normalized | ||
* floating point: -1022. | ||
*/ | ||
export const MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1; | ||
|
||
/** | ||
* MAX_NORMAL_EXPONENT is the maximum exponent of a normalized | ||
* floating point: 1023. | ||
*/ | ||
export const MAX_NORMAL_EXPONENT = EXPONENT_BIAS; | ||
|
||
/** | ||
* MIN_VALUE is the smallest normal number | ||
*/ | ||
export const MIN_VALUE = Math.pow(2, -1022); | ||
|
||
/** | ||
* getNormalBase2 extracts the normalized base-2 fractional exponent. | ||
* This returns k for the equation f x 2**k where f is | ||
* in the range [1, 2). Note that this function is not called for | ||
* subnormal numbers. | ||
* @param {number} value - the value to determine normalized base-2 fractional | ||
* exponent for | ||
* @returns {number} the normalized base-2 exponent | ||
*/ | ||
export function getNormalBase2(value: number): number { | ||
const dv = new DataView(new ArrayBuffer(8)); | ||
dv.setFloat64(0, value); | ||
// access the raw 64-bit float as 32-bit uints | ||
const hiBits = dv.getUint32(0); | ||
const expBits = (hiBits & EXPONENT_MASK) >> 20; | ||
return expBits - EXPONENT_BIAS; | ||
} | ||
|
||
/** | ||
* GetSignificand returns the 52 bit (unsigned) significand as a signed value. | ||
* @param {number} value - the floating point number to extract the significand from | ||
* @returns {number} The 52-bit significand | ||
*/ | ||
export function getSignificand(value: number): number { | ||
const dv = new DataView(new ArrayBuffer(8)); | ||
dv.setFloat64(0, value); | ||
// access the raw 64-bit float as two 32-bit uints | ||
const hiBits = dv.getUint32(0); | ||
const loBits = dv.getUint32(4); | ||
// extract the significand bits from the hi bits and left shift 32 places note: | ||
// we can't use the native << operator as it will truncate the result to 32-bits | ||
const significandHiBits = (hiBits & SIGNIFICAND_MASK) * Math.pow(2, 32); | ||
// combine the hi and lo bits and return | ||
return significandHiBits + loBits; | ||
} |
27 changes: 27 additions & 0 deletions
27
packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* 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 | ||
* | ||
* https://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. | ||
*/ | ||
export class MappingError extends Error {} | ||
|
||
/** | ||
* The mapping interface is used by the exponential histogram to determine | ||
* where to bucket values. The interface is implemented by ExponentMapping, | ||
* used for scales [-10, 0] and LogarithmMapping, used for scales [1, 20]. | ||
*/ | ||
export interface Mapping { | ||
mapToIndex(value: number): number; | ||
lowerBoundary(index: number): number; | ||
scale(): number; | ||
} |
Oops, something went wrong.