Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: exponential histogram - part 1 - mapping functions #3504

Merged
merged 14 commits into from
Jan 27, 2023
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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

### :bug: (Bug Fix)

* fix: avoid grpc types dependency [#3551](https://github.com/open-telemetry/opentelemetry-js/pull/3551) @flarna
Expand Down
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));
}
}
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;
}
}
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];
}
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;
}
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;
}
mwear marked this conversation as resolved.
Show resolved Hide resolved
Loading