Files
bun.sh/src/js/node/perf_hooks.ts
robobun edb7214e6c feat(perf_hooks): Implement monitorEventLoopDelay() for Node.js compatibility (#22429)
## Summary
This PR implements `perf_hooks.monitorEventLoopDelay()` for Node.js
compatibility, enabling monitoring of event loop delays and collection
of performance metrics via histograms.

Fixes #17650

## Implementation Details

### JavaScript Layer (`perf_hooks.ts`)
- Added `IntervalHistogram` class with:
  - `enable()` / `disable()` methods with proper state tracking
  - `reset()` method to clear histogram data
  - Properties: `min`, `max`, `mean`, `stddev`, `exceeds`, `percentiles`
  - `percentile(p)` method with validation
- Full input validation matching Node.js behavior (TypeError vs
RangeError)

### C++ Bindings (`JSNodePerformanceHooksHistogramPrototype.cpp`)
- `jsFunction_monitorEventLoopDelay` - Creates histogram for event loop
monitoring
- `jsFunction_enableEventLoopDelay` - Enables monitoring and starts
timer
- `jsFunction_disableEventLoopDelay` - Disables monitoring and stops
timer
- `JSNodePerformanceHooksHistogram_recordDelay` - Records delay
measurements

### Zig Implementation (`EventLoopDelayMonitor.zig`)
- Embedded `EventLoopTimer` that fires periodically based on resolution
- Tracks last fire time and calculates delay between expected vs actual
- Records delays > 0 to the histogram
- Integrates seamlessly with existing Timer system

## Testing
 All tests pass:
- Custom test suite with 8 comprehensive tests
- Adapted Node.js core test for full compatibility
- Tests cover enable/disable behavior, percentiles, error handling, and
delay recording

## Test plan
- [x] Run `bun test
test/js/node/perf_hooks/test-monitorEventLoopDelay.test.js`
- [x] Run adapted Node.js test
`test/js/node/test/sequential/test-performance-eventloopdelay-adapted.test.js`
- [x] Verify proper error handling for invalid arguments
- [x] Confirm delay measurements are recorded correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-09-06 00:31:32 -07:00

233 lines
6.9 KiB
TypeScript

// Hardcoded module "node:perf_hooks"
const { throwNotImplemented } = require("internal/shared");
const cppCreateHistogram = $newCppFunction("JSNodePerformanceHooksHistogram.cpp", "jsFunction_createHistogram", 3) as (
min: number,
max: number,
figures: number,
) => import("node:perf_hooks").RecordableHistogram;
var {
Performance,
PerformanceEntry,
PerformanceMark,
PerformanceMeasure,
PerformanceObserver,
PerformanceObserverEntryList,
} = globalThis;
var constants = {
NODE_PERFORMANCE_ENTRY_TYPE_DNS: 4,
NODE_PERFORMANCE_ENTRY_TYPE_GC: 0,
NODE_PERFORMANCE_ENTRY_TYPE_HTTP: 1,
NODE_PERFORMANCE_ENTRY_TYPE_HTTP2: 2,
NODE_PERFORMANCE_ENTRY_TYPE_NET: 3,
NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE: 16,
NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY: 32,
NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED: 2,
NODE_PERFORMANCE_GC_FLAGS_FORCED: 4,
NODE_PERFORMANCE_GC_FLAGS_NO: 0,
NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE: 64,
NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING: 8,
NODE_PERFORMANCE_GC_INCREMENTAL: 8,
NODE_PERFORMANCE_GC_MAJOR: 4,
NODE_PERFORMANCE_GC_MINOR: 1,
NODE_PERFORMANCE_GC_WEAKCB: 16,
NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE: 7,
NODE_PERFORMANCE_MILESTONE_ENVIRONMENT: 2,
NODE_PERFORMANCE_MILESTONE_LOOP_EXIT: 6,
NODE_PERFORMANCE_MILESTONE_LOOP_START: 5,
NODE_PERFORMANCE_MILESTONE_NODE_START: 3,
NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN_TIMESTAMP: 0,
NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN: 1,
NODE_PERFORMANCE_MILESTONE_V8_START: 4,
};
// PerformanceEntry is not a valid constructor, so we have to fake it.
class PerformanceNodeTiming {
bootstrapComplete: number = 0;
environment: number = 0;
idleTime: number = 0;
loopExit: number = 0;
loopStart: number = 0;
nodeStart: number = 0;
v8Start: number = 0;
// we have to fake the properties since it's not real
get name() {
return "node";
}
get entryType() {
return "node";
}
get startTime() {
return this.nodeStart;
}
get duration() {
return performance.now();
}
toJSON() {
return {
name: this.name,
entryType: this.entryType,
startTime: this.startTime,
duration: this.duration,
bootstrapComplete: this.bootstrapComplete,
environment: this.environment,
idleTime: this.idleTime,
loopExit: this.loopExit,
loopStart: this.loopStart,
nodeStart: this.nodeStart,
v8Start: this.v8Start,
};
}
}
$toClass(PerformanceNodeTiming, "PerformanceNodeTiming", PerformanceEntry);
function createPerformanceNodeTiming() {
const object = Object.create(PerformanceNodeTiming.prototype);
object.bootstrapComplete = object.environment = object.nodeStart = object.v8Start = performance.timeOrigin;
object.loopStart = object.idleTime = 1;
object.loopExit = -1;
return object;
}
function eventLoopUtilization(_utilization1, _utilization2) {
return {
idle: 0,
active: 0,
utilization: 0,
};
}
// PerformanceEntry is not a valid constructor, so we have to fake it.
class PerformanceResourceTiming {
constructor() {
throwNotImplemented("PerformanceResourceTiming");
}
}
$toClass(PerformanceResourceTiming, "PerformanceResourceTiming", PerformanceEntry);
export default {
performance: {
mark(_) {
return performance.mark(...arguments);
},
measure(_) {
return performance.measure(...arguments);
},
clearMarks(_) {
return performance.clearMarks(...arguments);
},
clearMeasures(_) {
return performance.clearMeasures(...arguments);
},
getEntries(_) {
return performance.getEntries(...arguments);
},
getEntriesByName(_) {
return performance.getEntriesByName(...arguments);
},
getEntriesByType(_) {
return performance.getEntriesByType(...arguments);
},
setResourceTimingBufferSize(_) {
return performance.setResourceTimingBufferSize(...arguments);
},
timeOrigin: performance.timeOrigin,
toJSON(_) {
return performance.toJSON(...arguments);
},
onresourcetimingbufferfull: performance.onresourcetimingbufferfull,
nodeTiming: createPerformanceNodeTiming(),
now: () => performance.now(),
eventLoopUtilization: eventLoopUtilization,
clearResourceTimings: function () {},
},
// performance: {
// clearMarks: [Function: clearMarks],
// clearMeasures: [Function: clearMeasures],
// clearResourceTimings: [Function: clearResourceTimings],
// getEntries: [Function: getEntries],
// getEntriesByName: [Function: getEntriesByName],
// getEntriesByType: [Function: getEntriesByType],
// mark: [Function: mark],
// measure: [Function: measure],
// now: performance.now,
// setResourceTimingBufferSize: [Function: setResourceTimingBufferSize],
// timeOrigin: performance.timeOrigin,
// toJSON: [Function: toJSON],
// onresourcetimingbufferfull: [Getter/Setter]
// },
constants,
Performance,
PerformanceEntry,
PerformanceMark,
PerformanceMeasure,
PerformanceObserver,
PerformanceObserverEntryList,
PerformanceNodeTiming,
monitorEventLoopDelay: function monitorEventLoopDelay(options?: { resolution?: number }) {
const impl = require("internal/perf_hooks/monitorEventLoopDelay");
return impl(options);
},
createHistogram: function createHistogram(options?: {
lowest?: number | bigint;
highest?: number | bigint;
figures?: number;
}): import("node:perf_hooks").RecordableHistogram {
const opts = options || {};
let lowest = 1;
let highest = Number.MAX_SAFE_INTEGER;
let figures = 3;
if (opts.lowest !== undefined) {
if (typeof opts.lowest === "bigint") {
lowest = Number(opts.lowest);
} else if (typeof opts.lowest === "number") {
lowest = opts.lowest;
} else {
throw $ERR_INVALID_ARG_TYPE("options.lowest", ["number", "bigint"], opts.lowest);
}
}
if (opts.highest !== undefined) {
if (typeof opts.highest === "bigint") {
highest = Number(opts.highest);
} else if (typeof opts.highest === "number") {
highest = opts.highest;
} else {
throw $ERR_INVALID_ARG_TYPE("options.highest", ["number", "bigint"], opts.highest);
}
}
if (opts.figures !== undefined) {
if (typeof opts.figures !== "number") {
throw $ERR_INVALID_ARG_TYPE("options.figures", "number", opts.figures);
}
if (opts.figures < 1 || opts.figures > 5) {
throw $ERR_OUT_OF_RANGE("options.figures", ">= 1 && <= 5", opts.figures);
}
figures = opts.figures;
}
// Node.js validation - highest must be >= 2 * lowest
if (lowest < 1) {
throw $ERR_OUT_OF_RANGE("options.lowest", ">= 1 && <= 9007199254740991", lowest);
}
if (highest < 2 * lowest) {
throw $ERR_OUT_OF_RANGE("options.highest", `>= ${2 * lowest} && <= 9007199254740991`, highest);
}
return cppCreateHistogram(lowest, highest, figures);
},
PerformanceResourceTiming,
};