Files
bun.sh/src/bun.js/api/Timer/EventLoopDelayMonitor.zig
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

84 lines
2.6 KiB
Zig

const EventLoopDelayMonitor = @This();
/// We currently only globally share the same instance, which is kept alive by
/// the existence of the src/js/internal/perf_hooks/monitorEventLoopDelay.ts
/// function's scope.
///
/// I don't think having a single event loop delay monitor histogram instance
/// /will cause any issues? Let's find out.
js_histogram: jsc.JSValue = jsc.JSValue.zero,
event_loop_timer: jsc.API.Timer.EventLoopTimer = .{
.next = .epoch,
.tag = .EventLoopDelayMonitor,
},
resolution_ms: i32 = 10,
last_fire_ns: u64 = 0,
enabled: bool = false,
pub fn enable(this: *EventLoopDelayMonitor, vm: *VirtualMachine, histogram: jsc.JSValue, resolution_ms: i32) void {
if (this.enabled) return;
this.js_histogram = histogram;
this.resolution_ms = resolution_ms;
this.enabled = true;
// Schedule timer
const now = bun.timespec.now();
this.event_loop_timer.next = now.addMs(@intCast(resolution_ms));
vm.timer.insert(&this.event_loop_timer);
}
pub fn disable(this: *EventLoopDelayMonitor, vm: *VirtualMachine) void {
if (!this.enabled) return;
this.enabled = false;
this.js_histogram = jsc.JSValue.zero;
this.last_fire_ns = 0;
vm.timer.remove(&this.event_loop_timer);
}
pub fn isEnabled(this: *const EventLoopDelayMonitor) bool {
return this.enabled and this.js_histogram != jsc.JSValue.zero;
}
pub fn onFire(this: *EventLoopDelayMonitor, vm: *VirtualMachine, now: *const bun.timespec) void {
if (!this.enabled or this.js_histogram == jsc.JSValue.zero) {
return;
}
const now_ns = now.ns();
if (this.last_fire_ns > 0) {
const expected_ns = @as(u64, @intCast(this.resolution_ms)) *| 1_000_000;
const actual_ns = now_ns - this.last_fire_ns;
if (actual_ns > expected_ns) {
const delay_ns = @as(i64, @intCast(actual_ns -| expected_ns));
JSNodePerformanceHooksHistogram_recordDelay(this.js_histogram, delay_ns);
}
}
this.last_fire_ns = now_ns;
// Reschedule
this.event_loop_timer.next = now.addMs(@intCast(this.resolution_ms));
vm.timer.insert(&this.event_loop_timer);
}
// Record delay to histogram
extern fn JSNodePerformanceHooksHistogram_recordDelay(histogram: jsc.JSValue, delay_ns: i64) void;
// Export functions for C++
export fn Timer_enableEventLoopDelayMonitoring(vm: *VirtualMachine, histogram: jsc.JSValue, resolution_ms: i32) void {
vm.timer.event_loop_delay.enable(vm, histogram, resolution_ms);
}
export fn Timer_disableEventLoopDelayMonitoring(vm: *VirtualMachine) void {
vm.timer.event_loop_delay.disable(vm);
}
const bun = @import("bun");
const jsc = bun.jsc;
const VirtualMachine = jsc.VirtualMachine;