Skip to content

Commit ef3cea0

Browse files
author
John Doe
committed
refactor: impl clock logic
1 parent 1933055 commit ef3cea0

File tree

5 files changed

+288
-0
lines changed

5 files changed

+288
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import process from 'node:process';
2+
import { threadId } from 'node:worker_threads';
3+
4+
export type Microseconds = number;
5+
export type Milliseconds = number;
6+
export type EpochMilliseconds = number;
7+
8+
const hasPerf = (): boolean =>
9+
typeof performance !== 'undefined' && typeof performance.now === 'function';
10+
const hasTimeOrigin = (): boolean =>
11+
hasPerf() && typeof (performance as any).timeOrigin === 'number';
12+
13+
const msToUs = (ms: number): Microseconds => Math.round(ms * 1000);
14+
const usToUs = (us: number): Microseconds => Math.round(us);
15+
16+
/**
17+
* Defines clock utilities for time conversions.
18+
* Handles time origins in NodeJS and the Browser
19+
* Provides process and thread IDs.
20+
* @param init
21+
*/
22+
export interface EpochClockOptions {
23+
pid?: number;
24+
tid?: number;
25+
}
26+
/**
27+
* Creates epoch-based clock utility.
28+
* Epoch time has been the time since January 1, 1970 (UNIX epoch).
29+
* Date.now gives epoch time in milliseconds.
30+
* performance.now() + performance.timeOrigin when available is used for higher precision.
31+
*/
32+
export function epochClock(init: EpochClockOptions = {}) {
33+
const pid = init.pid ?? process.pid;
34+
const tid = init.tid ?? threadId;
35+
36+
const timeOriginMs = hasTimeOrigin()
37+
? ((performance as any).timeOrigin as number)
38+
: undefined;
39+
40+
const epochNowUs = (): Microseconds => {
41+
if (hasTimeOrigin()) {
42+
return msToUs((performance as any).timeOrigin + performance.now());
43+
}
44+
return msToUs(Date.now());
45+
};
46+
47+
const fromEpochUs = (epochUs: Microseconds): Microseconds => usToUs(epochUs);
48+
49+
const fromEpochMs = (epochMs: EpochMilliseconds): Microseconds =>
50+
msToUs(epochMs);
51+
52+
const fromPerfMs = (perfMs: Milliseconds): Microseconds => {
53+
if (timeOriginMs === undefined) {
54+
return epochNowUs() - msToUs(performance.now() - perfMs);
55+
}
56+
return msToUs(timeOriginMs + perfMs);
57+
};
58+
59+
const fromEntryStartTimeMs = (startTimeMs: Milliseconds): Microseconds =>
60+
fromPerfMs(startTimeMs);
61+
const fromDateNowMs = (dateNowMs: EpochMilliseconds): Microseconds =>
62+
fromEpochMs(dateNowMs);
63+
64+
return {
65+
timeOriginMs,
66+
pid,
67+
tid,
68+
69+
hasTimeOrigin,
70+
epochNowUs,
71+
msToUs,
72+
usToUs,
73+
74+
fromEpochMs,
75+
fromEpochUs,
76+
fromPerfMs,
77+
fromEntryStartTimeMs,
78+
fromDateNowMs,
79+
};
80+
}
81+
82+
export const defaultClock = epochClock();
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
import { defaultClock, epochClock } from './clock-epoch';
3+
4+
describe('epochClock', () => {
5+
afterEach(() => {
6+
vi.unstubAllGlobals();
7+
});
8+
9+
it('should create epoch clock with defaults', () => {
10+
const c = epochClock();
11+
expect(c.timeOriginMs).toBe(500_000);
12+
expect(c.tid).toBe(1);
13+
expect(c.pid).toBe(10001);
14+
expect(typeof c.fromEpochMs).toBe('function');
15+
expect(typeof c.fromEpochUs).toBe('function');
16+
expect(typeof c.fromPerfMs).toBe('function');
17+
expect(typeof c.fromEntryStartTimeMs).toBe('function');
18+
expect(typeof c.fromDateNowMs).toBe('function');
19+
});
20+
21+
it('should use pid options', () => {
22+
expect(epochClock({ pid: 999 })).toStrictEqual(
23+
expect.objectContaining({
24+
pid: 999,
25+
tid: 1,
26+
}),
27+
);
28+
});
29+
30+
it('should use tid options', () => {
31+
expect(epochClock({ tid: 888 })).toStrictEqual(
32+
expect.objectContaining({
33+
pid: 10001,
34+
tid: 888,
35+
}),
36+
);
37+
});
38+
39+
it('should return undefined if performance.timeOrigin is NOT present', () => {
40+
Object.defineProperty(performance, 'timeOrigin', {
41+
value: undefined,
42+
writable: true,
43+
configurable: true,
44+
});
45+
const c = epochClock();
46+
expect(c.hasTimeOrigin()).toBe(false);
47+
});
48+
49+
it('should return timeorigin if performance.timeOrigin is present', () => {
50+
const c = epochClock();
51+
expect(c.hasTimeOrigin()).toBe(true);
52+
});
53+
54+
it('should support performance clock by default for epochNowUs', () => {
55+
const c = epochClock();
56+
expect(c.timeOriginMs).toBe(500_000);
57+
expect(c.epochNowUs()).toBe(1_000_000_000); // timeOrigin + (Date.now() - timeOrigin) = Date.now()
58+
});
59+
60+
it('should fallback to Date clock by if performance clock is not given in epochNowUs', () => {
61+
vi.stubGlobal('performance', {
62+
...performance,
63+
timeOrigin: undefined,
64+
});
65+
const c = epochClock();
66+
expect(c.timeOriginMs).toBeUndefined();
67+
expect(c.epochNowUs()).toBe(1_000_000_000); // Date.now() * 1000 when performance unavailable
68+
});
69+
70+
it('should fallback to Date clock by if performance clock is NOT given', () => {
71+
vi.stubGlobal('performance', {
72+
...performance,
73+
timeOrigin: undefined,
74+
});
75+
const c = epochClock();
76+
expect(c.timeOriginMs).toBeUndefined();
77+
expect(c.fromPerfMs(100)).toBe(500_100_000); // epochNowUs() - msToUs(performance.now() - perfMs)
78+
});
79+
80+
it.each([
81+
[1_000_000_000, 1_000_000_000],
82+
[1_001_000_000, 1_001_000_000],
83+
[999_000_000, 999_000_000],
84+
])('should convert epoch microseconds to microseconds', (us, result) => {
85+
const c = epochClock();
86+
expect(c.fromEpochUs(us)).toBe(result);
87+
});
88+
89+
it.each([
90+
[1_000_000, 1_000_000_000],
91+
[1_001_000.5, 1_001_000_500],
92+
[999_000.4, 999_000_400],
93+
])('should convert epoch milliseconds to microseconds', (ms, result) => {
94+
const c = epochClock();
95+
expect(c.fromEpochMs(ms)).toBe(result);
96+
});
97+
98+
it.each([
99+
[0, 500_000_000],
100+
[1_000, 501_000_000],
101+
])(
102+
'should convert performance milliseconds to microseconds',
103+
(perfMs, expected) => {
104+
expect(epochClock().fromPerfMs(perfMs)).toBe(expected);
105+
},
106+
);
107+
108+
it('should convert entry start time to microseconds', () => {
109+
const c = epochClock();
110+
expect([
111+
c.fromEntryStartTimeMs(0),
112+
c.fromEntryStartTimeMs(1_000),
113+
]).toStrictEqual([c.fromPerfMs(0), c.fromPerfMs(1_000)]);
114+
});
115+
116+
it('should convert Date.now() milliseconds to microseconds', () => {
117+
const c = epochClock();
118+
expect([
119+
c.fromDateNowMs(1_000_000),
120+
c.fromDateNowMs(2_000_000),
121+
]).toStrictEqual([1_000_000_000, 2_000_000_000]);
122+
});
123+
124+
it('should maintain conversion consistency', () => {
125+
const c = epochClock();
126+
127+
expect({
128+
fromEpochUs_2B: c.fromEpochUs(2_000_000_000),
129+
fromEpochMs_2M: c.fromEpochMs(2_000_000),
130+
fromEpochUs_1B: c.fromEpochUs(1_000_000_000),
131+
fromEpochMs_1M: c.fromEpochMs(1_000_000),
132+
}).toStrictEqual({
133+
fromEpochUs_2B: 2_000_000_000,
134+
fromEpochMs_2M: 2_000_000_000,
135+
fromEpochUs_1B: 1_000_000_000,
136+
fromEpochMs_1M: 1_000_000_000,
137+
});
138+
});
139+
140+
it.each([
141+
[1_000_000_000.1, 1_000_000_000],
142+
[1_000_000_000.4, 1_000_000_000],
143+
[1_000_000_000.5, 1_000_000_001],
144+
[1_000_000_000.9, 1_000_000_001],
145+
])('should round microseconds correctly', (value, result) => {
146+
const c = epochClock();
147+
expect(c.fromEpochUs(value)).toBe(result);
148+
});
149+
});
150+
151+
describe('defaultClock', () => {
152+
it('should have valid defaultClock export', () => {
153+
expect({
154+
tid: typeof defaultClock.tid,
155+
timeOriginMs: typeof defaultClock.timeOriginMs,
156+
}).toStrictEqual({
157+
tid: 'number',
158+
timeOriginMs: 'number',
159+
});
160+
});
161+
});

testing/test-setup-config/src/lib/vitest-setup-files.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const UNIT_TEST_SETUP_FILES = [
2525
'../../testing/test-setup/src/lib/logger.mock.ts',
2626
'../../testing/test-setup/src/lib/git.mock.ts',
2727
'../../testing/test-setup/src/lib/portal-client.mock.ts',
28+
'../../testing/test-setup/src/lib/process.setup-file.ts',
29+
'../../testing/test-setup/src/lib/clock.setup-file.ts',
2830
...CUSTOM_MATCHERS,
2931
] as const;
3032

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type MockInstance, afterEach, beforeEach, vi } from 'vitest';
2+
3+
const MOCK_DATE_NOW_MS = 1_000_000;
4+
const MOCK_TIME_ORIGIN = 500_000;
5+
6+
const dateNow = MOCK_DATE_NOW_MS;
7+
const performanceTimeOrigin = MOCK_TIME_ORIGIN;
8+
9+
let dateNowSpy: MockInstance<[], number> | undefined;
10+
let performanceNowSpy: MockInstance<[], number> | undefined;
11+
12+
beforeEach(() => {
13+
dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(dateNow);
14+
performanceNowSpy = vi
15+
.spyOn(performance, 'now')
16+
.mockReturnValue(dateNow - performanceTimeOrigin);
17+
18+
vi.stubGlobal('performance', {
19+
...performance,
20+
timeOrigin: performanceTimeOrigin,
21+
});
22+
});
23+
24+
afterEach(() => {
25+
vi.unstubAllGlobals();
26+
27+
if (dateNowSpy) {
28+
dateNowSpy.mockRestore();
29+
dateNowSpy = undefined;
30+
}
31+
if (performanceNowSpy) {
32+
performanceNowSpy.mockRestore();
33+
performanceNowSpy = undefined;
34+
}
35+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import process from 'node:process';
2+
import { beforeEach, vi } from 'vitest';
3+
4+
export const MOCK_PID = 10001;
5+
6+
beforeEach(() => {
7+
vi.spyOn(process, 'pid', 'get').mockReturnValue(MOCK_PID);
8+
});

0 commit comments

Comments
 (0)