From 7747720a22c50998be01607b37818ca2707d7fc8 Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Fri, 24 Apr 2026 14:28:07 -0400 Subject: [PATCH 1/9] perf_hooks: add per event loop iteration delay sampling add a samplePerIteration option to monitorEventLoopDelay that records event loop delay from libuv event loop iterations instead of the existing timer interval sampler. The default remains the interval based, uses of monitorEventLoopDelay() will still behave the same unless the samplePerIteration options is passed through. Signed-off-by: Pablo Erhard Hernandez --- 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 61634056679062..70b13f8620bf39 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1707,8 +1707,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._ @@ -1716,11 +1719,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'; @@ -1999,7 +2002,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()` @@ -2009,7 +2012,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()` @@ -2020,7 +2023,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]()` @@ -2029,7 +2032,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 6530f89ec918ac..40e7bba383e991 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -431,6 +431,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 e984fd4c3bf003..5d2d0c623c9097 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(); @@ -413,6 +419,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 2660e2ec382d165830fd89507648188d4ffc4f59 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 89a5c17f2400293bcec4d8d84c345ccd96b52b71 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 5d2d0c623c9097..198fe136f38c20 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 3b216177c5c035c59dea16a27844d2102a115cc0 Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Tue, 19 May 2026 17:20:45 -0400 Subject: [PATCH 4/9] refactor: 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 d243595c66d5743340a0a1218bd72e855c371dd9 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 c52710d2f23f6a97eb81bbcbc7f25989013e0fae Mon Sep 17 00:00:00 2001 From: pabloerhard Date: Wed, 27 May 2026 15:48:50 -0400 Subject: [PATCH 6/9] docs: improve docs Modified docs to be on par with the changes and modifications made. --- 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 70b13f8620bf39..32a0e9188dafc2 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1704,6 +1704,10 @@ are not guaranteed to reflect any correct state of the event loop. * `options` {Object} @@ -1712,18 +1716,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'; @@ -2002,7 +2008,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()` @@ -2043,11 +2049,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 62dfe080a093c4be09f9a341272bdf3070488f63 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 40e7bba383e991..489fc220d0de03 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -431,7 +431,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 198fe136f38c20..9900e08b974245 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; } @@ -419,7 +419,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 88fce8d6ed4805787eaba9fbd23f7a803e3de249 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 32a0e9188dafc2..7e9c86abf2935f 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1716,7 +1716,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._ @@ -2006,9 +2006,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()` @@ -2049,17 +2050,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` @@ -2367,6 +2362,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 29b708d5606be711a9b14ddbca2a8aa9dc967bfb 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 489fc220d0de03..99a9041f6f5ddd 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -431,7 +431,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; }