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: Add Exponential Histogram #3498

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add mapping functions for exponential histogram
  • Loading branch information
mwear committed Dec 21, 2022
commit d5a188e98ac227f250c20874201fb3f357333f5a
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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';

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),
];

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) {}

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:
const correction = util.rightShift(
ieee754.getSignificand(value) - 1,
ieee754.SIGNIFICAND_WIDTH
);

return util.rightShift(exp + correction, this._shift);
}

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, util.leftShift(index, this._shift));
}

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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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';

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 greater than 0
* @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);
}

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 util.leftShift(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;
}

lowerBoundary(index: number): number {
const maxIndex = this._maxNormalLowerBoundaryIndex();
if (index >= maxIndex) {
if (index === maxIndex) {
return (
2 *
Math.exp((index - util.leftShift(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 + util.leftShift(1, this._scale)) / this._scaleFactor
) / 2
);
}
throw new MappingError(
`overflow: ${index} is < minimum lower boundary: ${minIndex}`
);
}

return Math.exp(index * this._inverseFactor);
}

scale(): number {
return this._scale;
}

private _minNormalLowerBoundaryIndex(): number {
return util.leftShift(ieee754.MIN_NORMAL_EXPONENT, this._scale);
}

private _maxNormalLowerBoundaryIndex(): number {
return util.leftShift(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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading