Skip to content
Closed
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
18 changes: 15 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,22 @@ if (PerformanceEntry) {
Object.setPrototypeOf(PerformanceNodeTiming, PerformanceEntry);
}

// Own-property shape taken from Node's lib/internal/perf/nodetiming.js.
const performanceNodeTimingEntryDescriptors = {
__proto__: null,
name: { __proto__: null, value: "node", enumerable: true, configurable: true },
entryType: { __proto__: null, value: "node", enumerable: true, configurable: true },
startTime: { __proto__: null, value: 0, enumerable: true, configurable: true },
duration: { __proto__: null, get: () => performance.now(), enumerable: true, configurable: 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 ms offsets from timeOrigin; Bun doesn't record them yet.
object.nodeStart = object.v8Start = object.environment = object.bootstrapComplete = 0;
object.loopStart = object.idleTime = 0;
object.loopExit = -1;
return object;
}
Expand Down
81 changes: 81 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,65 @@ 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).toBeGreaterThan(0);

// 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 defines name/entryType/startTime as own data properties
// (writable:false) and duration as an own getter.
for (const [key, value] of [
["name", "node"],
["entryType", "node"],
["startTime", 0],
] as const) {
expect({ key, ...Object.getOwnPropertyDescriptor(nt, key) }).toEqual({
key,
value,
writable: false,
enumerable: true,
configurable: true,
});
}
const durationDesc = Object.getOwnPropertyDescriptor(nt, "duration");
expect({
enumerable: durationDesc?.enumerable,
configurable: durationDesc?.configurable,
get: typeof durationDesc?.get,
}).toEqual({ enumerable: true, configurable: true, get: "function" });

const json = nt.toJSON();
// duration is a live reading, so assert it's positive and drop it before comparing.
expect(json.duration).toBeGreaterThan(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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,
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("doesn't throw", () => {
expect(() => performance.mark("test")).not.toThrow();
expect(() => performance.measure("test", "test")).not.toThrow();
Expand Down Expand Up @@ -144,6 +203,28 @@ test("timerify and createHistogram survive Object.prototype option pollution", a
expect(stderr).not.toContain("ERR_INVALID_ARG_TYPE");
});

test("nodeTiming initialization survives Object.prototype.value pollution", async () => {
// The own-property descriptor for `duration` is an accessor descriptor; an
// inherited `value` key would make it invalid ("Invalid property descriptor")
// without the null-prototype guard.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`Object.prototype.value = 1;
const nt = require("node:perf_hooks").performance.nodeTiming;
console.log(JSON.stringify({ name: nt.name, entryType: nt.entryType, startTime: nt.startTime }));`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("Invalid property descriptor");
expect(JSON.parse(stdout)).toEqual({ name: "node", entryType: "node", startTime: 0 });
expect(exitCode).toBe(0);
});

test("timerify and AsyncResource.bind survive Object.prototype.get pollution", async () => {
await using proc = Bun.spawn({
cmd: [
Expand Down
Loading