From 8ec90ffd36b44acef053b2c93503b7a20c32c6f3 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Wed, 21 Dec 2022 15:09:53 -0800 Subject: [PATCH 01/10] feat: add exponential histogram mapping functions --- CHANGELOG.md | 1 + .../mapping/ExponentMapping.ts | 139 ++++++++ .../mapping/LogarithmMapping.ts | 138 ++++++++ .../exponential-histogram/mapping/ieee754.ts | 91 ++++++ .../exponential-histogram/mapping/types.ts | 22 ++ .../aggregator/exponential-histogram/util.ts | 40 +++ .../ExponentMapping.test.ts | 309 ++++++++++++++++++ .../LogarithmMapping.test.ts | 196 +++++++++++ .../exponential-histogram/helpers.ts | 41 +++ .../exponential-histogram/ieee754.test.ts | 58 ++++ 10 files changed, 1035 insertions(+) create mode 100644 packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts create mode 100644 packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts create mode 100644 packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts create mode 100644 packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts create mode 100644 packages/sdk-metrics/src/aggregator/exponential-histogram/util.ts create mode 100644 packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts create mode 100644 packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts create mode 100644 packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts create mode 100644 packages/sdk-metrics/test/aggregator/exponential-histogram/ieee754.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c390c68fbb..ad7fcb9639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ ### :rocket: (Enhancement) +* feat(sdk-metrics): add exponential histogram mapping functions [#3504](https://github.com/open-telemetry/opentelemetry-js/pull/3504) @mwear * feat(api): add `getActiveBaggage` API [#3385](https://github.com/open-telemetry/opentelemetry-js/pull/3385) * feat(instrumentation-grpc): set net.peer.name and net.peer.port on client spans [#3430](https://github.com/open-telemetry/opentelemetry-js/pull/3430) diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts new file mode 100644 index 0000000000..953cd81c1f --- /dev/null +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts @@ -0,0 +1,139 @@ +/* + * 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 a exponential mapping functions for + * for scales <=0. For scales > 0 LogarithmMapping should be used. + */ +export class ExponentMapping implements Mapping { + static readonly MIN_SCALE = -10; + static readonly MAX_SCALE = 0; + private static readonly _PREBUILT_MAPPINGS = [ + new ExponentMapping(10), + new ExponentMapping(9), + new ExponentMapping(8), + new ExponentMapping(7), + new ExponentMapping(6), + new ExponentMapping(5), + new ExponentMapping(4), + new ExponentMapping(3), + new ExponentMapping(2), + new ExponentMapping(1), + new ExponentMapping(0), + ]; + + /** + * Returns the pre-built mapping for the given scale + * @param scale An integer >= -10 and <= 0 + * @returns {ExponentMapping} + */ + public static get(scale: number) { + if (scale > ExponentMapping.MAX_SCALE) { + throw new MappingError( + `exponent mapping requires scale <= ${ExponentMapping.MAX_SCALE}` + ); + } + if (scale < ExponentMapping.MIN_SCALE) { + throw new MappingError( + `exponent mapping requires a scale > ${ExponentMapping.MIN_SCALE}` + ); + } + + return ExponentMapping._PREBUILT_MAPPINGS[ + scale - ExponentMapping.MIN_SCALE + ]; + } + + private constructor(private readonly _shift: number) {} + + /** + * 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 accomodate 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)); + } +} diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts new file mode 100644 index 0000000000..cf99f44508 --- /dev/null +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts @@ -0,0 +1,138 @@ +/* + * 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 { + static readonly MIN_SCALE = 1; + static readonly MAX_SCALE = 20; + private static _PREBUILT_MAPPINGS = new Map(); + + /** + * get creates or returns a memoized logarithm mapping function for + * the given scale. used for scales > 0. + * @param scale - a number > 0 and <= 20 + * @returns {LogarithmMapping} + */ + static get(scale: number): LogarithmMapping { + if ( + scale > LogarithmMapping.MAX_SCALE || + scale < LogarithmMapping.MIN_SCALE + ) { + throw new MappingError( + `logarithm mapping requires scale in the range [${LogarithmMapping.MIN_SCALE}, ${LogarithmMapping.MAX_SCALE}]` + ); + } + + let mapping = this._PREBUILT_MAPPINGS.get(scale); + if (mapping) { + return mapping; + } + + mapping = new LogarithmMapping(scale); + this._PREBUILT_MAPPINGS.set(scale, mapping); + return mapping; + } + + private readonly _scale: number; + private readonly _scaleFactor: number; + private readonly _inverseFactor: number; + + private 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; + } +} diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts new file mode 100644 index 0000000000..0dac0070ad --- /dev/null +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts @@ -0,0 +1,91 @@ +/* + * 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 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; + +/** + * MAX_VALUE is the largest normal number + */ +export const MAX_VALUE = Number.MAX_VALUE; + +/** + * 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 + const significandHiBits = (hiBits & SIGNIFICAND_MASK) * Math.pow(2, 32); + // combine the hi and lo bits and return + return significandHiBits + loBits; +} diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts new file mode 100644 index 0000000000..d54d778131 --- /dev/null +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts @@ -0,0 +1,22 @@ +/* + * 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 {} + +export interface Mapping { + mapToIndex(value: number): number; + lowerBoundary(index: number): number; + scale(): number; +} diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/util.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/util.ts new file mode 100644 index 0000000000..356bbab260 --- /dev/null +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/util.ts @@ -0,0 +1,40 @@ +/* + * 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. + */ + +/** + * Note: other languages provide this as a built in function. This is + * a naive, but functionally correct implementation. This is used sparingly, + * when creating a new mapping in a running application. + * + * ldexp returns frac × 2**exp. With the following special cases: + * ldexp(±0, exp) = ±0 + * ldexp(±Inf, exp) = ±Inf + * ldexp(NaN, exp) = NaN + * @param frac + * @param exp + * @returns {number} + */ +export function ldexp(frac: number, exp: number): number { + if ( + frac === 0 || + frac === Number.POSITIVE_INFINITY || + frac === Number.NEGATIVE_INFINITY || + Number.isNaN(frac) + ) { + return frac; + } + return frac * Math.pow(2, exp); +} diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts new file mode 100644 index 0000000000..47b160a28d --- /dev/null +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts @@ -0,0 +1,309 @@ +/* + * 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 '../../../src/aggregator/exponential-histogram/mapping/ExponentMapping'; +import * as ieee754 from '../../../src/aggregator/exponential-histogram/mapping/ieee754'; +import * as assert from 'assert'; + +describe('ExponentMapping', () => { + it('maps expected values for scale 0', () => { + const mapping = ExponentMapping.get(0); + assert.strictEqual(mapping.scale(), 0); + + const expectedMappings = [ + // near +inf + [Number.MAX_VALUE, ieee754.MAX_NORMAL_EXPONENT], + [Number.MAX_VALUE, 1023], + [Math.pow(2, 1023), 1022], + [1.0625 * Math.pow(2, 1023), 1023], + [Math.pow(2, 1022), 1021], + [1.0625 * Math.pow(2, 1023), 1023], + + // near 0 + [Math.pow(2, -1022), -1023], + [1.0625 * Math.pow(2, -1022), -1022], + [Math.pow(2, -1021), -1022], + [1.0625 * Math.pow(2, -1021), -1021], + + [Math.pow(2, -1022), ieee754.MIN_NORMAL_EXPONENT - 1], + [Math.pow(2, -1021), ieee754.MIN_NORMAL_EXPONENT], + [Number.MIN_VALUE, ieee754.MIN_NORMAL_EXPONENT - 1], + + // near 1 + [4, 1], + [3, 1], + [2, 0], + [1.5, 0], + [1, -1], + [0.75, -1], + [0.51, -1], + [0.5, -2], + [0.26, -2], + [0.25, -3], + [0.126, -3], + [0.125, -4], + ]; + + expectedMappings.forEach(([value, expected]) => { + const result = mapping.mapToIndex(value); + assert.strictEqual( + result, + expected, + `expected: ${value} to map to: ${expected}, got: ${result}` + ); + }); + }); + + it('maps expected values for min scale', () => { + const mapping = ExponentMapping.get(ExponentMapping.MIN_SCALE); + assert.strictEqual(mapping.scale(), ExponentMapping.MIN_SCALE); + + const expectedMappings = [ + [1.000001, 0], + [1, -1], + [Number.MAX_VALUE / 2, 0], + [Number.MAX_VALUE, 0], + [Number.MIN_VALUE, -1], + [0.5, -1], + ]; + + expectedMappings.forEach(([value, expected]) => { + const result = mapping.mapToIndex(value); + assert.strictEqual( + result, + expected, + `expected: ${value} to map to: ${expected}, got: ${result}` + ); + }); + }); + + it('maps expected values for scale -1', () => { + const mapping = ExponentMapping.get(-1); + assert.strictEqual(mapping.scale(), -1); + + const expectedMappings = [ + [17, 2], + [16, 1], + [15, 1], + [9, 1], + [8, 1], + [5, 1], + [4, 0], + [3, 0], + [2, 0], + [1.5, 0], + [1, -1], + [0.75, -1], + [0.5, -1], + [0.25, -2], + [0.2, -2], + [0.13, -2], + [0.125, -2], + [0.1, -2], + [0.0625, -3], + [0.06, -3], + ]; + + expectedMappings.forEach(([value, expected]) => { + const result = mapping.mapToIndex(value); + assert.strictEqual( + result, + expected, + `expected: ${value} to map to: ${expected}, got: ${result}` + ); + }); + }); + + it('maps expected values for scale -4', () => { + const mapping = ExponentMapping.get(-4); + assert.strictEqual(mapping.scale(), -4); + + const expectedMappings = [ + [0x1, -1], + [0x10, 0], + [0x100, 0], + [0x1000, 0], + [0x10000, 0], // Base == 2**16 + [0x100000, 1], + [0x1000000, 1], + [0x10000000, 1], + [0x100000000, 1], // == 2**32 + [0x1000000000, 2], + [0x10000000000, 2], + [0x100000000000, 2], + [0x1000000000000, 2], // 2**48 + [0x10000000000000, 3], + [0x1000000000000000, 3], + [0x10000000000000000, 3], // 2**64 + [0x100000000000000000, 4], + [0x1000000000000000000, 4], + [0x10000000000000000000, 4], + [0x100000000000000000000, 4], // 2**80 + [0x1000000000000000000000, 5], + + [1 / 0x1, -1], + [1 / 0x10, -1], + [1 / 0x100, -1], + [1 / 0x1000, -1], + [1 / 0x10000, -2], // 2**-16 + [1 / 0x100000, -2], + [1 / 0x1000000, -2], + [1 / 0x10000000, -2], + [1 / 0x100000000, -3], // 2**-32 + [1 / 0x1000000000, -3], + [1 / 0x10000000000, -3], + [1 / 0x100000000000, -3], + [1 / 0x1000000000000, -4], // 2**-48 + [1 / 0x10000000000000, -4], + [1 / 0x100000000000000, -4], + [1 / 0x1000000000000000, -4], + [1 / 0x10000000000000000, -5], // 2**-64 + [1 / 0x100000000000000000, -5], + + // Max values + // below is equivalent to [0x1.FFFFFFFFFFFFFp1023, 63], + [ + Array.from({ length: 13 }, (_, x) => 0xf * Math.pow(16, -x - 1)).reduce( + (x, y) => x + y, + 1 + ) * Math.pow(2, 1023), + 63, + ], + [Math.pow(2, 1023), 63], + [Math.pow(2, 1019), 63], + [Math.pow(2, 1009), 63], + [Math.pow(2, 1008), 62], + [Math.pow(2, 1007), 62], + [Math.pow(2, 1000), 62], + [Math.pow(2, 993), 62], + [Math.pow(2, 992), 61], + [Math.pow(2, 991), 61], + + // Min and subnormal values + [Math.pow(2, -1074), -64], + [Math.pow(2, -1073), -64], + [Math.pow(2, -1072), -64], + [Math.pow(2, -1057), -64], + [Math.pow(2, -1056), -64], + [Math.pow(2, -1041), -64], + [Math.pow(2, -1040), -64], + [Math.pow(2, -1025), -64], + [Math.pow(2, -1024), -64], + [Math.pow(2, -1023), -64], + [Math.pow(2, -1022), -64], + [Math.pow(2, -1009), -64], + [Math.pow(2, -1008), -64], + [Math.pow(2, -1007), -63], + [Math.pow(2, -993), -63], + [Math.pow(2, -992), -63], + [Math.pow(2, -991), -62], + [Math.pow(2, -977), -62], + [Math.pow(2, -976), -62], + [Math.pow(2, -975), -61], + ]; + + expectedMappings.forEach(([value, expected]) => { + const result = mapping.mapToIndex(value); + assert.strictEqual( + result, + expected, + `expected: ${value} to map to: ${expected}, got: ${result}` + ); + }); + }); + + it('throws errors for invalid scales', () => { + assert.throws(() => { + ExponentMapping.get(1); + }); + assert.throws(() => { + ExponentMapping.get(ExponentMapping.MIN_SCALE - 1); + }); + }); + + it('handles max index for all scales', () => { + for ( + let scale = ExponentMapping.MIN_SCALE; + scale <= ExponentMapping.MAX_SCALE; + scale++ + ) { + const mapping = ExponentMapping.get(scale); + const index = mapping.mapToIndex(ieee754.MAX_VALUE); + const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) >> -scale) - 1; + assert.strictEqual( + index, + maxIndex, + `expected index: ${index} and ${maxIndex} to be equal for scale: ${scale}` + ); + + const boundary = mapping.lowerBoundary(index); + assert.strictEqual(boundary, roundedBoundary(scale, maxIndex)); + + assert.throws(() => { + // one larger will overflow + mapping.lowerBoundary(index + 1); + }); + } + }); + + it('handles min index for all scales', () => { + for ( + let scale = ExponentMapping.MIN_SCALE; + scale <= ExponentMapping.MAX_SCALE; + scale++ + ) { + const mapping = ExponentMapping.get(scale); + const minIndex = mapping.mapToIndex(ieee754.MIN_VALUE); + let expectedMinIndex = ieee754.MIN_NORMAL_EXPONENT >> -scale; + if (ieee754.MIN_NORMAL_EXPONENT % (1 << -scale) === 0) { + expectedMinIndex--; + } + assert.strictEqual( + minIndex, + expectedMinIndex, + `expected expectedMinIndex: ${expectedMinIndex} and ${minIndex} to be equal for scale: ${scale}` + ); + + const boundary = mapping.lowerBoundary(minIndex); + const expectedBoundary = roundedBoundary(scale, expectedMinIndex); + assert.strictEqual(boundary, expectedBoundary); + + //one smaller will underflow + assert.throws(() => { + mapping.lowerBoundary(minIndex - 1); + }); + + // subnormals map to the min index + [ + ieee754.MIN_VALUE / 2, + ieee754.MIN_VALUE / 3, + Math.pow(2, -1050), + Math.pow(2, -1073), + 1.0625 * Math.pow(2, -1073), + Math.pow(2, -1074), + ].forEach(value => { + assert.strictEqual(mapping.mapToIndex(value), expectedMinIndex); + }); + } + }); +}); + +function roundedBoundary(scale: number, index: number): number { + let result = Math.pow(2, index); + for (let i = scale; i < 0; i++) { + result = result * result; + } + return result; +} diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts new file mode 100644 index 0000000000..c5bd1c9f1d --- /dev/null +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts @@ -0,0 +1,196 @@ +/* + * 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 { LogarithmMapping } from '../../../src/aggregator/exponential-histogram/mapping/LogarithmMapping'; +import * as ieee754 from '../../../src/aggregator/exponential-histogram/mapping/ieee754'; +import * as assert from 'assert'; +import { assertInEpsilon } from './helpers'; + +describe('LogarithmMapping', () => { + it('throws for invalid scale', () => { + assert.throws(() => { + LogarithmMapping.get(-1); + }); + }); + + it('maps values for scale 1', () => { + const mapping = LogarithmMapping.get(1); + assert.strictEqual(1, mapping.scale()); + + const expectedMappings = [ + [15, 7], + [9, 6], + [7, 5], + [5, 4], + [3, 3], + [2.5, 2], + [1.5, 1], + [1.2, 0], + [1, -1], + [0.75, -1], + [0.55, -2], + [0.45, -3], + ]; + + expectedMappings.forEach(([value, expected]) => { + const result = mapping.mapToIndex(value); + assert.strictEqual( + result, + expected, + `expected: ${value} to map to: ${expected}, got: ${result}` + ); + }); + }); + + it('computes boundary', () => { + [1, 2, 3, 4, 10, 15].forEach(scale => { + const mapping = LogarithmMapping.get(scale); + [-100, -10, -1, 0, 1, 10, 100].forEach(index => { + const boundary = mapping.lowerBoundary(index); + const mappedIndex = mapping.mapToIndex(boundary); + + assert.ok(index - 1 <= mappedIndex); + assert.ok(index >= mappedIndex); + assertInEpsilon(roundedBoundary(scale, index), boundary, 1e-9); + }); + }); + }); + + it('handles max index for each scale', () => { + for ( + let scale = LogarithmMapping.MIN_SCALE; + scale <= LogarithmMapping.MAX_SCALE; + scale++ + ) { + const mapping = LogarithmMapping.get(scale); + const index = mapping.mapToIndex(ieee754.MAX_VALUE); + + // the max index is one less than the first index that + // overflows Number.MAX_VALUE + const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) << scale) - 1; + + assert.strictEqual(index, maxIndex); + + const boundary = mapping.lowerBoundary(index); + const base = mapping.lowerBoundary(1); + + assert.ok( + boundary < ieee754.MAX_VALUE, + `expected boundary: ${boundary} to be < max value: ${ieee754.MAX_VALUE}` + ); + + assertInEpsilon( + base - 1, + (ieee754.MAX_VALUE - boundary) / boundary, + 10e-6 + ); + } + }); + + it('handles min index for each scale', () => { + for ( + let scale = LogarithmMapping.MIN_SCALE; + scale <= LogarithmMapping.MAX_SCALE; + scale++ + ) { + const mapping = LogarithmMapping.get(scale); + const minIndex = mapping.mapToIndex(ieee754.MIN_VALUE); + + const expectedMinIndex = (ieee754.MIN_NORMAL_EXPONENT << scale) - 1; + assert.strictEqual(minIndex, expectedMinIndex); + + const expectedBoundary = roundedBoundary(scale, expectedMinIndex); + assert.ok(expectedBoundary < ieee754.MIN_VALUE); + + const expectedUpperBoundary = roundedBoundary( + scale, + expectedMinIndex + 1 + ); + assert.strictEqual(ieee754.MIN_VALUE, expectedUpperBoundary); + + const mappedLowerBoundary = mapping.lowerBoundary(minIndex + 1); + assertInEpsilon(ieee754.MIN_VALUE, mappedLowerBoundary, 1e-6); + + // subnormals map to the min index + [ + ieee754.MIN_VALUE / 2, + ieee754.MIN_VALUE / 3, + ieee754.MIN_VALUE / 100, + Math.pow(2, -1050), + Math.pow(2, -1073), + 1.0625 * Math.pow(2, -1073), + Math.pow(2, -1074), + ].forEach(value => { + const result = mapping.mapToIndex(value); + assert.strictEqual(result, expectedMinIndex); + }); + + const mappedMinLower = mapping.lowerBoundary(minIndex); + + assertInEpsilon(expectedBoundary, mappedMinLower, 1e-6); + + // one smaller will underflow + assert.throws(() => { + mapping.lowerBoundary(minIndex - 1); + }); + } + }); + + it('maps max float to max index for each scale', () => { + for ( + let scale = LogarithmMapping.MIN_SCALE; + scale <= LogarithmMapping.MAX_SCALE; + scale++ + ) { + const mapping = LogarithmMapping.get(scale); + const index = mapping.mapToIndex(ieee754.MAX_VALUE); + const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) << scale) - 1; + assert.strictEqual(maxIndex, index); + + const boundary = mapping.lowerBoundary(index); + const base = mapping.lowerBoundary(1); + + assert.ok(boundary < ieee754.MAX_VALUE); + assertInEpsilon( + base - 1, + (ieee754.MAX_VALUE - boundary) / boundary, + 1e-6 + ); + + //one larger will overflow + assert.throws(() => { + mapping.lowerBoundary(index + 1); + }); + } + }); +}); + +function roundedBoundary(scale: number, index: number): number { + while (scale > 0) { + if (index < -1022) { + index /= 2; + scale--; + } else { + break; + } + } + + let result = Math.pow(2, index); + for (let i = scale; i > 0; i--) { + result = Math.sqrt(result); + } + + return result; +} diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts new file mode 100644 index 0000000000..5f89b34771 --- /dev/null +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts @@ -0,0 +1,41 @@ +/* + * 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 assert from 'assert'; + +export function assertInEpsilon( + actual: number, + expected: number, + epsilon: number +) { + assert.ok(!Number.isNaN(actual), 'unexpected NaN for actual argument'); + assert.ok(!Number.isNaN(expected), 'unexpected NaN for exepected argument'); + assert.ok(actual !== 0, 'unexpected 0 for actual argument'); + + const relErr = Math.abs(actual - expected) / Math.abs(actual); + + assert.ok( + relErr < epsilon, + `expected relative error: ${relErr} to be < ${epsilon}` + ); +} + +export function assertInDelta(actual: number, expected: number, delta: number) { + const actualDelta = Math.abs(expected - actual); + assert.ok( + actualDelta < delta, + `expected delta: ${delta} to be < ${actualDelta}` + ); +} diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/ieee754.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/ieee754.test.ts new file mode 100644 index 0000000000..3db0da3268 --- /dev/null +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/ieee754.test.ts @@ -0,0 +1,58 @@ +/* + * 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 '../../../src/aggregator/exponential-histogram/mapping/ieee754'; +import * as assert from 'assert'; + +describe('ieee754 helpers', () => { + describe('MIN_NORMAL_EXPONENT', () => { + it('has expected value', () => { + assert.strictEqual(ieee754.MIN_NORMAL_EXPONENT, -1022); + }); + }); + + describe('MAX_NORMAL_EXPONENT', () => { + it('has expected value', () => { + assert.strictEqual(ieee754.MAX_NORMAL_EXPONENT, 1023); + }); + }); + + describe('getNormalBase2', () => { + it('extracts exponent', () => { + assert.strictEqual( + ieee754.getNormalBase2(Math.pow(2, 1023)), + ieee754.MAX_NORMAL_EXPONENT + ); + assert.strictEqual(ieee754.getNormalBase2(Math.pow(2, 1022)), 1022); + assert.strictEqual(ieee754.getNormalBase2(18.9), 4); + assert.strictEqual(ieee754.getNormalBase2(1), 0); + assert.strictEqual(ieee754.getNormalBase2(Math.pow(2, -1021)), -1021); + assert.strictEqual(ieee754.getNormalBase2(Math.pow(2, -1022)), -1022); + + // Subnormals below + assert.strictEqual(ieee754.getNormalBase2(Math.pow(2, -1023)), -1023); + assert.strictEqual(ieee754.getNormalBase2(Math.pow(2, -1024)), -1023); + assert.strictEqual(ieee754.getNormalBase2(Math.pow(2, -1025)), -1023); + assert.strictEqual(ieee754.getNormalBase2(Math.pow(2, -1074)), -1023); + }); + }); + + describe('getSignificand', () => { + it('returns expected values', () => { + // The number 1.5 has a single most-significant bit set, i.e., 1<<51. + assert.strictEqual(ieee754.getSignificand(1.5), Math.pow(2, 51)); + }); + }); +}); From f02cc2f0de4a12ed01b08ef2f1842da95798cc58 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Wed, 4 Jan 2023 09:20:21 -0800 Subject: [PATCH 02/10] Apply suggestions from code review Co-authored-by: Marc Pichler Co-authored-by: Daniel Dyla --- .../mapping/ExponentMapping.ts | 22 +++++-------------- .../mapping/LogarithmMapping.ts | 10 +++------ .../exponential-histogram/mapping/ieee754.ts | 4 ++-- .../LogarithmMapping.test.ts | 2 +- .../exponential-histogram/helpers.ts | 2 +- 5 files changed, 13 insertions(+), 27 deletions(-) diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts index 953cd81c1f..373fca5341 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts @@ -18,25 +18,15 @@ import * as util from '../util'; import { Mapping, MappingError } from './types'; /** - * ExponentMapping implements a exponential mapping functions for - * for scales <=0. For scales > 0 LogarithmMapping should be used. + * ExponentMapping implements exponential mapping functions for + * scales <=0. For scales > 0 LogarithmMapping should be used. */ export class ExponentMapping implements Mapping { static readonly MIN_SCALE = -10; static readonly MAX_SCALE = 0; - private static readonly _PREBUILT_MAPPINGS = [ - new ExponentMapping(10), - new ExponentMapping(9), - new ExponentMapping(8), - new ExponentMapping(7), - new ExponentMapping(6), - new ExponentMapping(5), - new ExponentMapping(4), - new ExponentMapping(3), - new ExponentMapping(2), - new ExponentMapping(1), - new ExponentMapping(0), - ]; + private static readonly _PREBUILT_MAPPINGS = Array(11) + .fill(10) + .map((v, i) => new ExponentMapping(v - i)) /** * Returns the pre-built mapping for the given scale @@ -76,7 +66,7 @@ export class ExponentMapping implements Mapping { // In case the value is an exact power of two, compute a // correction of -1. Note, we are using a custom _rightShift - // to accomodate a 52-bit argument, which the native bitwise + // to accommodate a 52-bit argument, which the native bitwise // operators do not support const correction = this._rightShift( ieee754.getSignificand(value) - 1, diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts index cf99f44508..767ee3dfa0 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts @@ -42,14 +42,10 @@ export class LogarithmMapping implements Mapping { ); } - let mapping = this._PREBUILT_MAPPINGS.get(scale); - if (mapping) { - return mapping; + if (!this._PREBUILT_MAPPINGS.has(scale)) { + this._PREBUILT_MAPPINGS.set(scale, new LogarithmMapping(scale)); } - - mapping = new LogarithmMapping(scale); - this._PREBUILT_MAPPINGS.set(scale, mapping); - return mapping; + return this._PREBUILT_MAPPINGS.get(scale); } private readonly _scale: number; diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts index 0dac0070ad..bf612a59fd 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts @@ -27,7 +27,7 @@ const EXPONENT_MASK = 0x7ff00000; */ const SIGNIFICAND_MASK = 0xfffff; -/* +/** * EXPONENT_BIAS is the exponent bias specified for encoding * the IEEE 754 double-precision floating point exponent: 1023 */ @@ -39,7 +39,7 @@ const EXPONENT_BIAS = 1023; */ export const MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1; -/* +/** * MAX_NORMAL_EXPONENT is the maximum exponent of a normalized * floating point: 1023. */ diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts index c5bd1c9f1d..d090e3492f 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts @@ -27,7 +27,7 @@ describe('LogarithmMapping', () => { it('maps values for scale 1', () => { const mapping = LogarithmMapping.get(1); - assert.strictEqual(1, mapping.scale()); + assert.strictEqual(mapping.scale(), 1); const expectedMappings = [ [15, 7], diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts index 5f89b34771..2636508e82 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts @@ -21,7 +21,7 @@ export function assertInEpsilon( epsilon: number ) { assert.ok(!Number.isNaN(actual), 'unexpected NaN for actual argument'); - assert.ok(!Number.isNaN(expected), 'unexpected NaN for exepected argument'); + assert.ok(!Number.isNaN(expected), 'unexpected NaN for expected argument'); assert.ok(actual !== 0, 'unexpected 0 for actual argument'); const relErr = Math.abs(actual - expected) / Math.abs(actual); From 1c0e8fb561bd5549463044c2180f32086bd91429 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Wed, 4 Jan 2023 14:23:49 -0800 Subject: [PATCH 03/10] chore: fix compile --- .../exponential-histogram/mapping/LogarithmMapping.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts index 767ee3dfa0..06dfbb3cd0 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts @@ -45,7 +45,7 @@ export class LogarithmMapping implements Mapping { if (!this._PREBUILT_MAPPINGS.has(scale)) { this._PREBUILT_MAPPINGS.set(scale, new LogarithmMapping(scale)); } - return this._PREBUILT_MAPPINGS.get(scale); + return this._PREBUILT_MAPPINGS.get(scale)!; } private readonly _scale: number; From 30e456900390f5e371274ec8187209f0404b6efb Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Wed, 4 Jan 2023 14:31:47 -0800 Subject: [PATCH 04/10] refactor: use Number.MAX_VALUE directly --- .../exponential-histogram/mapping/ieee754.ts | 5 ----- .../exponential-histogram/ExponentMapping.test.ts | 2 +- .../exponential-histogram/LogarithmMapping.test.ts | 14 +++++++------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts index bf612a59fd..224a64fc5a 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts @@ -45,11 +45,6 @@ export const MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1; */ export const MAX_NORMAL_EXPONENT = EXPONENT_BIAS; -/** - * MAX_VALUE is the largest normal number - */ -export const MAX_VALUE = Number.MAX_VALUE; - /** * MIN_VALUE is the smallest normal number */ diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts index 47b160a28d..ffadbdf0ba 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts @@ -240,7 +240,7 @@ describe('ExponentMapping', () => { scale++ ) { const mapping = ExponentMapping.get(scale); - const index = mapping.mapToIndex(ieee754.MAX_VALUE); + const index = mapping.mapToIndex(Number.MAX_VALUE); const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) >> -scale) - 1; assert.strictEqual( index, diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts index d090e3492f..d8187acc95 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts @@ -75,7 +75,7 @@ describe('LogarithmMapping', () => { scale++ ) { const mapping = LogarithmMapping.get(scale); - const index = mapping.mapToIndex(ieee754.MAX_VALUE); + const index = mapping.mapToIndex(Number.MAX_VALUE); // the max index is one less than the first index that // overflows Number.MAX_VALUE @@ -87,13 +87,13 @@ describe('LogarithmMapping', () => { const base = mapping.lowerBoundary(1); assert.ok( - boundary < ieee754.MAX_VALUE, - `expected boundary: ${boundary} to be < max value: ${ieee754.MAX_VALUE}` + boundary < Number.MAX_VALUE, + `expected boundary: ${boundary} to be < max value: ${Number.MAX_VALUE}` ); assertInEpsilon( base - 1, - (ieee754.MAX_VALUE - boundary) / boundary, + (Number.MAX_VALUE - boundary) / boundary, 10e-6 ); } @@ -155,17 +155,17 @@ describe('LogarithmMapping', () => { scale++ ) { const mapping = LogarithmMapping.get(scale); - const index = mapping.mapToIndex(ieee754.MAX_VALUE); + const index = mapping.mapToIndex(Number.MAX_VALUE); const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) << scale) - 1; assert.strictEqual(maxIndex, index); const boundary = mapping.lowerBoundary(index); const base = mapping.lowerBoundary(1); - assert.ok(boundary < ieee754.MAX_VALUE); + assert.ok(boundary < Number.MAX_VALUE); assertInEpsilon( base - 1, - (ieee754.MAX_VALUE - boundary) / boundary, + (Number.MAX_VALUE - boundary) / boundary, 1e-6 ); From daada4c23cbe58c0534066d236622e0b21cd4c28 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Wed, 4 Jan 2023 16:01:09 -0800 Subject: [PATCH 05/10] chore: add docs to mapping and ieee754 --- .../exponential-histogram/mapping/ieee754.ts | 14 +++++++++++++- .../exponential-histogram/mapping/types.ts | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts index 224a64fc5a..0dc4f01bc3 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ieee754.ts @@ -13,6 +13,17 @@ * 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; /** @@ -79,7 +90,8 @@ export function getSignificand(value: number): number { // 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 + // 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; diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts index d54d778131..afe6ed9118 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/types.ts @@ -15,6 +15,11 @@ */ 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; From c47ec0686dc42c9102e0af8a4b70507f60af62f8 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Wed, 4 Jan 2023 16:09:46 -0800 Subject: [PATCH 06/10] chore: move MIN_SCALE and MAX_SCALE to unexported constants --- .../mapping/ExponentMapping.ts | 16 +++++++++------- .../mapping/LogarithmMapping.ts | 11 ++++++----- .../ExponentMapping.test.ts | 17 ++++++++++------- .../LogarithmMapping.test.ts | 15 +++++++++------ 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts index 373fca5341..ee4551d553 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts @@ -17,13 +17,15 @@ import * as ieee754 from './ieee754'; import * as util from '../util'; import { Mapping, MappingError } from './types'; +const MIN_SCALE = -10; +const MAX_SCALE = 0; + /** * ExponentMapping implements exponential mapping functions for * scales <=0. For scales > 0 LogarithmMapping should be used. */ export class ExponentMapping implements Mapping { - static readonly MIN_SCALE = -10; - static readonly MAX_SCALE = 0; + private static readonly _PREBUILT_MAPPINGS = Array(11) .fill(10) .map((v, i) => new ExponentMapping(v - i)) @@ -34,19 +36,19 @@ export class ExponentMapping implements Mapping { * @returns {ExponentMapping} */ public static get(scale: number) { - if (scale > ExponentMapping.MAX_SCALE) { + if (scale > MAX_SCALE) { throw new MappingError( - `exponent mapping requires scale <= ${ExponentMapping.MAX_SCALE}` + `exponent mapping requires scale <= ${MAX_SCALE}` ); } - if (scale < ExponentMapping.MIN_SCALE) { + if (scale < MIN_SCALE) { throw new MappingError( - `exponent mapping requires a scale > ${ExponentMapping.MIN_SCALE}` + `exponent mapping requires a scale > ${MIN_SCALE}` ); } return ExponentMapping._PREBUILT_MAPPINGS[ - scale - ExponentMapping.MIN_SCALE + scale - MIN_SCALE ]; } diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts index 06dfbb3cd0..1f6baef50d 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts @@ -17,13 +17,14 @@ import * as ieee754 from './ieee754'; import * as util from '../util'; import { Mapping, MappingError } from './types'; +const MIN_SCALE = 1; +const MAX_SCALE = 20; + /** * LogarithmMapping implements exponential mapping functions for scale > 0. * For scales <= 0 the exponent mapping should be used. */ export class LogarithmMapping implements Mapping { - static readonly MIN_SCALE = 1; - static readonly MAX_SCALE = 20; private static _PREBUILT_MAPPINGS = new Map(); /** @@ -34,11 +35,11 @@ export class LogarithmMapping implements Mapping { */ static get(scale: number): LogarithmMapping { if ( - scale > LogarithmMapping.MAX_SCALE || - scale < LogarithmMapping.MIN_SCALE + scale > MAX_SCALE || + scale < MIN_SCALE ) { throw new MappingError( - `logarithm mapping requires scale in the range [${LogarithmMapping.MIN_SCALE}, ${LogarithmMapping.MAX_SCALE}]` + `logarithm mapping requires scale in the range [${MIN_SCALE}, ${MAX_SCALE}]` ); } diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts index ffadbdf0ba..918fd75fd6 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts @@ -17,6 +17,9 @@ import { ExponentMapping } from '../../../src/aggregator/exponential-histogram/m import * as ieee754 from '../../../src/aggregator/exponential-histogram/mapping/ieee754'; import * as assert from 'assert'; +const MIN_SCALE = -10; +const MAX_SCALE = 0; + describe('ExponentMapping', () => { it('maps expected values for scale 0', () => { const mapping = ExponentMapping.get(0); @@ -67,8 +70,8 @@ describe('ExponentMapping', () => { }); it('maps expected values for min scale', () => { - const mapping = ExponentMapping.get(ExponentMapping.MIN_SCALE); - assert.strictEqual(mapping.scale(), ExponentMapping.MIN_SCALE); + const mapping = ExponentMapping.get(MIN_SCALE); + assert.strictEqual(mapping.scale(), MIN_SCALE); const expectedMappings = [ [1.000001, 0], @@ -229,14 +232,14 @@ describe('ExponentMapping', () => { ExponentMapping.get(1); }); assert.throws(() => { - ExponentMapping.get(ExponentMapping.MIN_SCALE - 1); + ExponentMapping.get(MIN_SCALE - 1); }); }); it('handles max index for all scales', () => { for ( - let scale = ExponentMapping.MIN_SCALE; - scale <= ExponentMapping.MAX_SCALE; + let scale = MIN_SCALE; + scale <= MAX_SCALE; scale++ ) { const mapping = ExponentMapping.get(scale); @@ -260,8 +263,8 @@ describe('ExponentMapping', () => { it('handles min index for all scales', () => { for ( - let scale = ExponentMapping.MIN_SCALE; - scale <= ExponentMapping.MAX_SCALE; + let scale = MIN_SCALE; + scale <= MAX_SCALE; scale++ ) { const mapping = ExponentMapping.get(scale); diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts index d8187acc95..65a06b1e37 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts @@ -18,6 +18,9 @@ import * as ieee754 from '../../../src/aggregator/exponential-histogram/mapping/ import * as assert from 'assert'; import { assertInEpsilon } from './helpers'; +const MIN_SCALE = 1; +const MAX_SCALE = 20; + describe('LogarithmMapping', () => { it('throws for invalid scale', () => { assert.throws(() => { @@ -70,8 +73,8 @@ describe('LogarithmMapping', () => { it('handles max index for each scale', () => { for ( - let scale = LogarithmMapping.MIN_SCALE; - scale <= LogarithmMapping.MAX_SCALE; + let scale = MIN_SCALE; + scale <= MAX_SCALE; scale++ ) { const mapping = LogarithmMapping.get(scale); @@ -101,8 +104,8 @@ describe('LogarithmMapping', () => { it('handles min index for each scale', () => { for ( - let scale = LogarithmMapping.MIN_SCALE; - scale <= LogarithmMapping.MAX_SCALE; + let scale = MIN_SCALE; + scale <= MAX_SCALE; scale++ ) { const mapping = LogarithmMapping.get(scale); @@ -150,8 +153,8 @@ describe('LogarithmMapping', () => { it('maps max float to max index for each scale', () => { for ( - let scale = LogarithmMapping.MIN_SCALE; - scale <= LogarithmMapping.MAX_SCALE; + let scale = MIN_SCALE; + scale <= MAX_SCALE; scale++ ) { const mapping = LogarithmMapping.get(scale); From 125df3cf2d0bc0bacf4bf14278c753f3467ec697 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Wed, 4 Jan 2023 16:11:12 -0800 Subject: [PATCH 07/10] chore: remove currently unused test helper --- .../test/aggregator/exponential-histogram/helpers.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts index 2636508e82..a2241fbf66 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/helpers.ts @@ -31,11 +31,3 @@ export function assertInEpsilon( `expected relative error: ${relErr} to be < ${epsilon}` ); } - -export function assertInDelta(actual: number, expected: number, delta: number) { - const actualDelta = Math.abs(expected - actual); - assert.ok( - actualDelta < delta, - `expected delta: ${delta} to be < ${actualDelta}` - ); -} From 44b34f90d08b6aeeda6a04bda61749670487bec4 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Wed, 4 Jan 2023 18:40:38 -0800 Subject: [PATCH 08/10] chore: lint --- .../mapping/ExponentMapping.ts | 11 +++------ .../mapping/LogarithmMapping.ts | 5 +--- .../ExponentMapping.test.ts | 12 ++-------- .../LogarithmMapping.test.ts | 24 ++++--------------- 4 files changed, 10 insertions(+), 42 deletions(-) diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts index ee4551d553..483e10cf41 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts @@ -25,10 +25,9 @@ const MAX_SCALE = 0; * scales <=0. For scales > 0 LogarithmMapping should be used. */ export class ExponentMapping implements Mapping { - private static readonly _PREBUILT_MAPPINGS = Array(11) .fill(10) - .map((v, i) => new ExponentMapping(v - i)) + .map((v, i) => new ExponentMapping(v - i)); /** * Returns the pre-built mapping for the given scale @@ -37,9 +36,7 @@ export class ExponentMapping implements Mapping { */ public static get(scale: number) { if (scale > MAX_SCALE) { - throw new MappingError( - `exponent mapping requires scale <= ${MAX_SCALE}` - ); + throw new MappingError(`exponent mapping requires scale <= ${MAX_SCALE}`); } if (scale < MIN_SCALE) { throw new MappingError( @@ -47,9 +44,7 @@ export class ExponentMapping implements Mapping { ); } - return ExponentMapping._PREBUILT_MAPPINGS[ - scale - MIN_SCALE - ]; + return ExponentMapping._PREBUILT_MAPPINGS[scale - MIN_SCALE]; } private constructor(private readonly _shift: number) {} diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts index 1f6baef50d..ad3c4e35f1 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts @@ -34,10 +34,7 @@ export class LogarithmMapping implements Mapping { * @returns {LogarithmMapping} */ static get(scale: number): LogarithmMapping { - if ( - scale > MAX_SCALE || - scale < MIN_SCALE - ) { + if (scale > MAX_SCALE || scale < MIN_SCALE) { throw new MappingError( `logarithm mapping requires scale in the range [${MIN_SCALE}, ${MAX_SCALE}]` ); diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts index 918fd75fd6..965f250853 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts @@ -237,11 +237,7 @@ describe('ExponentMapping', () => { }); it('handles max index for all scales', () => { - for ( - let scale = MIN_SCALE; - scale <= MAX_SCALE; - scale++ - ) { + for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { const mapping = ExponentMapping.get(scale); const index = mapping.mapToIndex(Number.MAX_VALUE); const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) >> -scale) - 1; @@ -262,11 +258,7 @@ describe('ExponentMapping', () => { }); it('handles min index for all scales', () => { - for ( - let scale = MIN_SCALE; - scale <= MAX_SCALE; - scale++ - ) { + for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { const mapping = ExponentMapping.get(scale); const minIndex = mapping.mapToIndex(ieee754.MIN_VALUE); let expectedMinIndex = ieee754.MIN_NORMAL_EXPONENT >> -scale; diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts index 65a06b1e37..e0f0a741c0 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts @@ -72,11 +72,7 @@ describe('LogarithmMapping', () => { }); it('handles max index for each scale', () => { - for ( - let scale = MIN_SCALE; - scale <= MAX_SCALE; - scale++ - ) { + for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { const mapping = LogarithmMapping.get(scale); const index = mapping.mapToIndex(Number.MAX_VALUE); @@ -103,11 +99,7 @@ describe('LogarithmMapping', () => { }); it('handles min index for each scale', () => { - for ( - let scale = MIN_SCALE; - scale <= MAX_SCALE; - scale++ - ) { + for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { const mapping = LogarithmMapping.get(scale); const minIndex = mapping.mapToIndex(ieee754.MIN_VALUE); @@ -152,11 +144,7 @@ describe('LogarithmMapping', () => { }); it('maps max float to max index for each scale', () => { - for ( - let scale = MIN_SCALE; - scale <= MAX_SCALE; - scale++ - ) { + for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { const mapping = LogarithmMapping.get(scale); const index = mapping.mapToIndex(Number.MAX_VALUE); const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) << scale) - 1; @@ -166,11 +154,7 @@ describe('LogarithmMapping', () => { const base = mapping.lowerBoundary(1); assert.ok(boundary < Number.MAX_VALUE); - assertInEpsilon( - base - 1, - (Number.MAX_VALUE - boundary) / boundary, - 1e-6 - ); + assertInEpsilon(base - 1, (Number.MAX_VALUE - boundary) / boundary, 1e-6); //one larger will overflow assert.throws(() => { From 50cb6d3824818a19d178f9300f8a785aa0df5ad0 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Mon, 9 Jan 2023 19:09:12 -0800 Subject: [PATCH 09/10] refactor: build all scales, extract single getMapping function --- .../mapping/ExponentMapping.ts | 27 ++--------- .../mapping/LogarithmMapping.ts | 26 +---------- .../mapping/getMapping.ts | 44 ++++++++++++++++++ .../ExponentMapping.test.ts | 21 +++------ .../LogarithmMapping.test.ts | 16 +++---- .../exponential-histogram/getMapping.test.ts | 45 +++++++++++++++++++ 6 files changed, 104 insertions(+), 75 deletions(-) create mode 100644 packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts create mode 100644 packages/sdk-metrics/test/aggregator/exponential-histogram/getMapping.test.ts diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts index 483e10cf41..49662b44f4 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/ExponentMapping.ts @@ -17,38 +17,17 @@ import * as ieee754 from './ieee754'; import * as util from '../util'; import { Mapping, MappingError } from './types'; -const MIN_SCALE = -10; -const MAX_SCALE = 0; - /** * ExponentMapping implements exponential mapping functions for * scales <=0. For scales > 0 LogarithmMapping should be used. */ export class ExponentMapping implements Mapping { - private static readonly _PREBUILT_MAPPINGS = Array(11) - .fill(10) - .map((v, i) => new ExponentMapping(v - i)); + private readonly _shift: number; - /** - * Returns the pre-built mapping for the given scale - * @param scale An integer >= -10 and <= 0 - * @returns {ExponentMapping} - */ - public static get(scale: number) { - if (scale > MAX_SCALE) { - throw new MappingError(`exponent mapping requires scale <= ${MAX_SCALE}`); - } - if (scale < MIN_SCALE) { - throw new MappingError( - `exponent mapping requires a scale > ${MIN_SCALE}` - ); - } - - return ExponentMapping._PREBUILT_MAPPINGS[scale - MIN_SCALE]; + constructor(scale: number) { + this._shift = -scale; } - private constructor(private readonly _shift: number) {} - /** * Maps positive floating point values to indexes corresponding to scale * @param value diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts index ad3c4e35f1..974d9ff84e 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/LogarithmMapping.ts @@ -17,40 +17,16 @@ import * as ieee754 from './ieee754'; import * as util from '../util'; import { Mapping, MappingError } from './types'; -const MIN_SCALE = 1; -const MAX_SCALE = 20; - /** * LogarithmMapping implements exponential mapping functions for scale > 0. * For scales <= 0 the exponent mapping should be used. */ export class LogarithmMapping implements Mapping { - private static _PREBUILT_MAPPINGS = new Map(); - - /** - * get creates or returns a memoized logarithm mapping function for - * the given scale. used for scales > 0. - * @param scale - a number > 0 and <= 20 - * @returns {LogarithmMapping} - */ - static get(scale: number): LogarithmMapping { - if (scale > MAX_SCALE || scale < MIN_SCALE) { - throw new MappingError( - `logarithm mapping requires scale in the range [${MIN_SCALE}, ${MAX_SCALE}]` - ); - } - - if (!this._PREBUILT_MAPPINGS.has(scale)) { - this._PREBUILT_MAPPINGS.set(scale, new LogarithmMapping(scale)); - } - return this._PREBUILT_MAPPINGS.get(scale)!; - } - private readonly _scale: number; private readonly _scaleFactor: number; private readonly _inverseFactor: number; - private constructor(scale: number) { + constructor(scale: number) { this._scale = scale; this._scaleFactor = util.ldexp(Math.LOG2E, scale); this._inverseFactor = util.ldexp(Math.LN2, -scale); diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts new file mode 100644 index 0000000000..a134e21573 --- /dev/null +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts @@ -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: 30 }, (_, 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]; +} diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts index 965f250853..8a9ed82157 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/ExponentMapping.test.ts @@ -22,7 +22,7 @@ const MAX_SCALE = 0; describe('ExponentMapping', () => { it('maps expected values for scale 0', () => { - const mapping = ExponentMapping.get(0); + const mapping = new ExponentMapping(0); assert.strictEqual(mapping.scale(), 0); const expectedMappings = [ @@ -70,7 +70,7 @@ describe('ExponentMapping', () => { }); it('maps expected values for min scale', () => { - const mapping = ExponentMapping.get(MIN_SCALE); + const mapping = new ExponentMapping(MIN_SCALE); assert.strictEqual(mapping.scale(), MIN_SCALE); const expectedMappings = [ @@ -93,7 +93,7 @@ describe('ExponentMapping', () => { }); it('maps expected values for scale -1', () => { - const mapping = ExponentMapping.get(-1); + const mapping = new ExponentMapping(-1); assert.strictEqual(mapping.scale(), -1); const expectedMappings = [ @@ -130,7 +130,7 @@ describe('ExponentMapping', () => { }); it('maps expected values for scale -4', () => { - const mapping = ExponentMapping.get(-4); + const mapping = new ExponentMapping(-4); assert.strictEqual(mapping.scale(), -4); const expectedMappings = [ @@ -227,18 +227,9 @@ describe('ExponentMapping', () => { }); }); - it('throws errors for invalid scales', () => { - assert.throws(() => { - ExponentMapping.get(1); - }); - assert.throws(() => { - ExponentMapping.get(MIN_SCALE - 1); - }); - }); - it('handles max index for all scales', () => { for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { - const mapping = ExponentMapping.get(scale); + const mapping = new ExponentMapping(scale); const index = mapping.mapToIndex(Number.MAX_VALUE); const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) >> -scale) - 1; assert.strictEqual( @@ -259,7 +250,7 @@ describe('ExponentMapping', () => { it('handles min index for all scales', () => { for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { - const mapping = ExponentMapping.get(scale); + const mapping = new ExponentMapping(scale); const minIndex = mapping.mapToIndex(ieee754.MIN_VALUE); let expectedMinIndex = ieee754.MIN_NORMAL_EXPONENT >> -scale; if (ieee754.MIN_NORMAL_EXPONENT % (1 << -scale) === 0) { diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts index e0f0a741c0..d6d04bf814 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/LogarithmMapping.test.ts @@ -22,14 +22,8 @@ const MIN_SCALE = 1; const MAX_SCALE = 20; describe('LogarithmMapping', () => { - it('throws for invalid scale', () => { - assert.throws(() => { - LogarithmMapping.get(-1); - }); - }); - it('maps values for scale 1', () => { - const mapping = LogarithmMapping.get(1); + const mapping = new LogarithmMapping(1); assert.strictEqual(mapping.scale(), 1); const expectedMappings = [ @@ -59,7 +53,7 @@ describe('LogarithmMapping', () => { it('computes boundary', () => { [1, 2, 3, 4, 10, 15].forEach(scale => { - const mapping = LogarithmMapping.get(scale); + const mapping = new LogarithmMapping(scale); [-100, -10, -1, 0, 1, 10, 100].forEach(index => { const boundary = mapping.lowerBoundary(index); const mappedIndex = mapping.mapToIndex(boundary); @@ -73,7 +67,7 @@ describe('LogarithmMapping', () => { it('handles max index for each scale', () => { for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { - const mapping = LogarithmMapping.get(scale); + const mapping = new LogarithmMapping(scale); const index = mapping.mapToIndex(Number.MAX_VALUE); // the max index is one less than the first index that @@ -100,7 +94,7 @@ describe('LogarithmMapping', () => { it('handles min index for each scale', () => { for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { - const mapping = LogarithmMapping.get(scale); + const mapping = new LogarithmMapping(scale); const minIndex = mapping.mapToIndex(ieee754.MIN_VALUE); const expectedMinIndex = (ieee754.MIN_NORMAL_EXPONENT << scale) - 1; @@ -145,7 +139,7 @@ describe('LogarithmMapping', () => { it('maps max float to max index for each scale', () => { for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { - const mapping = LogarithmMapping.get(scale); + const mapping = new LogarithmMapping(scale); const index = mapping.mapToIndex(Number.MAX_VALUE); const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) << scale) - 1; assert.strictEqual(maxIndex, index); diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/getMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/getMapping.test.ts new file mode 100644 index 0000000000..8c77245f4b --- /dev/null +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/getMapping.test.ts @@ -0,0 +1,45 @@ +/* + * 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 '../../../src/aggregator/exponential-histogram/mapping/ExponentMapping'; +import { LogarithmMapping } from '../../../src/aggregator/exponential-histogram/mapping/LogarithmMapping'; +import { getMapping } from '../../../src/aggregator/exponential-histogram/mapping/getMapping'; +import * as assert from 'assert'; + +const MIN_SCALE = -10; +const MAX_SCALE = 20; + +describe('getMapping', () => { + it('returns correct mapping for all scales', () => { + for (let scale = MIN_SCALE; scale < MAX_SCALE; scale++) { + const mapping = getMapping(scale); + if (scale > 0) { + assert.ok(mapping instanceof LogarithmMapping); + } else { + assert.ok(mapping instanceof ExponentMapping); + } + assert.strictEqual(mapping.scale(), scale); + } + }); + + it('throws for invalid scales', () => { + assert.throws(() => { + getMapping(MIN_SCALE - 1); + }); + assert.throws(() => { + getMapping(MAX_SCALE + 1); + }); + }); +}); From 76bda759c6130c08fbbd8544bb5d72e42d0e7649 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Tue, 10 Jan 2023 14:25:38 -0800 Subject: [PATCH 10/10] fix: off by one error when pre-building mappings --- .../src/aggregator/exponential-histogram/mapping/getMapping.ts | 2 +- .../test/aggregator/exponential-histogram/getMapping.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts index a134e21573..ce8949b325 100644 --- a/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts +++ b/packages/sdk-metrics/src/aggregator/exponential-histogram/mapping/getMapping.ts @@ -19,7 +19,7 @@ import { MappingError, Mapping } from './types'; const MIN_SCALE = -10; const MAX_SCALE = 20; -const PREBUILT_MAPPINGS = Array.from({ length: 30 }, (_, i) => { +const PREBUILT_MAPPINGS = Array.from({ length: 31 }, (_, i) => { if (i > 10) { return new LogarithmMapping(i - 10); } diff --git a/packages/sdk-metrics/test/aggregator/exponential-histogram/getMapping.test.ts b/packages/sdk-metrics/test/aggregator/exponential-histogram/getMapping.test.ts index 8c77245f4b..4d7326be54 100644 --- a/packages/sdk-metrics/test/aggregator/exponential-histogram/getMapping.test.ts +++ b/packages/sdk-metrics/test/aggregator/exponential-histogram/getMapping.test.ts @@ -23,7 +23,7 @@ const MAX_SCALE = 20; describe('getMapping', () => { it('returns correct mapping for all scales', () => { - for (let scale = MIN_SCALE; scale < MAX_SCALE; scale++) { + for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) { const mapping = getMapping(scale); if (scale > 0) { assert.ok(mapping instanceof LogarithmMapping);