mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 20:39:05 +00:00
## 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>
233 lines
6.9 KiB
TypeScript
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,
|
|
};
|