Skip to content

Commit c93ab9e

Browse files
authored
fix(core): fix precision loss in numberToHrtime (#3480)
1 parent 3fd6fb8 commit c93ab9e

File tree

3 files changed

+22
-13
lines changed

3 files changed

+22
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/
2424
* `telemetry.sdk.version`
2525
* fix(selenium-tests): updated webpack version for selenium test issue [#3456](https://github.com/open-telemetry/opentelemetry-js/issues/3456) @SaumyaBhushan
2626
* fix(sdk-metrics): fix duplicated registration of metrics for collectors [#3488](https://github.com/open-telemetry/opentelemetry-js/pull/3488) @legendecas
27+
* fix(core): fix precision loss in numberToHrtime [#3480](https://github.com/open-telemetry/opentelemetry-js/pull/3480) @legendecas
2728

2829
### :books: (Refine Doc)
2930

packages/opentelemetry-core/src/common/time.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,20 @@ import { otperformance as performance } from '../platform';
1919
import { TimeOriginLegacy } from './types';
2020

2121
const NANOSECOND_DIGITS = 9;
22+
const NANOSECOND_DIGITS_IN_MILLIS = 6;
23+
const MILLISECONDS_TO_NANOSECONDS = Math.pow(10, NANOSECOND_DIGITS_IN_MILLIS);
2224
const SECOND_TO_NANOSECONDS = Math.pow(10, NANOSECOND_DIGITS);
2325

2426
/**
25-
* Converts a number to HrTime, HrTime = [number, number].
26-
* The first number is UNIX Epoch time in seconds since 00:00:00 UTC on 1 January 1970.
27-
* The second number represents the partial second elapsed since Unix Epoch time represented by first number in nanoseconds.
28-
* For example, 2021-01-01T12:30:10.150Z in UNIX Epoch time in milliseconds is represented as 1609504210150.
29-
* numberToHrtime calculates the first number by converting and truncating the Epoch time in milliseconds to seconds:
30-
* HrTime[0] = Math.trunc(1609504210150 / 1000) = 1609504210.
31-
* numberToHrtime calculates the second number by converting the digits after the decimal point of the subtraction, (1609504210150 / 1000) - HrTime[0], to nanoseconds:
32-
* HrTime[1] = Number((1609504210.150 - HrTime[0]).toFixed(9)) * SECOND_TO_NANOSECONDS = 150000000.
33-
* This is represented in HrTime format as [1609504210, 150000000].
27+
* Converts a number of milliseconds from epoch to HrTime([seconds, remainder in nanoseconds]).
3428
* @param epochMillis
3529
*/
3630
function numberToHrtime(epochMillis: number): api.HrTime {
3731
const epochSeconds = epochMillis / 1000;
3832
// Decimals only.
3933
const seconds = Math.trunc(epochSeconds);
4034
// Round sub-nanosecond accuracy to nanosecond.
41-
const nanos =
42-
Number((epochSeconds - seconds).toFixed(NANOSECOND_DIGITS)) *
43-
SECOND_TO_NANOSECONDS;
35+
const nanos = Math.round((epochMillis % 1000) * MILLISECONDS_TO_NANOSECONDS);
4436
return [seconds, nanos];
4537
}
4638

packages/opentelemetry-core/test/common/time.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('time', () => {
105105
it('should convert Date hrTime', () => {
106106
const timeInput = new Date(1609297640313);
107107
const output = timeInputToHrTime(timeInput);
108-
assert.deepStrictEqual(output, [1609297640, 312999964]);
108+
assert.deepStrictEqual(output, [1609297640, 313000000]);
109109
});
110110

111111
it('should convert epoch milliseconds hrTime', () => {
@@ -114,6 +114,22 @@ describe('time', () => {
114114
assert.deepStrictEqual(output[0], Math.trunc(timeInput / 1000));
115115
});
116116

117+
it('should convert arbitrary epoch milliseconds (with sub-millis precision) hrTime', () => {
118+
sinon.stub(performance, 'timeOrigin').value(111.5);
119+
const inputs = [
120+
// [ input, expected ]
121+
[1609297640313, [1609297640, 313000000]],
122+
// inevitable precision loss without decimal arithmetics.
123+
[1609297640313.333, [1609297640, 313333008]],
124+
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
125+
[1609297640313.333333333, [1609297640, 313333252]],
126+
] as const;
127+
for (const [idx, input] of inputs.entries()) {
128+
const output = timeInputToHrTime(input[0]);
129+
assert.deepStrictEqual(output, input[1], `input[${idx}]: ${input}`);
130+
}
131+
});
132+
117133
it('should convert performance.now() hrTime', () => {
118134
sinon.stub(performance, 'timeOrigin').value(111.5);
119135

0 commit comments

Comments
 (0)