Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
74f6f58875 fix: use proper milestone values and module-scope imports
- Set nodeStart/v8Start/environment to 0 (VM start IS timeOrigin in Bun)
- Capture bootstrapComplete once at module load time instead of per-call
- Set loopStart/loopExit to -1 and idleTime to 0 to match Node.js defaults
- Use module-scope import in test file instead of repeated require() calls
- Add assertions for exact nodeStart/v8Start values and bootstrapComplete > 0

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 05:01:20 +00:00
Claude Bot
27b6ab1687 fix(perf_hooks): PerformanceNodeTiming startTime/duration throw TypeError (#23041)
`$toClass` replaced the PerformanceNodeTiming prototype with a new empty
object, discarding the JS getters (startTime, duration, name, entryType,
toJSON) that were supposed to shadow the C++ brand-checked getters on
PerformanceEntry.prototype. When those C++ getters were reached, they
performed a jsDynamicCast<JSPerformanceEntry*> which failed on plain JS
objects, throwing TypeError.

Fix by using Object.setPrototypeOf to set up the inheritance chain while
preserving the existing prototype object with its getters.

Also fix timing values (nodeStart, environment, bootstrapComplete, v8Start)
to be relative offsets from timeOrigin (via performance.now()) instead of
absolute epoch timestamps (performance.timeOrigin), matching Node.js
behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 04:40:46 +00:00
2 changed files with 95 additions and 5 deletions

View File

@@ -44,6 +44,10 @@ var constants = {
};
// PerformanceEntry is not a valid constructor, so we have to fake it.
// We cannot use $toClass here because it replaces the prototype object,
// which would discard our JS getters that need to shadow the C++ getters
// on PerformanceEntry.prototype (which perform brand checks that fail for
// plain JS objects).
class PerformanceNodeTiming {
bootstrapComplete: number = 0;
environment: number = 0;
@@ -53,7 +57,6 @@ class PerformanceNodeTiming {
nodeStart: number = 0;
v8Start: number = 0;
// we have to fake the properties since it's not real
get name() {
return "node";
}
@@ -63,7 +66,7 @@ class PerformanceNodeTiming {
}
get startTime() {
return this.nodeStart;
return 0;
}
get duration() {
@@ -86,14 +89,29 @@ class PerformanceNodeTiming {
};
}
}
$toClass(PerformanceNodeTiming, "PerformanceNodeTiming", PerformanceEntry);
// Set up the prototype chain manually: PerformanceNodeTiming.prototype inherits
// from PerformanceEntry.prototype, but we keep the existing prototype object
// (with its getters) so they properly shadow the C++ brand-checked getters.
Object.setPrototypeOf(PerformanceNodeTiming.prototype, PerformanceEntry.prototype);
Object.setPrototypeOf(PerformanceNodeTiming, PerformanceEntry);
// Capture the bootstrap-complete timestamp once at module load time.
// This is the earliest point we can measure; individual milestones are
// approximated since Bun doesn't track them separately from native code.
const _bootstrapComplete = performance.now();
function createPerformanceNodeTiming() {
const object = Object.create(PerformanceNodeTiming.prototype);
object.bootstrapComplete = object.environment = object.nodeStart = object.v8Start = performance.timeOrigin;
object.loopStart = object.idleTime = 1;
// All values are offsets (ms) relative to performance.timeOrigin.
// In Bun the VM start IS the time origin, so nodeStart/v8Start ≈ 0.
object.nodeStart = 0;
object.v8Start = 0;
object.environment = 0;
object.bootstrapComplete = _bootstrapComplete;
object.loopStart = -1;
object.loopExit = -1;
object.idleTime = 0;
return object;
}

View File

@@ -0,0 +1,72 @@
import { expect, test } from "bun:test";
import { performance, PerformanceEntry } from "node:perf_hooks";
// https://github.com/oven-sh/bun/issues/23041
// perf_hooks PerformanceNodeTiming: startTime/duration throw TypeError,
// timing values should be relative offsets not absolute timestamps
test("PerformanceNodeTiming startTime and duration do not throw", () => {
const nt = performance.nodeTiming;
// These should not throw - they previously threw:
// "The PerformanceEntry.startTime getter can only be used on instances of PerformanceEntry"
expect(() => nt.startTime).not.toThrow();
expect(() => nt.duration).not.toThrow();
// startTime should be 0 (matching Node.js behavior)
expect(nt.startTime).toBe(0);
// duration should be a positive number (elapsed time)
expect(typeof nt.duration).toBe("number");
expect(nt.duration).toBeGreaterThan(0);
});
test("PerformanceNodeTiming has correct name and entryType", () => {
const nt = performance.nodeTiming;
expect(nt.name).toBe("node");
expect(nt.entryType).toBe("node");
});
test("PerformanceNodeTiming timing values are relative offsets, not absolute timestamps", () => {
const nt = performance.nodeTiming;
// nodeStart should be a small offset relative to timeOrigin, not an epoch timestamp.
// Epoch timestamps are > 1e12 (year ~2001+), offsets should be much smaller.
expect(nt.nodeStart).toBeLessThan(10_000); // should be well under 10 seconds
expect(nt.nodeStart).toBeGreaterThanOrEqual(0);
// Same for other timing properties
expect(nt.environment).toBeLessThan(10_000);
expect(nt.bootstrapComplete).toBeLessThan(10_000);
expect(nt.v8Start).toBeLessThan(10_000);
// In Bun, nodeStart and v8Start are 0 (VM start IS the time origin)
expect(nt.nodeStart).toBe(0);
expect(nt.v8Start).toBe(0);
// bootstrapComplete should be > 0 (time taken to bootstrap)
expect(nt.bootstrapComplete).toBeGreaterThan(0);
});
test("PerformanceNodeTiming is instanceof PerformanceEntry", () => {
const nt = performance.nodeTiming;
expect(nt instanceof PerformanceEntry).toBe(true);
});
test("PerformanceNodeTiming toJSON returns correct shape", () => {
const nt = performance.nodeTiming;
const json = nt.toJSON();
expect(json).toHaveProperty("name", "node");
expect(json).toHaveProperty("entryType", "node");
expect(json).toHaveProperty("startTime", 0);
expect(typeof json.duration).toBe("number");
expect(typeof json.nodeStart).toBe("number");
expect(typeof json.bootstrapComplete).toBe("number");
expect(typeof json.environment).toBe("number");
expect(typeof json.v8Start).toBe("number");
expect(typeof json.idleTime).toBe("number");
expect(typeof json.loopStart).toBe("number");
expect(typeof json.loopExit).toBe("number");
});