Skip to content
49 changes: 30 additions & 19 deletions doc/api/perf_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1704,23 +1704,32 @@ are not guaranteed to reflect any correct state of the event loop.

<!-- YAML
added: v11.10.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/62935
description: Added the `samplePerIteration` option.
-->
Comment thread
pabloerhard marked this conversation as resolved.

* `options` {Object}
* `resolution` {number} The sampling rate in milliseconds. Must be greater
than zero. **Default:** `10`.
* Returns: {IntervalHistogram}
* `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: {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.

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.
Comment thread
pabloerhard marked this conversation as resolved.
The two sampling modes produce significantly different results and should not
be compared directly.

```mjs
import { monitorEventLoopDelay } from 'node:perf_hooks';
Expand Down Expand Up @@ -1997,9 +2006,10 @@ added: v11.10.0

The standard deviation of the recorded event loop delays.

## Class: `IntervalHistogram extends Histogram`
## Class: `ELDHistogram extends Histogram`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the rename? the intent here was for it to be named generically so that it could potentially be used for things other than just event loop delay monitoring.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rename was meant to avoid documenting that monitorEventLoopDelay() strictly returns an IntervalHistogram, this because with the new samplePerIteration option the native backing is no longer always interval based. Leaving the documented type as IntervalHistogram felt misleading considering the same API can be backed by either an iteration or interval based sampling.

My first approach was to document that monitorEventLoopDelay() returns {IntervalHistogram|IterationHistogram}, but that felt redundant considering both modes expose the same JS API shape and only differ in how samples are collected. That said, maybe this approach is a better option if we want to maintain IntervalHistogram as a generic documented type while also introducing IterationHistogram.


A `Histogram` that is periodically updated on a given interval.
A `Histogram` that records event loop delay, returned by
[`perf_hooks.monitorEventLoopDelay()`][].

### `histogram.disable()`

Expand All @@ -2009,7 +2019,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()`
Expand All @@ -2020,7 +2030,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]()`
Expand All @@ -2029,7 +2039,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');
Expand All @@ -2040,11 +2050,11 @@ const { monitorEventLoopDelay } = require('node:perf_hooks');
}
```

### Cloning an `IntervalHistogram`
### Cloning an `ELDHistogram`

{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.
{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`

Expand Down Expand Up @@ -2352,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
Expand Down
7 changes: 5 additions & 2 deletions lib/internal/perf/event_loop_delay.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
} = internalBinding('performance');

const {
validateBoolean,
validateInteger,
validateObject,
} = require('internal/validators');
Expand Down Expand Up @@ -74,21 +75,23 @@ class ELDHistogram extends Histogram {

/**
* @param {{
* samplePerIteration : boolean,
* resolution : number
* }} [options]
* @returns {ELDHistogram}
*/
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);
}
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +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(iter_template, v8::DictionaryTemplate) \
V(js_transferable_constructor_template, v8::FunctionTemplate) \
V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \
Expand Down
175 changes: 163 additions & 12 deletions src/histogram.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ using v8::String;
using v8::Uint32;
using v8::Value;

template <typename T>
void StartHandleHistogram(Local<Value> receiver, bool reset) {
T* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
histogram->OnStart(reset ? T::StartFlags::RESET : T::StartFlags::NONE);
}

template <typename T>
void StopHandleHistogram(Local<Value> 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,
Expand Down Expand Up @@ -68,6 +82,10 @@ CFunction IntervalHistogram::fast_start_(
CFunction::Make(&IntervalHistogram::FastStart));
CFunction IntervalHistogram::fast_stop_(
CFunction::Make(&IntervalHistogram::FastStop));
CFunction IterationHistogram::fast_start_(
CFunction::Make(&IterationHistogram::FastStart));
CFunction IterationHistogram::fast_stop_(
CFunction::Make(&IterationHistogram::FastStop));

void HistogramImpl::AddMethods(Isolate* isolate, Local<FunctionTemplate> tmpl) {
// TODO(@jasnell): The bigint API variations do not yet support fast
Expand Down Expand Up @@ -419,29 +437,157 @@ void IntervalHistogram::OnStop() {
}

void IntervalHistogram::Start(const FunctionCallbackInfo<Value>& args) {
IntervalHistogram* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
histogram->OnStart(args[0]->IsTrue() ? StartFlags::RESET : StartFlags::NONE);
StartHandleHistogram<IntervalHistogram>(args.This(), args[0]->IsTrue());
}

void IntervalHistogram::FastStart(Local<Value> 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<IntervalHistogram>(receiver, reset);
}

void IntervalHistogram::Stop(const FunctionCallbackInfo<Value>& args) {
IntervalHistogram* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
histogram->OnStop();
StopHandleHistogram<IntervalHistogram>(args.This());
}

void IntervalHistogram::FastStop(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.stop");
IntervalHistogram* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
histogram->OnStop();
StopHandleHistogram<IntervalHistogram>(receiver);
}

Local<FunctionTemplate> IterationHistogram::GetConstructorTemplate(
Environment* env) {
Local<FunctionTemplate> 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(IterationHistogram::kInternalFieldCount);
HistogramImpl::AddMethods(isolate, tmpl);
SetFastMethod(isolate, instance, "start", Start, &fast_start_);
SetFastMethod(isolate, instance, "stop", Stop, &fast_stop_);
env->set_iterationhistogram_constructor_template(tmpl);
}
return tmpl;
}

void IterationHistogram::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(Start);
registry->Register(Stop);
registry->Register(fast_start_);
registry->Register(fast_stop_);
HistogramImpl::RegisterExternalReferences(registry);
}

IterationHistogram::IterationHistogram(Environment* env,
Local<Object> wrap,
AsyncWrap::ProviderType type,
const Histogram::Options& options)
: HandleWrap(
env, wrap, reinterpret_cast<uv_handle_t*>(&check_handle_), type),
HistogramImpl(options) {
MakeWeak();
wrap->SetAlignedPointerInInternalField(
HistogramImpl::InternalFields::kImplField,
static_cast<HistogramImpl*>(this),
EmbedderDataTag::kDefault);
uv_check_init(env->event_loop(), &check_handle_);
uv_prepare_init(env->event_loop(), &prepare_handle_);
uv_unref(reinterpret_cast<uv_handle_t*>(&check_handle_));
uv_unref(reinterpret_cast<uv_handle_t*>(&prepare_handle_));
prepare_handle_.data = this;
}

BaseObjectPtr<IterationHistogram> IterationHistogram::Create(
Environment* env, const Histogram::Options& options) {
Local<Object> obj;
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return nullptr;
}

return MakeBaseObject<IterationHistogram>(
env, obj, AsyncWrap::PROVIDER_ELDHISTOGRAM, options);
}

void IterationHistogram::PrepareCB(uv_prepare_t* handle) {
IterationHistogram* self = static_cast<IterationHistogram*>(handle->data);
if (!self->enabled_) return;
self->prepare_time_ = uv_hrtime();
self->timeout_ = uv_backend_timeout(handle->loop);
}

void IterationHistogram::CheckCB(uv_check_t* handle) {
IterationHistogram* self =
ContainerOf(&IterationHistogram::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<uint64_t>(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 IterationHistogram::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("histogram", histogram());
}

void IterationHistogram::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<uv_handle_t*>(&check_handle_));
uv_unref(reinterpret_cast<uv_handle_t*>(&prepare_handle_));
}

void IterationHistogram::OnStop() {
if (!enabled_ || IsHandleClosing()) return;
enabled_ = false;
uv_check_stop(&check_handle_);
uv_prepare_stop(&prepare_handle_);
}

void IterationHistogram::Close(Local<Value> close_callback) {
if (IsHandleClosing()) return;
OnStop();
HandleWrap::Close(close_callback);
uv_close(reinterpret_cast<uv_handle_t*>(&prepare_handle_), nullptr);
}

void IterationHistogram::Start(const FunctionCallbackInfo<Value>& args) {
StartHandleHistogram<IterationHistogram>(args.This(), args[0]->IsTrue());
}

void IterationHistogram::FastStart(Local<Value> receiver, bool reset) {
TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.start");
StartHandleHistogram<IterationHistogram>(receiver, reset);
}

void IterationHistogram::Stop(const FunctionCallbackInfo<Value>& args) {
StopHandleHistogram<IterationHistogram>(args.This());
}

void IterationHistogram::FastStop(Local<Value> receiver) {
TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.stop");
StopHandleHistogram<IterationHistogram>(receiver);
}

void HistogramImpl::GetCount(const FunctionCallbackInfo<Value>& args) {
Expand Down Expand Up @@ -607,6 +753,11 @@ HistogramImpl* HistogramImpl::FromJSObject(Local<Value> value) {
HistogramImpl::kImplField, EmbedderDataTag::kDefault));
}

std::unique_ptr<worker::TransferData> IterationHistogram::CloneForMessaging()
const {
return std::make_unique<HistogramBase::HistogramTransferData>(histogram());
}

std::unique_ptr<worker::TransferData>
IntervalHistogram::CloneForMessaging() const {
return std::make_unique<HistogramBase::HistogramTransferData>(histogram());
Expand Down
Loading
Loading