Skip to content

Commit

Permalink
feat: exponential histogram - part 1 - mapping functions (#3504)
Browse files Browse the repository at this point in the history
* 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
4 people authored Jan 27, 2023
1 parent 3670071 commit 3bc93a9
Show file tree
Hide file tree
Showing 12 changed files with 1,032 additions and 0 deletions.
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;
}
Loading

0 comments on commit 3bc93a9

Please sign in to comment.