From bdb5fc96979ec328a68240962e4e7cb35b104dd0 Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Fri, 24 Apr 2026 14:28:07 -0400 Subject: [PATCH 1/9] perf_hooks: sample delay per event loop iteration Add a samplePerIteration option to monitorEventLoopDelay that records event loop delay from libuv event loop iterations instead of the timer interval sampler. The default remains interval-based; existing uses of monitorEventLoopDelay() keep behaving the same unless the samplePerIteration option is passed through. Signed-off-by: Pablo Erhard --- doc/api/perf_hooks.md | 25 +-- lib/internal/perf/event_loop_delay.js | 7 +- src/env_properties.h | 1 + src/histogram.cc | 176 ++++++++++++++++++ src/histogram.h | 63 +++++++ src/node_perf.cc | 7 + .../test-performance-eventloopdelay.js | 22 +++ 7 files changed, 288 insertions(+), 13 deletions(-) diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index fbd1d2e08a8c30..4e8ca260cbc1b4 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1709,8 +1709,11 @@ added: v11.10.0 --> * `options` {Object} - * `resolution` {number} The sampling rate in milliseconds. Must be greater - than zero. **Default:** `10`. + * `samplePerIteration` {boolean} When `true`, samples are taken once per + event loop iteration. **Default:** `false`. + * `resolution` {number} The sampling rate in milliseconds for interval-based + sampling. Must be greater than zero. This option is ignored when + `samplePerIteration` is `true`. **Default:** `10`. * Returns: {IntervalHistogram} _This property is an extension by Node.js. It is not available in Web browsers._ @@ -1718,11 +1721,11 @@ _This property is an extension by Node.js. It is not available in Web browsers._ Creates an `IntervalHistogram` object that samples and reports the event loop delay over time. The delays will be reported in nanoseconds. -Using a timer to detect approximate event loop delay works because the -execution of timers is tied specifically to the lifecycle of the libuv -event loop. That is, a delay in the loop will cause a delay in the execution -of the timer, and those delays are specifically what this API is intended to -detect. +By default, the histogram is updated by a timer using the configured +`resolution`. When `samplePerIteration` is `true`, samples are taken once per +event loop iteration using `uv_prepare_t` and `uv_check_t` hooks. In that mode, +the histogram does not keep the loop alive or force additional iterations when +the application is idle. ```mjs import { monitorEventLoopDelay } from 'node:perf_hooks'; @@ -2001,7 +2004,7 @@ The standard deviation of the recorded event loop delays. ## Class: `IntervalHistogram extends Histogram` -A `Histogram` that is periodically updated on a given interval. +A `Histogram` that records event loop delay. ### `histogram.disable()` @@ -2011,7 +2014,7 @@ added: v11.10.0 * Returns: {boolean} -Disables the update interval timer. Returns `true` if the timer was +Disables event loop delay sampling. Returns `true` if sampling was stopped, `false` if it was already stopped. ### `histogram.enable()` @@ -2022,7 +2025,7 @@ added: v11.10.0 * Returns: {boolean} -Enables the update interval timer. Returns `true` if the timer was +Enables event loop delay sampling. Returns `true` if sampling was started, `false` if it was already started. ### `histogram[Symbol.dispose]()` @@ -2031,7 +2034,7 @@ started, `false` if it was already started. added: v24.2.0 --> -Disables the update interval timer when the histogram is disposed. +Disables event loop delay sampling when the histogram is disposed. ```js const { monitorEventLoopDelay } = require('node:perf_hooks'); diff --git a/lib/internal/perf/event_loop_delay.js b/lib/internal/perf/event_loop_delay.js index 17581b1310c5c0..ebf0017b70df85 100644 --- a/lib/internal/perf/event_loop_delay.js +++ b/lib/internal/perf/event_loop_delay.js @@ -18,6 +18,7 @@ const { } = internalBinding('performance'); const { + validateBoolean, validateInteger, validateObject, } = require('internal/validators'); @@ -74,6 +75,7 @@ class ELDHistogram extends Histogram { /** * @param {{ + * samplePerIteration : boolean, * resolution : number * }} [options] * @returns {ELDHistogram} @@ -81,14 +83,15 @@ class ELDHistogram extends Histogram { function monitorEventLoopDelay(options = kEmptyObject) { validateObject(options, 'options'); - const { resolution = 10 } = options; + const { samplePerIteration = false, resolution = 10 } = options; + validateBoolean(samplePerIteration, 'options.samplePerIteration'); validateInteger(resolution, 'options.resolution', 1); return ReflectConstruct( function() { markTransferMode(this, true, false); this[kEnabled] = false; - this[kHandle] = createELDHistogram(resolution); + this[kHandle] = createELDHistogram(resolution, samplePerIteration); this[kMap] = new SafeMap(); }, [], ELDHistogram); } diff --git a/src/env_properties.h b/src/env_properties.h index 3c07d4a0f19e49..60179f1ffe476a 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -434,6 +434,7 @@ V(http2ping_constructor_template, v8::ObjectTemplate) \ V(i18n_converter_template, v8::ObjectTemplate) \ V(intervalhistogram_constructor_template, v8::FunctionTemplate) \ + V(eldhistogram_constructor_template, v8::FunctionTemplate) \ V(iter_template, v8::DictionaryTemplate) \ V(js_transferable_constructor_template, v8::FunctionTemplate) \ V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \ diff --git a/src/histogram.cc b/src/histogram.cc index 8752a419ec4030..e6acfdbd967b41 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -68,6 +68,10 @@ CFunction IntervalHistogram::fast_start_( CFunction::Make(&IntervalHistogram::FastStart)); CFunction IntervalHistogram::fast_stop_( CFunction::Make(&IntervalHistogram::FastStop)); +CFunction ELDHistogram::fast_start_( + CFunction::Make(&ELDHistogram::FastStart)); +CFunction ELDHistogram::fast_stop_( + CFunction::Make(&ELDHistogram::FastStop)); void HistogramImpl::AddMethods(Isolate* isolate, Local tmpl) { // TODO(@jasnell): The bigint API variations do not yet support fast @@ -444,6 +448,173 @@ void IntervalHistogram::FastStop(Local receiver) { histogram->OnStop(); } +Local ELDHistogram::GetConstructorTemplate( + Environment* env) { + Local tmpl = env->eldhistogram_constructor_template(); + if (tmpl.IsEmpty()) { + Isolate* isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, nullptr); + tmpl->Inherit(HandleWrap::GetConstructorTemplate(env)); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram")); + auto instance = tmpl->InstanceTemplate(); + instance->SetInternalFieldCount(ELDHistogram::kInternalFieldCount); + HistogramImpl::AddMethods(isolate, tmpl); + SetFastMethod(isolate, instance, "start", Start, &fast_start_); + SetFastMethod(isolate, instance, "stop", Stop, &fast_stop_); + env->set_eldhistogram_constructor_template(tmpl); + } + return tmpl; +} + +void ELDHistogram::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(Start); + registry->Register(Stop); + registry->Register(fast_start_); + registry->Register(fast_stop_); + HistogramImpl::RegisterExternalReferences(registry); +} + +ELDHistogram::ELDHistogram( + Environment* env, + Local wrap, + AsyncWrap::ProviderType type, + const Histogram::Options& options) + : HandleWrap( + env, + wrap, + reinterpret_cast(&check_handle_), + type), + HistogramImpl(options) { + MakeWeak(); + wrap->SetAlignedPointerInInternalField( + HistogramImpl::InternalFields::kImplField, + static_cast(this), + EmbedderDataTag::kDefault); + uv_check_init(env->event_loop(), &check_handle_); + uv_prepare_init(env->event_loop(), &prepare_handle_); + uv_unref(reinterpret_cast(&check_handle_)); + uv_unref(reinterpret_cast(&prepare_handle_)); + prepare_handle_.data = this; +} + +BaseObjectPtr ELDHistogram::Create( + Environment* env, + const Histogram::Options& options) { + Local obj; + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()).ToLocal(&obj)) { + return nullptr; + } + + return MakeBaseObject( + env, + obj, + AsyncWrap::PROVIDER_ELDHISTOGRAM, + options); +} + +void ELDHistogram::PrepareCB(uv_prepare_t* handle) { + ELDHistogram* self = static_cast(handle->data); + if (!self->enabled_) return; + self->prepare_time_ = uv_hrtime(); + self->timeout_ = uv_backend_timeout(handle->loop); +} + +void ELDHistogram::CheckCB(uv_check_t* handle) { + ELDHistogram* self = + ContainerOf(&ELDHistogram::check_handle_, handle); + if (!self->enabled_) return; + + uint64_t check_time = uv_hrtime(); + uint64_t poll_time = check_time - self->prepare_time_; + uint64_t latency = self->prepare_time_ - self->check_time_; + + if (self->timeout_ >= 0) { + uint64_t timeout_ns = static_cast(self->timeout_) * 1000 * 1000; + if (poll_time > timeout_ns) { + latency += poll_time - timeout_ns; + } + } + + self->histogram()->Record(latency == 0 ? 1 : latency); + self->check_time_ = check_time; +} + +void ELDHistogram::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("histogram", histogram()); +} + +void ELDHistogram::OnStart(StartFlags flags) { + if (enabled_ || IsHandleClosing()) return; + enabled_ = true; + if (flags == StartFlags::RESET) + histogram()->Reset(); + check_time_ = uv_hrtime(); + prepare_time_ = check_time_; + timeout_ = 0; + uv_check_start(&check_handle_, CheckCB); + uv_prepare_start(&prepare_handle_, PrepareCB); + uv_unref(reinterpret_cast(&check_handle_)); + uv_unref(reinterpret_cast(&prepare_handle_)); +} + +void ELDHistogram::OnStop() { + if (!enabled_ || IsHandleClosing()) return; + enabled_ = false; + uv_check_stop(&check_handle_); + uv_prepare_stop(&prepare_handle_); +} + +void ELDHistogram::PrepareCloseCB(uv_handle_t* handle) { + ELDHistogram* self = static_cast(handle->data); + uv_close(reinterpret_cast(&self->check_handle_), + HandleWrap::OnClose); +} + +void ELDHistogram::Close(Local close_callback) { + if (IsHandleClosing()) return; + OnStop(); + state_ = kClosing; + + if (!close_callback.IsEmpty() && close_callback->IsFunction() && + !persistent().IsEmpty()) { + object()->Set(env()->context(), + env()->handle_onclose_symbol(), + close_callback).Check(); + } + + uv_close(reinterpret_cast(&prepare_handle_), + PrepareCloseCB); +} + +void ELDHistogram::Start(const FunctionCallbackInfo& args) { + ELDHistogram* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); + histogram->OnStart(args[0]->IsTrue() ? StartFlags::RESET : StartFlags::NONE); +} + +void ELDHistogram::FastStart(Local receiver, bool reset) { + TRACK_V8_FAST_API_CALL("histogram.start"); + ELDHistogram* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); + histogram->OnStart(reset ? StartFlags::RESET : StartFlags::NONE); +} + +void ELDHistogram::Stop(const FunctionCallbackInfo& args) { + ELDHistogram* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); + histogram->OnStop(); +} + +void ELDHistogram::FastStop(Local receiver) { + TRACK_V8_FAST_API_CALL("histogram.stop"); + ELDHistogram* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); + histogram->OnStop(); +} + void HistogramImpl::GetCount(const FunctionCallbackInfo& args) { HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); double value = static_cast((*histogram)->Count()); @@ -607,6 +778,11 @@ HistogramImpl* HistogramImpl::FromJSObject(Local value) { HistogramImpl::kImplField, EmbedderDataTag::kDefault)); } +std::unique_ptr +ELDHistogram::CloneForMessaging() const { + return std::make_unique(histogram()); +} + std::unique_ptr IntervalHistogram::CloneForMessaging() const { return std::make_unique(histogram()); diff --git a/src/histogram.h b/src/histogram.h index 31c6564b9b1f12..9952c9b648ec72 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -266,6 +266,69 @@ class IntervalHistogram final : public HandleWrap, public HistogramImpl { static v8::CFunction fast_stop_; }; +class ELDHistogram final : public HandleWrap, public HistogramImpl { + public: + enum InternalFields { + kInternalFieldCount = std::max( + HandleWrap::kInternalFieldCount, HistogramImpl::kInternalFieldCount), + }; + + enum class StartFlags { + NONE, + RESET + }; + + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + static v8::Local GetConstructorTemplate( + Environment* env); + + static BaseObjectPtr Create( + Environment* env, + const Histogram::Options& options); + + ELDHistogram( + Environment* env, + v8::Local wrap, + AsyncWrap::ProviderType type, + const Histogram::Options& options = Histogram::Options {}); + + static void Start(const v8::FunctionCallbackInfo& args); + static void Stop(const v8::FunctionCallbackInfo& args); + + static void FastStart(v8::Local receiver, bool reset); + static void FastStop(v8::Local receiver); + + BaseObject::TransferMode GetTransferMode() const override { + return TransferMode::kCloneable; + } + std::unique_ptr CloneForMessaging() const override; + + void Close(v8::Local close_callback = + v8::Local()) override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(ELDHistogram) + SET_SELF_SIZE(ELDHistogram) + + private: + static void PrepareCB(uv_prepare_t* handle); + static void CheckCB(uv_check_t* handle); + static void PrepareCloseCB(uv_handle_t* handle); + void OnStart(StartFlags flags = StartFlags::RESET); + void OnStop(); + + bool enabled_ = false; + uv_prepare_t prepare_handle_; + uv_check_t check_handle_; + uint64_t prepare_time_ = 0; + uint64_t check_time_ = 0; + int64_t timeout_ = 0; + + static v8::CFunction fast_start_; + static v8::CFunction fast_stop_; +}; + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/node_perf.cc b/src/node_perf.cc index ca1b2eaf1c18b3..d1dca6d2242222 100644 --- a/src/node_perf.cc +++ b/src/node_perf.cc @@ -282,6 +282,12 @@ void CreateELDHistogram(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); int64_t interval = args[0].As()->Value(); CHECK_GT(interval, 0); + if (args[1]->IsTrue()) { + BaseObjectPtr histogram = + ELDHistogram::Create(env, Histogram::Options { 1 }); + args.GetReturnValue().Set(histogram->object()); + return; + } BaseObjectPtr histogram = IntervalHistogram::Create(env, interval, [](Histogram& histogram) { uint64_t delta = histogram.RecordDelta(); @@ -414,6 +420,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(fast_performance_now); HistogramBase::RegisterExternalReferences(registry); IntervalHistogram::RegisterExternalReferences(registry); + ELDHistogram::RegisterExternalReferences(registry); } } // namespace performance } // namespace node diff --git a/test/sequential/test-performance-eventloopdelay.js b/test/sequential/test-performance-eventloopdelay.js index 66493318f5651d..c3a87ac53d3794 100644 --- a/test/sequential/test-performance-eventloopdelay.js +++ b/test/sequential/test-performance-eventloopdelay.js @@ -49,6 +49,16 @@ const { sleep } = require('internal/util'); } ); }); + + [null, 'a', 1, {}, []].forEach((i) => { + assert.throws( + () => monitorEventLoopDelay({ samplePerIteration: i }), + { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + } + ); + }); } { @@ -114,6 +124,18 @@ const { sleep } = require('internal/util'); spinAWhile(); } +{ + const histogram = monitorEventLoopDelay({ samplePerIteration: true }); + histogram.enable(); + setTimeout(common.mustCall(() => { + histogram.disable(); + assert(histogram.count > 0, + `Expected samples to be recorded, got count=${histogram.count}`); + assert(histogram.min > 0); + assert(histogram.max > 0); + }), common.platformTimeout(20)); +} + // Make sure that the histogram instances can be garbage-collected without // and not just implicitly destroyed when the Environment is torn down. process.on('exit', global.gc); From 630a62c10953da72051aaaab230025ff07a24adb Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Thu, 30 Apr 2026 13:54:56 -0400 Subject: [PATCH 2/9] perf_hooks: test event loop delay fast api callbacks Add test coverage for the per iteration event loop delay histogram start and stop callbacks Use disitinct debug tracking keys for the event loop delay histogram callbacks. --- src/histogram.cc | 4 +-- ...oks-monitor-event-loop-delay-fast-calls.js | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-perf-hooks-monitor-event-loop-delay-fast-calls.js diff --git a/src/histogram.cc b/src/histogram.cc index e6acfdbd967b41..6df2fc151f5c09 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -596,7 +596,7 @@ void ELDHistogram::Start(const FunctionCallbackInfo& args) { } void ELDHistogram::FastStart(Local receiver, bool reset) { - TRACK_V8_FAST_API_CALL("histogram.start"); + TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.start"); ELDHistogram* histogram; ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); histogram->OnStart(reset ? StartFlags::RESET : StartFlags::NONE); @@ -609,7 +609,7 @@ void ELDHistogram::Stop(const FunctionCallbackInfo& args) { } void ELDHistogram::FastStop(Local receiver) { - TRACK_V8_FAST_API_CALL("histogram.stop"); + TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.stop"); ELDHistogram* histogram; ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); histogram->OnStop(); diff --git a/test/parallel/test-perf-hooks-monitor-event-loop-delay-fast-calls.js b/test/parallel/test-perf-hooks-monitor-event-loop-delay-fast-calls.js new file mode 100644 index 00000000000000..1954f52e441557 --- /dev/null +++ b/test/parallel/test-perf-hooks-monitor-event-loop-delay-fast-calls.js @@ -0,0 +1,26 @@ +// Flags: --allow-natives-syntax --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const { internalBinding } = require('internal/test/binding'); +const { createELDHistogram } = internalBinding('performance'); + +const histogram = createELDHistogram(1, true); + +function testFastMethods() { + histogram.start(true); + histogram.stop(); +} + +eval('%PrepareFunctionForOptimization(testFastMethods)'); +testFastMethods(); +eval('%OptimizeFunctionOnNextCall(testFastMethods)'); +testFastMethods(); + +if (common.isDebug) { + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual(getV8FastApiCallCount('histogram.eventLoopDelay.start'), 1); + assert.strictEqual(getV8FastApiCallCount('histogram.eventLoopDelay.stop'), 1); +} From 6363bc857e3919a33e6dd33a48e6eada763cbb58 Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Thu, 30 Apr 2026 19:33:22 -0400 Subject: [PATCH 3/9] perf_hooks: format cpp --- src/histogram.cc | 54 ++++++++++++++++++------------------------------ src/histogram.h | 23 ++++++++------------- src/node_perf.cc | 2 +- 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/histogram.cc b/src/histogram.cc index 6df2fc151f5c09..77bb98a408da95 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -68,10 +68,8 @@ CFunction IntervalHistogram::fast_start_( CFunction::Make(&IntervalHistogram::FastStart)); CFunction IntervalHistogram::fast_stop_( CFunction::Make(&IntervalHistogram::FastStop)); -CFunction ELDHistogram::fast_start_( - CFunction::Make(&ELDHistogram::FastStart)); -CFunction ELDHistogram::fast_stop_( - CFunction::Make(&ELDHistogram::FastStop)); +CFunction ELDHistogram::fast_start_(CFunction::Make(&ELDHistogram::FastStart)); +CFunction ELDHistogram::fast_stop_(CFunction::Make(&ELDHistogram::FastStop)); void HistogramImpl::AddMethods(Isolate* isolate, Local tmpl) { // TODO(@jasnell): The bigint API variations do not yet support fast @@ -448,8 +446,7 @@ void IntervalHistogram::FastStop(Local receiver) { histogram->OnStop(); } -Local ELDHistogram::GetConstructorTemplate( - Environment* env) { +Local ELDHistogram::GetConstructorTemplate(Environment* env) { Local tmpl = env->eldhistogram_constructor_template(); if (tmpl.IsEmpty()) { Isolate* isolate = env->isolate(); @@ -475,16 +472,12 @@ void ELDHistogram::RegisterExternalReferences( HistogramImpl::RegisterExternalReferences(registry); } -ELDHistogram::ELDHistogram( - Environment* env, - Local wrap, - AsyncWrap::ProviderType type, - const Histogram::Options& options) +ELDHistogram::ELDHistogram(Environment* env, + Local wrap, + AsyncWrap::ProviderType type, + const Histogram::Options& options) : HandleWrap( - env, - wrap, - reinterpret_cast(&check_handle_), - type), + env, wrap, reinterpret_cast(&check_handle_), type), HistogramImpl(options) { MakeWeak(); wrap->SetAlignedPointerInInternalField( @@ -499,20 +492,17 @@ ELDHistogram::ELDHistogram( } BaseObjectPtr ELDHistogram::Create( - Environment* env, - const Histogram::Options& options) { + Environment* env, const Histogram::Options& options) { Local obj; if (!GetConstructorTemplate(env) - ->InstanceTemplate() - ->NewInstance(env->context()).ToLocal(&obj)) { + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { return nullptr; } return MakeBaseObject( - env, - obj, - AsyncWrap::PROVIDER_ELDHISTOGRAM, - options); + env, obj, AsyncWrap::PROVIDER_ELDHISTOGRAM, options); } void ELDHistogram::PrepareCB(uv_prepare_t* handle) { @@ -523,8 +513,7 @@ void ELDHistogram::PrepareCB(uv_prepare_t* handle) { } void ELDHistogram::CheckCB(uv_check_t* handle) { - ELDHistogram* self = - ContainerOf(&ELDHistogram::check_handle_, handle); + ELDHistogram* self = ContainerOf(&ELDHistogram::check_handle_, handle); if (!self->enabled_) return; uint64_t check_time = uv_hrtime(); @@ -549,8 +538,7 @@ void ELDHistogram::MemoryInfo(MemoryTracker* tracker) const { void ELDHistogram::OnStart(StartFlags flags) { if (enabled_ || IsHandleClosing()) return; enabled_ = true; - if (flags == StartFlags::RESET) - histogram()->Reset(); + if (flags == StartFlags::RESET) histogram()->Reset(); check_time_ = uv_hrtime(); prepare_time_ = check_time_; timeout_ = 0; @@ -580,13 +568,12 @@ void ELDHistogram::Close(Local close_callback) { if (!close_callback.IsEmpty() && close_callback->IsFunction() && !persistent().IsEmpty()) { - object()->Set(env()->context(), - env()->handle_onclose_symbol(), - close_callback).Check(); + object() + ->Set(env()->context(), env()->handle_onclose_symbol(), close_callback) + .Check(); } - uv_close(reinterpret_cast(&prepare_handle_), - PrepareCloseCB); + uv_close(reinterpret_cast(&prepare_handle_), PrepareCloseCB); } void ELDHistogram::Start(const FunctionCallbackInfo& args) { @@ -778,8 +765,7 @@ HistogramImpl* HistogramImpl::FromJSObject(Local value) { HistogramImpl::kImplField, EmbedderDataTag::kDefault)); } -std::unique_ptr -ELDHistogram::CloneForMessaging() const { +std::unique_ptr ELDHistogram::CloneForMessaging() const { return std::make_unique(histogram()); } diff --git a/src/histogram.h b/src/histogram.h index 9952c9b648ec72..0fec31d447b631 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -273,25 +273,20 @@ class ELDHistogram final : public HandleWrap, public HistogramImpl { HandleWrap::kInternalFieldCount, HistogramImpl::kInternalFieldCount), }; - enum class StartFlags { - NONE, - RESET - }; + enum class StartFlags { NONE, RESET }; static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static v8::Local GetConstructorTemplate( Environment* env); - static BaseObjectPtr Create( - Environment* env, - const Histogram::Options& options); + static BaseObjectPtr Create(Environment* env, + const Histogram::Options& options); - ELDHistogram( - Environment* env, - v8::Local wrap, - AsyncWrap::ProviderType type, - const Histogram::Options& options = Histogram::Options {}); + ELDHistogram(Environment* env, + v8::Local wrap, + AsyncWrap::ProviderType type, + const Histogram::Options& options = Histogram::Options{}); static void Start(const v8::FunctionCallbackInfo& args); static void Stop(const v8::FunctionCallbackInfo& args); @@ -304,8 +299,8 @@ class ELDHistogram final : public HandleWrap, public HistogramImpl { } std::unique_ptr CloneForMessaging() const override; - void Close(v8::Local close_callback = - v8::Local()) override; + void Close( + v8::Local close_callback = v8::Local()) override; void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(ELDHistogram) diff --git a/src/node_perf.cc b/src/node_perf.cc index d1dca6d2242222..269f54e0dd29ad 100644 --- a/src/node_perf.cc +++ b/src/node_perf.cc @@ -284,7 +284,7 @@ void CreateELDHistogram(const FunctionCallbackInfo& args) { CHECK_GT(interval, 0); if (args[1]->IsTrue()) { BaseObjectPtr histogram = - ELDHistogram::Create(env, Histogram::Options { 1 }); + ELDHistogram::Create(env, Histogram::Options{1}); args.GetReturnValue().Set(histogram->object()); return; } From c50b0fb84039ce7db11e3e1fbf76dbe647554ace Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Tue, 19 May 2026 17:20:45 -0400 Subject: [PATCH 4/9] perf_hooks: use super Close method Use the super Close method instead of duplicating code --- src/histogram.cc | 18 ++---------------- src/histogram.h | 1 - 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/histogram.cc b/src/histogram.cc index 77bb98a408da95..9bac52b3be286f 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -555,25 +555,11 @@ void ELDHistogram::OnStop() { uv_prepare_stop(&prepare_handle_); } -void ELDHistogram::PrepareCloseCB(uv_handle_t* handle) { - ELDHistogram* self = static_cast(handle->data); - uv_close(reinterpret_cast(&self->check_handle_), - HandleWrap::OnClose); -} - void ELDHistogram::Close(Local close_callback) { if (IsHandleClosing()) return; OnStop(); - state_ = kClosing; - - if (!close_callback.IsEmpty() && close_callback->IsFunction() && - !persistent().IsEmpty()) { - object() - ->Set(env()->context(), env()->handle_onclose_symbol(), close_callback) - .Check(); - } - - uv_close(reinterpret_cast(&prepare_handle_), PrepareCloseCB); + HandleWrap::Close(close_callback); + uv_close(reinterpret_cast(&prepare_handle_), nullptr); } void ELDHistogram::Start(const FunctionCallbackInfo& args) { diff --git a/src/histogram.h b/src/histogram.h index 0fec31d447b631..836b784b202b3e 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -309,7 +309,6 @@ class ELDHistogram final : public HandleWrap, public HistogramImpl { private: static void PrepareCB(uv_prepare_t* handle); static void CheckCB(uv_check_t* handle); - static void PrepareCloseCB(uv_handle_t* handle); void OnStart(StartFlags flags = StartFlags::RESET); void OnStop(); From 164ea8a8cb40d0172c3ae24ec1fce8774460ae89 Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Wed, 27 May 2026 15:38:42 -0400 Subject: [PATCH 5/9] test: improve event loop delay sample-per-iteration coverage Added extra test to add coverage of the overall functionality of the new ELD histogram. --- .../test-performance-eventloopdelay.js | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/test/sequential/test-performance-eventloopdelay.js b/test/sequential/test-performance-eventloopdelay.js index c3a87ac53d3794..76fe1ef9a107a8 100644 --- a/test/sequential/test-performance-eventloopdelay.js +++ b/test/sequential/test-performance-eventloopdelay.js @@ -133,9 +133,92 @@ const { sleep } = require('internal/util'); `Expected samples to be recorded, got count=${histogram.count}`); assert(histogram.min > 0); assert(histogram.max > 0); + assert(histogram.mean > 0); + assert(histogram.percentiles.size > 0); + for (let n = 1; n < 100; n = n + 10) { + assert(histogram.percentile(n) >= 0); + } + // reset() should restore the histogram to its initial state + histogram.reset(); + assert.strictEqual(histogram.count, 0); + assert.strictEqual(histogram.max, 0); + assert.strictEqual(histogram.min, 9223372036854776000); + assert(Number.isNaN(histogram.mean)); + assert(Number.isNaN(histogram.stddev)); + assert.strictEqual(histogram.percentiles.size, 1); + }), common.platformTimeout(20)); +} + +{ + // enable()/disable() return values for ELDHistogram (samplePerIteration: true) + const histogram = monitorEventLoopDelay({ samplePerIteration: true }); + assert.strictEqual(histogram.enable(), true); + assert.strictEqual(histogram.enable(), false); // already enabled, no-op + assert.strictEqual(histogram.disable(), true); + assert.strictEqual(histogram.disable(), false); // already disabled, no-op + // Re-enabling after disable should work + assert.strictEqual(histogram.enable(), true); + setTimeout(common.mustCall(() => { + histogram.disable(); + assert(histogram.count > 0, + `Expected samples after re-enable, got count=${histogram.count}`); }), common.platformTimeout(20)); } +{ + // Verify that samplePerIteration records exactly one sample per event loop iteration. + const N = 10; + const histogram = monitorEventLoopDelay({ samplePerIteration: true }); + histogram.enable(); + + let iterations = 0; + const verify = common.mustCall(() => { + histogram.disable(); + assert( + histogram.count >= N - 1, + `Expected at least ${N - 1} samples for ${N} iterations, got ${histogram.count}` + ); + }); + + function tick() { + if (++iterations < N) { + setImmediate(tick); + } else { + verify(); + } + } + setImmediate(tick); +} + +{ + // samplePerIteration should sample per event loop iteration, independent of + // the timer resolution used by the legacy monitorEventLoopDelay path. + const N = 10; + const histogram = monitorEventLoopDelay({ + samplePerIteration: true, + resolution: 60 * 1000, + }); + histogram.enable(); + + let iterations = 0; + const verify = common.mustCall(() => { + histogram.disable(); + assert( + histogram.count >= N - 1, + `Expected samples despite large resolution, got count=${histogram.count}` + ); + }); + + function tick() { + if (++iterations < N) { + setImmediate(tick); + } else { + verify(); + } + } + setImmediate(tick); +} + // Make sure that the histogram instances can be garbage-collected without // and not just implicitly destroyed when the Environment is torn down. process.on('exit', global.gc); From 26b7684c90d0aedf56a1042d9e2e159034f117f2 Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Wed, 27 May 2026 15:48:50 -0400 Subject: [PATCH 6/9] doc: update perf_hooks histogram docs Update docs to match the event loop delay sampling changes. --- doc/api/perf_hooks.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 4e8ca260cbc1b4..8529049a78661f 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1706,6 +1706,10 @@ are not guaranteed to reflect any correct state of the event loop. * `options` {Object} @@ -1714,18 +1718,20 @@ added: v11.10.0 * `resolution` {number} The sampling rate in milliseconds for interval-based sampling. Must be greater than zero. This option is ignored when `samplePerIteration` is `true`. **Default:** `10`. -* Returns: {IntervalHistogram} +* Returns: {IntervalHistogram|ELDHistogram} _This property is an extension by Node.js. It is not available in Web browsers._ -Creates an `IntervalHistogram` object that samples and reports the event loop -delay over time. The delays will be reported in nanoseconds. +Creates a histogram object that samples and reports the event loop delay over +time. The delays will be reported in nanoseconds. By default, the histogram is updated by a timer using the configured `resolution`. When `samplePerIteration` is `true`, samples are taken once per event loop iteration using `uv_prepare_t` and `uv_check_t` hooks. In that mode, the histogram does not keep the loop alive or force additional iterations when the application is idle. +The two sampling modes produce significantly different results and should not +be compared directly. ```mjs import { monitorEventLoopDelay } from 'node:perf_hooks'; @@ -2004,7 +2010,7 @@ The standard deviation of the recorded event loop delays. ## Class: `IntervalHistogram extends Histogram` -A `Histogram` that records event loop delay. +A `Histogram` that records event loop delay using interval-based sampling. ### `histogram.disable()` @@ -2045,11 +2051,17 @@ const { monitorEventLoopDelay } = require('node:perf_hooks'); } ``` -### Cloning an `IntervalHistogram` +### Cloning event loop delay histograms + +{IntervalHistogram} and {ELDHistogram} instances can be cloned via +{MessagePort}. On the receiving end, the histogram is cloned as a plain +{Histogram} object that does not implement the `enable()` and `disable()` +methods. + +## Class: `ELDHistogram extends Histogram` -{IntervalHistogram} instances can be cloned via {MessagePort}. On the receiving -end, the histogram is cloned as a plain {Histogram} object that does not -implement the `enable()` and `disable()` methods. +A `Histogram` that records event loop delay once per event loop iteration. It +provides the same API as {IntervalHistogram}. ## Class: `RecordableHistogram extends Histogram` From 6e6b503fb55048b9962c1d28d105b4742d52c65e Mon Sep 17 00:00:00 2001 From: Pablo Erhard Date: Wed, 17 Jun 2026 12:54:37 -0400 Subject: [PATCH 7/9] perf_hooks: rename internal ELDHistogram to IterationHistogram The C++ class `ELDHistogram` is the per-iteration sampling implementation that sits next to `IntervalHistogram` (the timer-based one). Its name collided with the JS exposed `ELDHistogram` class that `monitorEventLoopDelay()` returns regardless of sampling mode, making the layering confusing: "ELDHistogram" was both the generic JS concept and the name of one specific C++ backing. --- src/env_properties.h | 2 +- src/histogram.cc | 62 ++++++++++--------- src/histogram.h | 18 +++--- src/node_perf.cc | 6 +- .../test-performance-eventloopdelay.js | 4 +- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/env_properties.h b/src/env_properties.h index 60179f1ffe476a..77cb002c4c532e 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -434,7 +434,7 @@ V(http2ping_constructor_template, v8::ObjectTemplate) \ V(i18n_converter_template, v8::ObjectTemplate) \ V(intervalhistogram_constructor_template, v8::FunctionTemplate) \ - V(eldhistogram_constructor_template, v8::FunctionTemplate) \ + V(iterationhistogram_constructor_template, v8::FunctionTemplate) \ V(iter_template, v8::DictionaryTemplate) \ V(js_transferable_constructor_template, v8::FunctionTemplate) \ V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \ diff --git a/src/histogram.cc b/src/histogram.cc index 9bac52b3be286f..2e87cda581b4e3 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -68,8 +68,10 @@ CFunction IntervalHistogram::fast_start_( CFunction::Make(&IntervalHistogram::FastStart)); CFunction IntervalHistogram::fast_stop_( CFunction::Make(&IntervalHistogram::FastStop)); -CFunction ELDHistogram::fast_start_(CFunction::Make(&ELDHistogram::FastStart)); -CFunction ELDHistogram::fast_stop_(CFunction::Make(&ELDHistogram::FastStop)); +CFunction IterationHistogram::fast_start_( + CFunction::Make(&IterationHistogram::FastStart)); +CFunction IterationHistogram::fast_stop_( + CFunction::Make(&IterationHistogram::FastStop)); void HistogramImpl::AddMethods(Isolate* isolate, Local tmpl) { // TODO(@jasnell): The bigint API variations do not yet support fast @@ -446,24 +448,24 @@ void IntervalHistogram::FastStop(Local receiver) { histogram->OnStop(); } -Local ELDHistogram::GetConstructorTemplate(Environment* env) { - Local tmpl = env->eldhistogram_constructor_template(); +Local IterationHistogram::GetConstructorTemplate(Environment* env) { + Local tmpl = env->iterationhistogram_constructor_template(); if (tmpl.IsEmpty()) { Isolate* isolate = env->isolate(); tmpl = NewFunctionTemplate(isolate, nullptr); tmpl->Inherit(HandleWrap::GetConstructorTemplate(env)); tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram")); auto instance = tmpl->InstanceTemplate(); - instance->SetInternalFieldCount(ELDHistogram::kInternalFieldCount); + instance->SetInternalFieldCount(IterationHistogram::kInternalFieldCount); HistogramImpl::AddMethods(isolate, tmpl); SetFastMethod(isolate, instance, "start", Start, &fast_start_); SetFastMethod(isolate, instance, "stop", Stop, &fast_stop_); - env->set_eldhistogram_constructor_template(tmpl); + env->set_iterationhistogram_constructor_template(tmpl); } return tmpl; } -void ELDHistogram::RegisterExternalReferences( +void IterationHistogram::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(Start); registry->Register(Stop); @@ -472,10 +474,10 @@ void ELDHistogram::RegisterExternalReferences( HistogramImpl::RegisterExternalReferences(registry); } -ELDHistogram::ELDHistogram(Environment* env, - Local wrap, - AsyncWrap::ProviderType type, - const Histogram::Options& options) +IterationHistogram::IterationHistogram(Environment* env, + Local wrap, + AsyncWrap::ProviderType type, + const Histogram::Options& options) : HandleWrap( env, wrap, reinterpret_cast(&check_handle_), type), HistogramImpl(options) { @@ -491,7 +493,7 @@ ELDHistogram::ELDHistogram(Environment* env, prepare_handle_.data = this; } -BaseObjectPtr ELDHistogram::Create( +BaseObjectPtr IterationHistogram::Create( Environment* env, const Histogram::Options& options) { Local obj; if (!GetConstructorTemplate(env) @@ -501,19 +503,19 @@ BaseObjectPtr ELDHistogram::Create( return nullptr; } - return MakeBaseObject( + return MakeBaseObject( env, obj, AsyncWrap::PROVIDER_ELDHISTOGRAM, options); } -void ELDHistogram::PrepareCB(uv_prepare_t* handle) { - ELDHistogram* self = static_cast(handle->data); +void IterationHistogram::PrepareCB(uv_prepare_t* handle) { + IterationHistogram* self = static_cast(handle->data); if (!self->enabled_) return; self->prepare_time_ = uv_hrtime(); self->timeout_ = uv_backend_timeout(handle->loop); } -void ELDHistogram::CheckCB(uv_check_t* handle) { - ELDHistogram* self = ContainerOf(&ELDHistogram::check_handle_, handle); +void IterationHistogram::CheckCB(uv_check_t* handle) { + IterationHistogram* self = ContainerOf(&IterationHistogram::check_handle_, handle); if (!self->enabled_) return; uint64_t check_time = uv_hrtime(); @@ -531,11 +533,11 @@ void ELDHistogram::CheckCB(uv_check_t* handle) { self->check_time_ = check_time; } -void ELDHistogram::MemoryInfo(MemoryTracker* tracker) const { +void IterationHistogram::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("histogram", histogram()); } -void ELDHistogram::OnStart(StartFlags flags) { +void IterationHistogram::OnStart(StartFlags flags) { if (enabled_ || IsHandleClosing()) return; enabled_ = true; if (flags == StartFlags::RESET) histogram()->Reset(); @@ -548,42 +550,42 @@ void ELDHistogram::OnStart(StartFlags flags) { uv_unref(reinterpret_cast(&prepare_handle_)); } -void ELDHistogram::OnStop() { +void IterationHistogram::OnStop() { if (!enabled_ || IsHandleClosing()) return; enabled_ = false; uv_check_stop(&check_handle_); uv_prepare_stop(&prepare_handle_); } -void ELDHistogram::Close(Local close_callback) { +void IterationHistogram::Close(Local close_callback) { if (IsHandleClosing()) return; OnStop(); HandleWrap::Close(close_callback); uv_close(reinterpret_cast(&prepare_handle_), nullptr); } -void ELDHistogram::Start(const FunctionCallbackInfo& args) { - ELDHistogram* histogram; +void IterationHistogram::Start(const FunctionCallbackInfo& args) { + IterationHistogram* histogram; ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); histogram->OnStart(args[0]->IsTrue() ? StartFlags::RESET : StartFlags::NONE); } -void ELDHistogram::FastStart(Local receiver, bool reset) { +void IterationHistogram::FastStart(Local receiver, bool reset) { TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.start"); - ELDHistogram* histogram; + IterationHistogram* histogram; ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); histogram->OnStart(reset ? StartFlags::RESET : StartFlags::NONE); } -void ELDHistogram::Stop(const FunctionCallbackInfo& args) { - ELDHistogram* histogram; +void IterationHistogram::Stop(const FunctionCallbackInfo& args) { + IterationHistogram* histogram; ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); histogram->OnStop(); } -void ELDHistogram::FastStop(Local receiver) { +void IterationHistogram::FastStop(Local receiver) { TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.stop"); - ELDHistogram* histogram; + IterationHistogram* histogram; ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); histogram->OnStop(); } @@ -751,7 +753,7 @@ HistogramImpl* HistogramImpl::FromJSObject(Local value) { HistogramImpl::kImplField, EmbedderDataTag::kDefault)); } -std::unique_ptr ELDHistogram::CloneForMessaging() const { +std::unique_ptr IterationHistogram::CloneForMessaging() const { return std::make_unique(histogram()); } diff --git a/src/histogram.h b/src/histogram.h index 836b784b202b3e..c914bc34d08d63 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -266,7 +266,7 @@ class IntervalHistogram final : public HandleWrap, public HistogramImpl { static v8::CFunction fast_stop_; }; -class ELDHistogram final : public HandleWrap, public HistogramImpl { +class IterationHistogram final : public HandleWrap, public HistogramImpl { public: enum InternalFields { kInternalFieldCount = std::max( @@ -280,13 +280,13 @@ class ELDHistogram final : public HandleWrap, public HistogramImpl { static v8::Local GetConstructorTemplate( Environment* env); - static BaseObjectPtr Create(Environment* env, - const Histogram::Options& options); + static BaseObjectPtr Create( + Environment* env, const Histogram::Options& options); - ELDHistogram(Environment* env, - v8::Local wrap, - AsyncWrap::ProviderType type, - const Histogram::Options& options = Histogram::Options{}); + IterationHistogram(Environment* env, + v8::Local wrap, + AsyncWrap::ProviderType type, + const Histogram::Options& options = Histogram::Options{}); static void Start(const v8::FunctionCallbackInfo& args); static void Stop(const v8::FunctionCallbackInfo& args); @@ -303,8 +303,8 @@ class ELDHistogram final : public HandleWrap, public HistogramImpl { v8::Local close_callback = v8::Local()) override; void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(ELDHistogram) - SET_SELF_SIZE(ELDHistogram) + SET_MEMORY_INFO_NAME(IterationHistogram) + SET_SELF_SIZE(IterationHistogram) private: static void PrepareCB(uv_prepare_t* handle); diff --git a/src/node_perf.cc b/src/node_perf.cc index 269f54e0dd29ad..177c2a7898543b 100644 --- a/src/node_perf.cc +++ b/src/node_perf.cc @@ -283,8 +283,8 @@ void CreateELDHistogram(const FunctionCallbackInfo& args) { int64_t interval = args[0].As()->Value(); CHECK_GT(interval, 0); if (args[1]->IsTrue()) { - BaseObjectPtr histogram = - ELDHistogram::Create(env, Histogram::Options{1}); + BaseObjectPtr histogram = + IterationHistogram::Create(env, Histogram::Options{1}); args.GetReturnValue().Set(histogram->object()); return; } @@ -420,7 +420,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(fast_performance_now); HistogramBase::RegisterExternalReferences(registry); IntervalHistogram::RegisterExternalReferences(registry); - ELDHistogram::RegisterExternalReferences(registry); + IterationHistogram::RegisterExternalReferences(registry); } } // namespace performance } // namespace node diff --git a/test/sequential/test-performance-eventloopdelay.js b/test/sequential/test-performance-eventloopdelay.js index 76fe1ef9a107a8..ddd33372ec5e8e 100644 --- a/test/sequential/test-performance-eventloopdelay.js +++ b/test/sequential/test-performance-eventloopdelay.js @@ -153,9 +153,9 @@ const { sleep } = require('internal/util'); // enable()/disable() return values for ELDHistogram (samplePerIteration: true) const histogram = monitorEventLoopDelay({ samplePerIteration: true }); assert.strictEqual(histogram.enable(), true); - assert.strictEqual(histogram.enable(), false); // already enabled, no-op + assert.strictEqual(histogram.enable(), false); // Already enabled, no-op assert.strictEqual(histogram.disable(), true); - assert.strictEqual(histogram.disable(), false); // already disabled, no-op + assert.strictEqual(histogram.disable(), false); // Already disabled, no-op // Re-enabling after disable should work assert.strictEqual(histogram.enable(), true); setTimeout(common.mustCall(() => { From 2583bbf91be0f1d66e78e86b31e5557a8cc6088a Mon Sep 17 00:00:00 2001 From: Pablo Erhard Date: Wed, 17 Jun 2026 12:55:39 -0400 Subject: [PATCH 8/9] doc: align perf_hooks histogram class name with implementation align perf_hooks docs after renaming internal ELDHistogram to IterationHistogram --- doc/api/perf_hooks.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 8529049a78661f..60c42ab1a8b8ba 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1718,7 +1718,7 @@ changes: * `resolution` {number} The sampling rate in milliseconds for interval-based sampling. Must be greater than zero. This option is ignored when `samplePerIteration` is `true`. **Default:** `10`. -* Returns: {IntervalHistogram|ELDHistogram} +* Returns: {ELDHistogram} _This property is an extension by Node.js. It is not available in Web browsers._ @@ -2008,9 +2008,10 @@ added: v11.10.0 The standard deviation of the recorded event loop delays. -## Class: `IntervalHistogram extends Histogram` +## Class: `ELDHistogram extends Histogram` -A `Histogram` that records event loop delay using interval-based sampling. +A `Histogram` that records event loop delay, returned by +[`perf_hooks.monitorEventLoopDelay()`][]. ### `histogram.disable()` @@ -2051,17 +2052,11 @@ const { monitorEventLoopDelay } = require('node:perf_hooks'); } ``` -### Cloning event loop delay histograms - -{IntervalHistogram} and {ELDHistogram} instances can be cloned via -{MessagePort}. On the receiving end, the histogram is cloned as a plain -{Histogram} object that does not implement the `enable()` and `disable()` -methods. - -## Class: `ELDHistogram extends Histogram` +### Cloning an `ELDHistogram` -A `Histogram` that records event loop delay once per event loop iteration. It -provides the same API as {IntervalHistogram}. +{ELDHistogram} instances can be cloned via {MessagePort}. On the receiving end, +the histogram is cloned as a plain {Histogram} object that does not implement +the `enable()` and `disable()` methods. ## Class: `RecordableHistogram extends Histogram` @@ -2369,6 +2364,7 @@ dns.promises.resolve('localhost'); [`'exit'`]: process.md#event-exit [`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options [`perf_hooks.eventLoopUtilization()`]: #perf_hookseventlooputilizationutilization1-utilization2 +[`perf_hooks.monitorEventLoopDelay()`]: #perf_hooksmonitoreventloopdelayoptions [`perf_hooks.timerify()`]: #perf_hookstimerifyfn-options [`process.hrtime()`]: process.md#processhrtimetime [`timeOrigin`]: https://w3c.github.io/hr-time/#dom-performance-timeorigin From 3f20e89304dcd3dd340b8f41762232d46ef90c7e Mon Sep 17 00:00:00 2001 From: Pablo Erhard Date: Wed, 17 Jun 2026 13:43:59 -0400 Subject: [PATCH 9/9] perf_hooks: deduplicate histogram Start/Stop via shared templates Added shared templates for duplicated start/stop logic in IntevalHistogram and IterationHistogram. --- src/env_properties.h | 2 +- src/histogram.cc | 55 +++++++++++++++++++++--------------------- src/histogram.h | 15 ++++++++++++ src/node_task_queue.cc | 8 +++--- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/env_properties.h b/src/env_properties.h index 77cb002c4c532e..2e6bc65f89641c 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -434,7 +434,7 @@ V(http2ping_constructor_template, v8::ObjectTemplate) \ V(i18n_converter_template, v8::ObjectTemplate) \ V(intervalhistogram_constructor_template, v8::FunctionTemplate) \ - V(iterationhistogram_constructor_template, v8::FunctionTemplate) \ + V(iterationhistogram_constructor_template, v8::FunctionTemplate) \ V(iter_template, v8::DictionaryTemplate) \ V(js_transferable_constructor_template, v8::FunctionTemplate) \ V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \ diff --git a/src/histogram.cc b/src/histogram.cc index 2e87cda581b4e3..5dd82c305bf7ae 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -25,6 +25,20 @@ using v8::String; using v8::Uint32; using v8::Value; +template +void StartHandleHistogram(Local receiver, bool reset) { + T* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); + histogram->OnStart(reset ? T::StartFlags::RESET : T::StartFlags::NONE); +} + +template +void StopHandleHistogram(Local receiver) { + T* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); + histogram->OnStop(); +} + Histogram::Histogram(const Options& options) { hdr_histogram* histogram; CHECK_EQ(0, hdr_init(options.lowest, @@ -423,32 +437,25 @@ void IntervalHistogram::OnStop() { } void IntervalHistogram::Start(const FunctionCallbackInfo& args) { - IntervalHistogram* histogram; - ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); - histogram->OnStart(args[0]->IsTrue() ? StartFlags::RESET : StartFlags::NONE); + StartHandleHistogram(args.This(), args[0]->IsTrue()); } void IntervalHistogram::FastStart(Local receiver, bool reset) { TRACK_V8_FAST_API_CALL("histogram.start"); - IntervalHistogram* histogram; - ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); - histogram->OnStart(reset ? StartFlags::RESET : StartFlags::NONE); + StartHandleHistogram(receiver, reset); } void IntervalHistogram::Stop(const FunctionCallbackInfo& args) { - IntervalHistogram* histogram; - ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); - histogram->OnStop(); + StopHandleHistogram(args.This()); } void IntervalHistogram::FastStop(Local receiver) { TRACK_V8_FAST_API_CALL("histogram.stop"); - IntervalHistogram* histogram; - ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); - histogram->OnStop(); + StopHandleHistogram(receiver); } -Local IterationHistogram::GetConstructorTemplate(Environment* env) { +Local IterationHistogram::GetConstructorTemplate( + Environment* env) { Local tmpl = env->iterationhistogram_constructor_template(); if (tmpl.IsEmpty()) { Isolate* isolate = env->isolate(); @@ -515,7 +522,8 @@ void IterationHistogram::PrepareCB(uv_prepare_t* handle) { } void IterationHistogram::CheckCB(uv_check_t* handle) { - IterationHistogram* self = ContainerOf(&IterationHistogram::check_handle_, handle); + IterationHistogram* self = + ContainerOf(&IterationHistogram::check_handle_, handle); if (!self->enabled_) return; uint64_t check_time = uv_hrtime(); @@ -565,29 +573,21 @@ void IterationHistogram::Close(Local close_callback) { } void IterationHistogram::Start(const FunctionCallbackInfo& args) { - IterationHistogram* histogram; - ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); - histogram->OnStart(args[0]->IsTrue() ? StartFlags::RESET : StartFlags::NONE); + StartHandleHistogram(args.This(), args[0]->IsTrue()); } void IterationHistogram::FastStart(Local receiver, bool reset) { TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.start"); - IterationHistogram* histogram; - ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); - histogram->OnStart(reset ? StartFlags::RESET : StartFlags::NONE); + StartHandleHistogram(receiver, reset); } void IterationHistogram::Stop(const FunctionCallbackInfo& args) { - IterationHistogram* histogram; - ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); - histogram->OnStop(); + StopHandleHistogram(args.This()); } void IterationHistogram::FastStop(Local receiver) { TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.stop"); - IterationHistogram* histogram; - ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver); - histogram->OnStop(); + StopHandleHistogram(receiver); } void HistogramImpl::GetCount(const FunctionCallbackInfo& args) { @@ -753,7 +753,8 @@ HistogramImpl* HistogramImpl::FromJSObject(Local value) { HistogramImpl::kImplField, EmbedderDataTag::kDefault)); } -std::unique_ptr IterationHistogram::CloneForMessaging() const { +std::unique_ptr IterationHistogram::CloneForMessaging() + const { return std::make_unique(histogram()); } diff --git a/src/histogram.h b/src/histogram.h index c914bc34d08d63..b9f968e8347c2e 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -20,6 +20,11 @@ namespace node { class ExternalReferenceRegistry; +template +void StartHandleHistogram(v8::Local receiver, bool reset); +template +void StopHandleHistogram(v8::Local receiver); + constexpr int kDefaultHistogramFigures = 3; class Histogram : public MemoryRetainer { @@ -257,6 +262,11 @@ class IntervalHistogram final : public HandleWrap, public HistogramImpl { void OnStart(StartFlags flags = StartFlags::RESET); void OnStop(); + template + friend void StartHandleHistogram(v8::Local, bool); + template + friend void StopHandleHistogram(v8::Local); + bool enabled_ = false; int32_t interval_ = 0; std::function on_interval_; @@ -312,6 +322,11 @@ class IterationHistogram final : public HandleWrap, public HistogramImpl { void OnStart(StartFlags flags = StartFlags::RESET); void OnStop(); + template + friend void StartHandleHistogram(v8::Local, bool); + template + friend void StopHandleHistogram(v8::Local); + bool enabled_ = false; uv_prepare_t prepare_handle_; uv_check_t check_handle_; diff --git a/src/node_task_queue.cc b/src/node_task_queue.cc index 0d40f3914b101e..396ed5c4758c16 100644 --- a/src/node_task_queue.cc +++ b/src/node_task_queue.cc @@ -76,9 +76,11 @@ void PromiseRejectCallback(PromiseRejectMessage message) { value = Undefined(isolate); rejectionsHandledAfter++; TRACE_COUNTER2(TRACING_CATEGORY_NODE2(promises, rejections), - "rejections", - "unhandled", unhandledRejections, - "handledAfter", rejectionsHandledAfter); + "rejections", + "unhandled", + unhandledRejections, + "handledAfter", + rejectionsHandledAfter); } else { return; }