Skip to content
This repository has been archived by the owner on Oct 16, 2022. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyGu committed Jan 2, 2018
0 parents commit 5ba656e
Show file tree
Hide file tree
Showing 21 changed files with 3,040 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 2
indent_style = space
trim_trailing_whitespace = true

[*.js]
max_line_length = 120
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# hr-time

An implementation of the W3C High Resolution Time Level 2 specification [[HR-TIME]][].

[[HR-TIME]]: https://w3c.github.io/hr-time/
11 changes: 11 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";

const { getGlobalMonotonicClockMS } = require("./lib/global-monotonic-clock");
const { Performance } = require("./lib/performance");
const clockIsAccurate = require("./lib/clock-is-accurate");

module.exports = {
Performance,
getGlobalMonotonicClockMS,
clockIsAccurate
};
37 changes: 37 additions & 0 deletions lib/calculate-clock-offset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use strict";

// This files implements the calculation of the offset between the global monotonic clock and UNIX time. This value is
// known as |t1| in the calculation of "time origin timestamp" in the spec. This value needs to be calculated once and
// can be used in all subsequent Performance instances.
//
// However, if the clock is not fast enough, the export is undefined to signify that we should use Date.now() to get the
// time origin timestamp with millisecond accuracy, per spec.

const { getGlobalMonotonicClockMS } = require("./global-monotonic-clock");
const clockIsAccurate = require("./clock-is-accurate");

// This function assumes the clock is accurate.
function calculateClockOffset() {
const start = Date.now();
let cur = start;
while (cur === start) {
cur = Date.now();
}

// At this point |cur| "just" became equal to the next millisecond -- the unseen digits after |cur| are approximately
// all 0, and |cur| is the closest to the actual value of the UNIX time. Now, get the current global monotonic clock
// value and do the remaining calculations.

return cur - getGlobalMonotonicClockMS();
}

if (clockIsAccurate) {
// Warm up the function.
calculateClockOffset();
calculateClockOffset();
calculateClockOffset();

module.exports = calculateClockOffset;
} else {
module.exports = undefined;
}
60 changes: 60 additions & 0 deletions lib/clock-is-accurate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use strict";

const { hrtime } = require("./utils");

// The HR-TIME spec calls for 5-μs accuracy. Check that we have that in both hrtime() and Date.now().

function testClockAccuracy() {
// Test hrtime() first. The check is simpler and more stable, and we use hrtime() to measure Date.now()'s performance.
const roundTrip = hrtime(hrtime());
if (roundTrip[0] > 1 || roundTrip[1] > 5e3 * 2) {
return false;
}

// Test Date.now() twice: first with a looser bound (10 μs) but with a smaller run time to filter out very bad
// Date.now() performance, and then with a tighter bound (5 μs) to check we have the accuracy we need.
let times;
let cur;
let start;
let end;

times = 100;
start = hrtime();
while (times-- > 0) {
cur = Date.now();
}
end = hrtime(start);
if ((end[0] * 1e9 + end[1]) > 1000000) {
return false;
}

times = 10000;
start = hrtime();
while (times-- > 0) {
cur = Date.now();
}
end = hrtime(start);
if ((end[0] * 1e9 + end[1]) > 50000000) {
return false;
}

return true;
}

// Warm up the function.
testClockAccuracy();
testClockAccuracy();
testClockAccuracy();

const TIMES = 5;
const THRESHOLD = .6 * TIMES;
let accurates = 0;
for (let i = 0; i < TIMES; i++) {
if (testClockAccuracy()) {
accurates++;
}
}

const isAccurate = accurates >= THRESHOLD;

module.exports = isAccurate;
10 changes: 10 additions & 0 deletions lib/global-monotonic-clock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use strict";

const { hrtime, toMS } = require("./utils");

// Returns the DOMHighResTimeStamp representing the high resolution time value of the global monotonic clock.
function getGlobalMonotonicClockMS() {
return toMS(hrtime());
}

module.exports = { getGlobalMonotonicClockMS };
47 changes: 47 additions & 0 deletions lib/performance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use strict";

// Actual implementation of the Performance class.

const clockIsAccurate = require("./clock-is-accurate");
const calculateClockOffset = require("./calculate-clock-offset");
const { hrtime, toMS } = require("./utils");

const kTimeOrigin = Symbol("time origin");
const kTimeOriginTimestamp = Symbol("time origin timestamp");

class Performance {
constructor() {
// Time origin.
const timeOrigin = hrtime();
this[kTimeOrigin] = timeOrigin;

if (clockIsAccurate) {
// Let |t1| be the DOMHighResTimeStamp representing the high resolution Unix time at which the global monotonic
// clock is zero. This has to be calculated for every Performance object to account for clock drifts.
const t1 = calculateClockOffset();

// Let |t2| be the DOMHighResTimeStamp representing the high resolution time value of the global monotonic clock
// at global's time origin.
const t2 = toMS(timeOrigin);

// Return the sum of |t1| and |t2|.
this[kTimeOriginTimestamp] = t1 + t2;
} else {
// Clock isn't accurate enough. Use millisecond accuracy per spec.
const cur = Date.now();
this[kTimeOriginTimestamp] = cur;
}
}

// The timeOrigin getter actually returns the time origin timestamp, not the raw time origin.
get timeOrigin() {
return this[kTimeOriginTimestamp];
}

now() {
const diff = toMS(hrtime(this[kTimeOrigin]));
return clockIsAccurate ? diff : Math.round(diff);
}
}

module.exports = { Performance };
11 changes: 11 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";

// Browserify's process implementation doesn't have hrtime, and this package is small so not much of a burden for
// Node.js users.
const hrtime = require("browser-process-hrtime");

function toMS([sec, nanosec]) {
return sec * 1e3 + nanosec / 1e6;
}

module.exports = { hrtime, toMS };
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "hr-time",
"version": "1.0.0",
"description": "An implementation of the W3C High Resolution Time Level 2 specification.",
"main": "index.js",
"repository": "https://github.com/jsdom/hr-time",
"author": "Timothy Gu <[email protected]>",
"license": "MIT",
"private": false,
"dependencies": {
"browser-process-hrtime": "^0.1.2"
},
"devDependencies": {
"jest": "^22.0.4"
},
"scripts": {
"test": "jest"
}
}
11 changes: 11 additions & 0 deletions test/clock-drift.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";

const FACTOR = require("./utils/slow-down-clock");

const { Performance } = require("../");
const timeoutTest = require("./utils/timeout-test");

test("mocked clock drift", () => {
const performance = new Performance();
return timeoutTest(performance, FACTOR);
});
15 changes: 15 additions & 0 deletions test/inaccurate-clock-only-date.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use strict";

require("./utils/inaccurate-clock")("Date.now");

const { clockIsAccurate, Performance } = require("../");
const timeoutTest = require("./utils/timeout-test");
const checkPerformanceNowIntegers = require("./utils/performance-now-integers");

test("mocked inaccurate clock: only Date.now()", () => {
const performance = new Performance();

expect(clockIsAccurate).toBe(false);
checkPerformanceNowIntegers(performance);
return timeoutTest(performance);
});
15 changes: 15 additions & 0 deletions test/inaccurate-clock-only-hrtime.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use strict";

require("./utils/inaccurate-clock")("process.hrtime");

const { clockIsAccurate, Performance } = require("../");
const timeoutTest = require("./utils/timeout-test");
const checkPerformanceNowIntegers = require("./utils/performance-now-integers");

test("mocked inaccurate clock: only process.hrtime()", () => {
const performance = new Performance();

expect(clockIsAccurate).toBe(false);
checkPerformanceNowIntegers(performance);
return timeoutTest(performance);
});
15 changes: 15 additions & 0 deletions test/inaccurate-clock.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use strict";

require("./utils/inaccurate-clock")("all");

const { clockIsAccurate, Performance } = require("../");
const timeoutTest = require("./utils/timeout-test");
const checkPerformanceNowIntegers = require("./utils/performance-now-integers");

test("mocked inaccurate clock: all clocks", () => {
const performance = new Performance();

expect(clockIsAccurate).toBe(false);
checkPerformanceNowIntegers(performance);
return timeoutTest(performance);
});
18 changes: 18 additions & 0 deletions test/normal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use strict";

const { clockIsAccurate, Performance } = require("../");
const timeoutTest = require("./utils/timeout-test");
const checkPerformanceNowSubmillisecond = require("./utils/performance-now-submillisecond");
const checkPerformanceNowIntegers = require("./utils/performance-now-integers");

test("normal timing, without any mocked functions", () => {
const performance = new Performance();

if (clockIsAccurate) {
checkPerformanceNowSubmillisecond(performance);
} else {
checkPerformanceNowIntegers(performance);
}

return timeoutTest(performance);
});
28 changes: 28 additions & 0 deletions test/utils/inaccurate-clock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use strict";

// Emulate inaccurate clock.

const realHrtime = process.hrtime;

function delay(func) {
return (...args) => {
const delayNS = 1e6; // 1 ms delay in clock
const start = realHrtime();
let duration = realHrtime(start);
while (duration[0] < 1 && duration[1] < delayNS) {
duration = realHrtime(start);
}
return func(...args);
};
}

function mock(target) {
if (target === "all" || target === "Date.now") {
Date.now = delay(Date.now);
}
if (target === "all" || target === "process.hrtime") {
process.hrtime = delay(process.hrtime);
}
}

module.exports = mock;
15 changes: 15 additions & 0 deletions test/utils/performance-now-integers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use strict";

// Check performance.now() always returns integers.

const checkPerformanceNowIntegers = performance => {
const start = performance.now();
expect(Number.isInteger(start)).toBe(true);
let end;
do {
end = performance.now();
} while (end === start);
expect(Number.isInteger(end)).toBe(true);
};

module.exports = checkPerformanceNowIntegers;
12 changes: 12 additions & 0 deletions test/utils/performance-now-submillisecond.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use strict";

// Check fractional parts of performance.now().

const checkPerformanceNowSubmillisecond = performance => {
const start = performance.now();
const end = performance.now();
expect(end - start).toBeGreaterThan(0);
expect(end - start).toBeLessThan(1);
};

module.exports = checkPerformanceNowSubmillisecond;
14 changes: 14 additions & 0 deletions test/utils/slow-down-clock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use strict";

// Emulate clock drift.

const realDateNow = Date.now;

const FACTOR = 0.6;
const start = realDateNow();

Date.now = () => {
return start + Math.round((realDateNow() - start) * FACTOR);
};

module.exports = FACTOR;
23 changes: 23 additions & 0 deletions test/utils/timeout-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use strict";

const { getGlobalMonotonicClockMS } = require("../../");

const TIMEOUT = 2000;
const DELTA = 20; // ±20 ms
// Jest expects accuracy to be expressed in terms of number of digits after the decimal point.
const NUM_DIGITS = -Math.log10(DELTA * 2);

function timeoutTest(performance, FACTOR = 1) {
const startDateNow = Date.now();
const startGlobalMonotonicClock = getGlobalMonotonicClockMS();

return new Promise(resolve => {
setTimeout(resolve, TIMEOUT);
}).then(() => {
expect(performance.now()).toBeCloseTo(TIMEOUT, NUM_DIGITS);
expect(Date.now() - startDateNow).toBeCloseTo(TIMEOUT * FACTOR, NUM_DIGITS);
expect(getGlobalMonotonicClockMS() - startGlobalMonotonicClock).toBeCloseTo(TIMEOUT, NUM_DIGITS);
});
};

module.exports = timeoutTest;
Loading

0 comments on commit 5ba656e

Please sign in to comment.