Skip to content

Commit

Permalink
feat: add exponential histogram mapping functions
Browse files Browse the repository at this point in the history
  • Loading branch information
mwear committed Dec 22, 2022
1 parent 1c3af6c commit 88cd0f3
Show file tree
Hide file tree
Showing 10 changed files with 1,035 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/

### :rocket: (Enhancement)

* feat(sdk-metrics): add exponential histogram mapping functions [#3502](https://github.com/open-telemetry/opentelemetry-js/pull/3502) @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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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<number, LogarithmMapping>();

/**
* 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 88cd0f3

Please sign in to comment.