Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 19 additions & 3 deletions src/js/node/perf_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class PerformanceNodeTiming {
}

get startTime() {
return this.nodeStart;
// A "node" entry is the timeOrigin reference, so its startTime is always 0.
return 0;
}

get duration() {
Expand Down Expand Up @@ -107,11 +108,26 @@ if (PerformanceEntry) {
Object.setPrototypeOf(PerformanceNodeTiming, PerformanceEntry);
}

// Node exposes the entry accessors as own enumerable properties of the
// nodeTiming object, not only on the prototype. Null-prototype descriptors so
// prototype pollution before this module loads cannot inject extra keys.
const performanceNodeTimingEntryDescriptors = {
__proto__: null,
name: { __proto__: null, get: () => "node", configurable: true, enumerable: true },
entryType: { __proto__: null, get: () => "node", configurable: true, enumerable: true },
startTime: { __proto__: null, get: () => 0, configurable: true, enumerable: true },
duration: { __proto__: null, get: () => performance.now(), configurable: true, enumerable: true },
};

function createPerformanceNodeTiming() {
const object = Object.create(PerformanceNodeTiming.prototype);
Object.defineProperties(object, performanceNodeTimingEntryDescriptors);

object.bootstrapComplete = object.environment = object.nodeStart = object.v8Start = performance.timeOrigin;
object.loopStart = object.idleTime = 1;
// Milestones are offsets in milliseconds from performance.timeOrigin, not
// absolute epoch timestamps. Bun does not record the individual startup
// milestones, so report them relative to the process start (0).
object.nodeStart = object.v8Start = object.environment = object.bootstrapComplete = 0;
object.loopStart = object.idleTime = 0;
object.loopExit = -1;
return object;
}
Expand Down
40 changes: 40 additions & 0 deletions test/js/node/perf_hooks/perf_hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,46 @@ test("stubs", () => {
expect(perf.performance.eventLoopUtilization()).toBeObject();
});

// https://github.com/oven-sh/bun/issues/23041
test("nodeTiming reports offsets from timeOrigin, not epoch timestamps", () => {
const nt = perf.performance.nodeTiming;

expect(nt.name).toBe("node");
expect(nt.entryType).toBe("node");
// A "node" entry is the timeOrigin reference, so startTime is 0 in Node.
expect(nt.startTime).toBe(0);
expect(nt.duration).toBeNumber();

// timeOrigin is epoch-scale; the milestones are offsets from it, so they must
// not themselves be epoch timestamps.
expect(perf.performance.timeOrigin).toBeGreaterThan(1e12);
for (const key of ["nodeStart", "v8Start", "environment", "bootstrapComplete", "loopStart", "idleTime"] as const) {
expect(nt[key]).toBeNumber();
expect(nt[key]).toBeLessThan(1e12);
}
expect(nt.loopExit).toBe(-1);

// Node exposes the entry accessors as own enumerable properties.
expect(Object.keys(nt)).toEqual(expect.arrayContaining(["name", "entryType", "startTime", "duration"]));

const json = nt.toJSON();
// duration is a live reading, so assert its type and drop it before comparing.
expect(json.duration).toBeNumber();
delete json.duration;
expect(json).toEqual({
name: "node",
entryType: "node",
startTime: 0,
nodeStart: nt.nodeStart,
v8Start: nt.v8Start,
bootstrapComplete: nt.bootstrapComplete,
environment: nt.environment,
loopStart: nt.loopStart,
loopExit: nt.loopExit,
idleTime: nt.idleTime,
});
});

test("doesn't throw", () => {
expect(() => performance.mark("test")).not.toThrow();
expect(() => performance.measure("test", "test")).not.toThrow();
Expand Down