Skip to content

Commit e7093c8

Browse files
committed
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 <[email protected]>
1 parent 2a601eb commit e7093c8

7 files changed

Lines changed: 288 additions & 13 deletions

File tree

doc/api/perf_hooks.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1708,20 +1708,23 @@ added: v11.10.0
17081708
-->
17091709

17101710
* `options` {Object}
1711-
* `resolution` {number} The sampling rate in milliseconds. Must be greater
1712-
than zero. **Default:** `10`.
1711+
* `samplePerIteration` {boolean} When `true`, samples are taken once per
1712+
event loop iteration. **Default:** `false`.
1713+
* `resolution` {number} The sampling rate in milliseconds for interval-based
1714+
sampling. Must be greater than zero. This option is ignored when
1715+
`samplePerIteration` is `true`. **Default:** `10`.
17131716
* Returns: {IntervalHistogram}
17141717

17151718
_This property is an extension by Node.js. It is not available in Web browsers._
17161719

17171720
Creates an `IntervalHistogram` object that samples and reports the event loop
17181721
delay over time. The delays will be reported in nanoseconds.
17191722

1720-
Using a timer to detect approximate event loop delay works because the
1721-
execution of timers is tied specifically to the lifecycle of the libuv
1722-
event loop. That is, a delay in the loop will cause a delay in the execution
1723-
of the timer, and those delays are specifically what this API is intended to
1724-
detect.
1723+
By default, the histogram is updated by a timer using the configured
1724+
`resolution`. When `samplePerIteration` is `true`, samples are taken once per
1725+
event loop iteration using `uv_prepare_t` and `uv_check_t` hooks. In that mode,
1726+
the histogram does not keep the loop alive or force additional iterations when
1727+
the application is idle.
17251728

17261729
```mjs
17271730
import { monitorEventLoopDelay } from 'node:perf_hooks';
@@ -2000,7 +2003,7 @@ The standard deviation of the recorded event loop delays.
20002003

20012004
## Class: `IntervalHistogram extends Histogram`
20022005

2003-
A `Histogram` that is periodically updated on a given interval.
2006+
A `Histogram` that records event loop delay.
20042007

20052008
### `histogram.disable()`
20062009

@@ -2010,7 +2013,7 @@ added: v11.10.0
20102013

20112014
* Returns: {boolean}
20122015

2013-
Disables the update interval timer. Returns `true` if the timer was
2016+
Disables event loop delay sampling. Returns `true` if sampling was
20142017
stopped, `false` if it was already stopped.
20152018

20162019
### `histogram.enable()`
@@ -2021,7 +2024,7 @@ added: v11.10.0
20212024

20222025
* Returns: {boolean}
20232026

2024-
Enables the update interval timer. Returns `true` if the timer was
2027+
Enables event loop delay sampling. Returns `true` if sampling was
20252028
started, `false` if it was already started.
20262029

20272030
### `histogram[Symbol.dispose]()`
@@ -2030,7 +2033,7 @@ started, `false` if it was already started.
20302033
added: v24.2.0
20312034
-->
20322035

2033-
Disables the update interval timer when the histogram is disposed.
2036+
Disables event loop delay sampling when the histogram is disposed.
20342037

20352038
```js
20362039
const { monitorEventLoopDelay } = require('node:perf_hooks');

lib/internal/perf/event_loop_delay.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const {
1818
} = internalBinding('performance');
1919

2020
const {
21+
validateBoolean,
2122
validateInteger,
2223
validateObject,
2324
} = require('internal/validators');
@@ -74,21 +75,23 @@ class ELDHistogram extends Histogram {
7475

7576
/**
7677
* @param {{
78+
* samplePerIteration : boolean,
7779
* resolution : number
7880
* }} [options]
7981
* @returns {ELDHistogram}
8082
*/
8183
function monitorEventLoopDelay(options = kEmptyObject) {
8284
validateObject(options, 'options');
8385

84-
const { resolution = 10 } = options;
86+
const { samplePerIteration = false, resolution = 10 } = options;
87+
validateBoolean(samplePerIteration, 'options.samplePerIteration');
8588
validateInteger(resolution, 'options.resolution', 1);
8689

8790
return ReflectConstruct(
8891
function() {
8992
markTransferMode(this, true, false);
9093
this[kEnabled] = false;
91-
this[kHandle] = createELDHistogram(resolution);
94+
this[kHandle] = createELDHistogram(resolution, samplePerIteration);
9295
this[kMap] = new SafeMap();
9396
}, [], ELDHistogram);
9497
}

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@
429429
V(http2ping_constructor_template, v8::ObjectTemplate) \
430430
V(i18n_converter_template, v8::ObjectTemplate) \
431431
V(intervalhistogram_constructor_template, v8::FunctionTemplate) \
432+
V(eldhistogram_constructor_template, v8::FunctionTemplate) \
432433
V(iter_template, v8::DictionaryTemplate) \
433434
V(js_transferable_constructor_template, v8::FunctionTemplate) \
434435
V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \

src/histogram.cc

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ CFunction IntervalHistogram::fast_start_(
6868
CFunction::Make(&IntervalHistogram::FastStart));
6969
CFunction IntervalHistogram::fast_stop_(
7070
CFunction::Make(&IntervalHistogram::FastStop));
71+
CFunction ELDHistogram::fast_start_(
72+
CFunction::Make(&ELDHistogram::FastStart));
73+
CFunction ELDHistogram::fast_stop_(
74+
CFunction::Make(&ELDHistogram::FastStop));
7175

7276
void HistogramImpl::AddMethods(Isolate* isolate, Local<FunctionTemplate> tmpl) {
7377
// TODO(@jasnell): The bigint API variations do not yet support fast
@@ -444,6 +448,173 @@ void IntervalHistogram::FastStop(Local<Value> receiver) {
444448
histogram->OnStop();
445449
}
446450

451+
Local<FunctionTemplate> ELDHistogram::GetConstructorTemplate(
452+
Environment* env) {
453+
Local<FunctionTemplate> tmpl = env->eldhistogram_constructor_template();
454+
if (tmpl.IsEmpty()) {
455+
Isolate* isolate = env->isolate();
456+
tmpl = NewFunctionTemplate(isolate, nullptr);
457+
tmpl->Inherit(HandleWrap::GetConstructorTemplate(env));
458+
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram"));
459+
auto instance = tmpl->InstanceTemplate();
460+
instance->SetInternalFieldCount(ELDHistogram::kInternalFieldCount);
461+
HistogramImpl::AddMethods(isolate, tmpl);
462+
SetFastMethod(isolate, instance, "start", Start, &fast_start_);
463+
SetFastMethod(isolate, instance, "stop", Stop, &fast_stop_);
464+
env->set_eldhistogram_constructor_template(tmpl);
465+
}
466+
return tmpl;
467+
}
468+
469+
void ELDHistogram::RegisterExternalReferences(
470+
ExternalReferenceRegistry* registry) {
471+
registry->Register(Start);
472+
registry->Register(Stop);
473+
registry->Register(fast_start_);
474+
registry->Register(fast_stop_);
475+
HistogramImpl::RegisterExternalReferences(registry);
476+
}
477+
478+
ELDHistogram::ELDHistogram(
479+
Environment* env,
480+
Local<Object> wrap,
481+
AsyncWrap::ProviderType type,
482+
const Histogram::Options& options)
483+
: HandleWrap(
484+
env,
485+
wrap,
486+
reinterpret_cast<uv_handle_t*>(&check_handle_),
487+
type),
488+
HistogramImpl(options) {
489+
MakeWeak();
490+
wrap->SetAlignedPointerInInternalField(
491+
HistogramImpl::InternalFields::kImplField,
492+
static_cast<HistogramImpl*>(this),
493+
EmbedderDataTag::kDefault);
494+
uv_check_init(env->event_loop(), &check_handle_);
495+
uv_prepare_init(env->event_loop(), &prepare_handle_);
496+
uv_unref(reinterpret_cast<uv_handle_t*>(&check_handle_));
497+
uv_unref(reinterpret_cast<uv_handle_t*>(&prepare_handle_));
498+
prepare_handle_.data = this;
499+
}
500+
501+
BaseObjectPtr<ELDHistogram> ELDHistogram::Create(
502+
Environment* env,
503+
const Histogram::Options& options) {
504+
Local<Object> obj;
505+
if (!GetConstructorTemplate(env)
506+
->InstanceTemplate()
507+
->NewInstance(env->context()).ToLocal(&obj)) {
508+
return nullptr;
509+
}
510+
511+
return MakeBaseObject<ELDHistogram>(
512+
env,
513+
obj,
514+
AsyncWrap::PROVIDER_ELDHISTOGRAM,
515+
options);
516+
}
517+
518+
void ELDHistogram::PrepareCB(uv_prepare_t* handle) {
519+
ELDHistogram* self = static_cast<ELDHistogram*>(handle->data);
520+
if (!self->enabled_) return;
521+
self->prepare_time_ = uv_hrtime();
522+
self->timeout_ = uv_backend_timeout(handle->loop);
523+
}
524+
525+
void ELDHistogram::CheckCB(uv_check_t* handle) {
526+
ELDHistogram* self =
527+
ContainerOf(&ELDHistogram::check_handle_, handle);
528+
if (!self->enabled_) return;
529+
530+
uint64_t check_time = uv_hrtime();
531+
uint64_t poll_time = check_time - self->prepare_time_;
532+
uint64_t latency = self->prepare_time_ - self->check_time_;
533+
534+
if (self->timeout_ >= 0) {
535+
uint64_t timeout_ns = static_cast<uint64_t>(self->timeout_) * 1000 * 1000;
536+
if (poll_time > timeout_ns) {
537+
latency += poll_time - timeout_ns;
538+
}
539+
}
540+
541+
self->histogram()->Record(latency == 0 ? 1 : latency);
542+
self->check_time_ = check_time;
543+
}
544+
545+
void ELDHistogram::MemoryInfo(MemoryTracker* tracker) const {
546+
tracker->TrackField("histogram", histogram());
547+
}
548+
549+
void ELDHistogram::OnStart(StartFlags flags) {
550+
if (enabled_ || IsHandleClosing()) return;
551+
enabled_ = true;
552+
if (flags == StartFlags::RESET)
553+
histogram()->Reset();
554+
check_time_ = uv_hrtime();
555+
prepare_time_ = check_time_;
556+
timeout_ = 0;
557+
uv_check_start(&check_handle_, CheckCB);
558+
uv_prepare_start(&prepare_handle_, PrepareCB);
559+
uv_unref(reinterpret_cast<uv_handle_t*>(&check_handle_));
560+
uv_unref(reinterpret_cast<uv_handle_t*>(&prepare_handle_));
561+
}
562+
563+
void ELDHistogram::OnStop() {
564+
if (!enabled_ || IsHandleClosing()) return;
565+
enabled_ = false;
566+
uv_check_stop(&check_handle_);
567+
uv_prepare_stop(&prepare_handle_);
568+
}
569+
570+
void ELDHistogram::PrepareCloseCB(uv_handle_t* handle) {
571+
ELDHistogram* self = static_cast<ELDHistogram*>(handle->data);
572+
uv_close(reinterpret_cast<uv_handle_t*>(&self->check_handle_),
573+
HandleWrap::OnClose);
574+
}
575+
576+
void ELDHistogram::Close(Local<Value> close_callback) {
577+
if (IsHandleClosing()) return;
578+
OnStop();
579+
state_ = kClosing;
580+
581+
if (!close_callback.IsEmpty() && close_callback->IsFunction() &&
582+
!persistent().IsEmpty()) {
583+
object()->Set(env()->context(),
584+
env()->handle_onclose_symbol(),
585+
close_callback).Check();
586+
}
587+
588+
uv_close(reinterpret_cast<uv_handle_t*>(&prepare_handle_),
589+
PrepareCloseCB);
590+
}
591+
592+
void ELDHistogram::Start(const FunctionCallbackInfo<Value>& args) {
593+
ELDHistogram* histogram;
594+
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
595+
histogram->OnStart(args[0]->IsTrue() ? StartFlags::RESET : StartFlags::NONE);
596+
}
597+
598+
void ELDHistogram::FastStart(Local<Value> receiver, bool reset) {
599+
TRACK_V8_FAST_API_CALL("histogram.start");
600+
ELDHistogram* histogram;
601+
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
602+
histogram->OnStart(reset ? StartFlags::RESET : StartFlags::NONE);
603+
}
604+
605+
void ELDHistogram::Stop(const FunctionCallbackInfo<Value>& args) {
606+
ELDHistogram* histogram;
607+
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This());
608+
histogram->OnStop();
609+
}
610+
611+
void ELDHistogram::FastStop(Local<Value> receiver) {
612+
TRACK_V8_FAST_API_CALL("histogram.stop");
613+
ELDHistogram* histogram;
614+
ASSIGN_OR_RETURN_UNWRAP(&histogram, receiver);
615+
histogram->OnStop();
616+
}
617+
447618
void HistogramImpl::GetCount(const FunctionCallbackInfo<Value>& args) {
448619
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
449620
double value = static_cast<double>((*histogram)->Count());
@@ -607,6 +778,11 @@ HistogramImpl* HistogramImpl::FromJSObject(Local<Value> value) {
607778
HistogramImpl::kImplField, EmbedderDataTag::kDefault));
608779
}
609780

781+
std::unique_ptr<worker::TransferData>
782+
ELDHistogram::CloneForMessaging() const {
783+
return std::make_unique<HistogramBase::HistogramTransferData>(histogram());
784+
}
785+
610786
std::unique_ptr<worker::TransferData>
611787
IntervalHistogram::CloneForMessaging() const {
612788
return std::make_unique<HistogramBase::HistogramTransferData>(histogram());

src/histogram.h

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,69 @@ class IntervalHistogram final : public HandleWrap, public HistogramImpl {
266266
static v8::CFunction fast_stop_;
267267
};
268268

269+
class ELDHistogram final : public HandleWrap, public HistogramImpl {
270+
public:
271+
enum InternalFields {
272+
kInternalFieldCount = std::max<uint32_t>(
273+
HandleWrap::kInternalFieldCount, HistogramImpl::kInternalFieldCount),
274+
};
275+
276+
enum class StartFlags {
277+
NONE,
278+
RESET
279+
};
280+
281+
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
282+
283+
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
284+
Environment* env);
285+
286+
static BaseObjectPtr<ELDHistogram> Create(
287+
Environment* env,
288+
const Histogram::Options& options);
289+
290+
ELDHistogram(
291+
Environment* env,
292+
v8::Local<v8::Object> wrap,
293+
AsyncWrap::ProviderType type,
294+
const Histogram::Options& options = Histogram::Options {});
295+
296+
static void Start(const v8::FunctionCallbackInfo<v8::Value>& args);
297+
static void Stop(const v8::FunctionCallbackInfo<v8::Value>& args);
298+
299+
static void FastStart(v8::Local<v8::Value> receiver, bool reset);
300+
static void FastStop(v8::Local<v8::Value> receiver);
301+
302+
BaseObject::TransferMode GetTransferMode() const override {
303+
return TransferMode::kCloneable;
304+
}
305+
std::unique_ptr<worker::TransferData> CloneForMessaging() const override;
306+
307+
void Close(v8::Local<v8::Value> close_callback =
308+
v8::Local<v8::Value>()) override;
309+
310+
void MemoryInfo(MemoryTracker* tracker) const override;
311+
SET_MEMORY_INFO_NAME(ELDHistogram)
312+
SET_SELF_SIZE(ELDHistogram)
313+
314+
private:
315+
static void PrepareCB(uv_prepare_t* handle);
316+
static void CheckCB(uv_check_t* handle);
317+
static void PrepareCloseCB(uv_handle_t* handle);
318+
void OnStart(StartFlags flags = StartFlags::RESET);
319+
void OnStop();
320+
321+
bool enabled_ = false;
322+
uv_prepare_t prepare_handle_;
323+
uv_check_t check_handle_;
324+
uint64_t prepare_time_ = 0;
325+
uint64_t check_time_ = 0;
326+
int64_t timeout_ = 0;
327+
328+
static v8::CFunction fast_start_;
329+
static v8::CFunction fast_stop_;
330+
};
331+
269332
} // namespace node
270333

271334
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

src/node_perf.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ void CreateELDHistogram(const FunctionCallbackInfo<Value>& args) {
282282
Environment* env = Environment::GetCurrent(args);
283283
int64_t interval = args[0].As<Integer>()->Value();
284284
CHECK_GT(interval, 0);
285+
if (args[1]->IsTrue()) {
286+
BaseObjectPtr<ELDHistogram> histogram =
287+
ELDHistogram::Create(env, Histogram::Options { 1 });
288+
args.GetReturnValue().Set(histogram->object());
289+
return;
290+
}
285291
BaseObjectPtr<IntervalHistogram> histogram =
286292
IntervalHistogram::Create(env, interval, [](Histogram& histogram) {
287293
uint64_t delta = histogram.RecordDelta();
@@ -413,6 +419,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
413419
registry->Register(fast_performance_now);
414420
HistogramBase::RegisterExternalReferences(registry);
415421
IntervalHistogram::RegisterExternalReferences(registry);
422+
ELDHistogram::RegisterExternalReferences(registry);
416423
}
417424
} // namespace performance
418425
} // namespace node

0 commit comments

Comments
 (0)