diff --git a/lib/internal/histogram.js b/lib/internal/histogram.js new file mode 100644 index 00000000000000..6deb8314a41bbb --- /dev/null +++ b/lib/internal/histogram.js @@ -0,0 +1,94 @@ +'use strict'; + +const { + customInspectSymbol: kInspect, +} = require('internal/util'); + +const { format } = require('util'); +const { Map, Symbol } = primordials; + +const { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, +} = require('internal/errors').codes; + +const kDestroy = Symbol('kDestroy'); +const kHandle = Symbol('kHandle'); + +// Histograms are created internally by Node.js and used to +// record various metrics. This Histogram class provides a +// generally read-only view of the internal histogram. +class Histogram { + #handle = undefined; + #map = new Map(); + + constructor(internal) { + this.#handle = internal; + } + + [kInspect]() { + const obj = { + min: this.min, + max: this.max, + mean: this.mean, + exceeds: this.exceeds, + stddev: this.stddev, + percentiles: this.percentiles, + }; + return `Histogram ${format(obj)}`; + } + + get min() { + return this.#handle ? this.#handle.min() : undefined; + } + + get max() { + return this.#handle ? this.#handle.max() : undefined; + } + + get mean() { + return this.#handle ? this.#handle.mean() : undefined; + } + + get exceeds() { + return this.#handle ? this.#handle.exceeds() : undefined; + } + + get stddev() { + return this.#handle ? this.#handle.stddev() : undefined; + } + + percentile(percentile) { + if (typeof percentile !== 'number') + throw new ERR_INVALID_ARG_TYPE('percentile', 'number', percentile); + + if (percentile <= 0 || percentile > 100) + throw new ERR_INVALID_ARG_VALUE.RangeError('percentile', percentile); + + return this.#handle ? this.#handle.percentile(percentile) : undefined; + } + + get percentiles() { + this.#map.clear(); + if (this.#handle) + this.#handle.percentiles(this.#map); + return this.#map; + } + + reset() { + if (this.#handle) + this.#handle.reset(); + } + + [kDestroy]() { + this.#handle = undefined; + } + + get [kHandle]() { return this.#handle; } +} + +module.exports = { + Histogram, + kDestroy, + kHandle, +}; diff --git a/lib/perf_hooks.js b/lib/perf_hooks.js index 267b4577ffee3d..a141705728227b 100644 --- a/lib/perf_hooks.js +++ b/lib/perf_hooks.js @@ -3,7 +3,6 @@ const { ArrayIsArray, Boolean, - Map, NumberIsSafeInteger, ObjectDefineProperties, ObjectDefineProperty, @@ -52,16 +51,18 @@ const kInspect = require('internal/util').customInspectSymbol; const { ERR_INVALID_CALLBACK, - ERR_INVALID_ARG_VALUE, ERR_INVALID_ARG_TYPE, ERR_INVALID_OPT_VALUE, ERR_VALID_PERFORMANCE_ENTRY_TYPE, ERR_INVALID_PERFORMANCE_MARK } = require('internal/errors').codes; +const { + Histogram, + kHandle, +} = require('internal/histogram'); + const { setImmediate } = require('timers'); -const kHandle = Symbol('handle'); -const kMap = Symbol('map'); const kCallback = Symbol('callback'); const kTypes = Symbol('types'); const kEntries = Symbol('entries'); @@ -557,47 +558,9 @@ function sortedInsert(list, entry) { list.splice(location, 0, entry); } -class ELDHistogram { - constructor(handle) { - this[kHandle] = handle; - this[kMap] = new Map(); - } - - reset() { this[kHandle].reset(); } +class ELDHistogram extends Histogram { enable() { return this[kHandle].enable(); } disable() { return this[kHandle].disable(); } - - get exceeds() { return this[kHandle].exceeds(); } - get min() { return this[kHandle].min(); } - get max() { return this[kHandle].max(); } - get mean() { return this[kHandle].mean(); } - get stddev() { return this[kHandle].stddev(); } - percentile(percentile) { - if (typeof percentile !== 'number') { - throw new ERR_INVALID_ARG_TYPE('percentile', 'number', percentile); - } - if (percentile <= 0 || percentile > 100) { - throw new ERR_INVALID_ARG_VALUE.RangeError('percentile', - percentile); - } - return this[kHandle].percentile(percentile); - } - get percentiles() { - this[kMap].clear(); - this[kHandle].percentiles(this[kMap]); - return this[kMap]; - } - - [kInspect]() { - return { - min: this.min, - max: this.max, - mean: this.mean, - stddev: this.stddev, - percentiles: this.percentiles, - exceeds: this.exceeds - }; - } } function monitorEventLoopDelay(options = {}) { diff --git a/node.gyp b/node.gyp index 9c632d383852ae..b941eff56570e4 100644 --- a/node.gyp +++ b/node.gyp @@ -141,6 +141,7 @@ 'lib/internal/fs/watchers.js', 'lib/internal/http.js', 'lib/internal/heap_utils.js', + 'lib/internal/histogram.js', 'lib/internal/idna.js', 'lib/internal/inspector_async_hook.js', 'lib/internal/js_stream_socket.js', @@ -534,6 +535,7 @@ 'src/fs_event_wrap.cc', 'src/handle_wrap.cc', 'src/heap_utils.cc', + 'src/histogram.cc', 'src/js_native_api.h', 'src/js_native_api_types.h', 'src/js_native_api_v8.cc', diff --git a/src/env.h b/src/env.h index 3b577e40307640..f02c8e9775ff7e 100644 --- a/src/env.h +++ b/src/env.h @@ -406,6 +406,7 @@ constexpr size_t kFsStatsBufferLength = V(filehandlereadwrap_template, v8::ObjectTemplate) \ V(fsreqpromise_constructor_template, v8::ObjectTemplate) \ V(handle_wrap_ctor_template, v8::FunctionTemplate) \ + V(histogram_instance_template, v8::ObjectTemplate) \ V(http2settings_constructor_template, v8::ObjectTemplate) \ V(http2stream_constructor_template, v8::ObjectTemplate) \ V(http2ping_constructor_template, v8::ObjectTemplate) \ diff --git a/src/histogram-inl.h b/src/histogram-inl.h index 3135041f7387a9..58911dae8f2dae 100644 --- a/src/histogram-inl.h +++ b/src/histogram-inl.h @@ -4,58 +4,78 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include "histogram.h" +#include "base_object-inl.h" #include "node_internals.h" namespace node { -inline Histogram::Histogram(int64_t lowest, int64_t highest, int figures) { - CHECK_EQ(0, hdr_init(lowest, highest, figures, &histogram_)); +void Histogram::Reset() { + hdr_reset(histogram_.get()); } -inline Histogram::~Histogram() { - hdr_close(histogram_); +bool Histogram::Record(int64_t value) { + return hdr_record_value(histogram_.get(), value); } -inline void Histogram::Reset() { - hdr_reset(histogram_); +int64_t Histogram::Min() { + return hdr_min(histogram_.get()); } -inline bool Histogram::Record(int64_t value) { - return hdr_record_value(histogram_, value); +int64_t Histogram::Max() { + return hdr_max(histogram_.get()); } -inline int64_t Histogram::Min() { - return hdr_min(histogram_); +double Histogram::Mean() { + return hdr_mean(histogram_.get()); } -inline int64_t Histogram::Max() { - return hdr_max(histogram_); +double Histogram::Stddev() { + return hdr_stddev(histogram_.get()); } -inline double Histogram::Mean() { - return hdr_mean(histogram_); -} - -inline double Histogram::Stddev() { - return hdr_stddev(histogram_); -} - -inline double Histogram::Percentile(double percentile) { +double Histogram::Percentile(double percentile) { CHECK_GT(percentile, 0); CHECK_LE(percentile, 100); - return hdr_value_at_percentile(histogram_, percentile); + return static_cast( + hdr_value_at_percentile(histogram_.get(), percentile)); } -inline void Histogram::Percentiles(std::function fn) { +template +void Histogram::Percentiles(Iterator&& fn) { hdr_iter iter; - hdr_iter_percentile_init(&iter, histogram_, 1); + hdr_iter_percentile_init(&iter, histogram_.get(), 1); while (hdr_iter_next(&iter)) { double key = iter.specifics.percentiles.percentile; - double value = iter.value; + double value = static_cast(iter.value); fn(key, value); } } +bool HistogramBase::RecordDelta() { + uint64_t time = uv_hrtime(); + bool ret = true; + if (prev_ > 0) { + int64_t delta = time - prev_; + if (delta > 0) { + ret = Record(delta); + TraceDelta(delta); + if (!ret) { + if (exceeds_ < 0xFFFFFFFF) + exceeds_++; + TraceExceeds(delta); + } + } + } + prev_ = time; + return ret; +} + +void HistogramBase::ResetState() { + Reset(); + exceeds_ = 0; + prev_ = 0; +} + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/histogram.cc b/src/histogram.cc new file mode 100644 index 00000000000000..8d1eb77b1bc88e --- /dev/null +++ b/src/histogram.cc @@ -0,0 +1,141 @@ +#include "histogram.h" // NOLINT(build/include_inline) +#include "histogram-inl.h" +#include "memory_tracker-inl.h" + +namespace node { + +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Local; +using v8::Map; +using v8::Number; +using v8::ObjectTemplate; +using v8::String; +using v8::Value; + +Histogram::Histogram(int64_t lowest, int64_t highest, int figures) { + hdr_histogram* histogram; + CHECK_EQ(0, hdr_init(lowest, highest, figures, &histogram)); + histogram_.reset(histogram); +} + +HistogramBase::HistogramBase( + Environment* env, + v8::Local wrap, + int64_t lowest, + int64_t highest, + int figures) + : BaseObject(env, wrap), + Histogram(lowest, highest, figures) { + MakeWeak(); +} + +void HistogramBase::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("histogram", GetMemorySize()); +} + +void HistogramBase::GetMin(const FunctionCallbackInfo& args) { + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder()); + double value = static_cast(histogram->Min()); + args.GetReturnValue().Set(value); +} + +void HistogramBase::GetMax(const FunctionCallbackInfo& args) { + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder()); + double value = static_cast(histogram->Max()); + args.GetReturnValue().Set(value); +} + +void HistogramBase::GetMean(const FunctionCallbackInfo& args) { + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder()); + args.GetReturnValue().Set(histogram->Mean()); +} + +void HistogramBase::GetExceeds(const FunctionCallbackInfo& args) { + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder()); + double value = static_cast(histogram->Exceeds()); + args.GetReturnValue().Set(value); +} + +void HistogramBase::GetStddev(const FunctionCallbackInfo& args) { + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder()); + args.GetReturnValue().Set(histogram->Stddev()); +} + +void HistogramBase::GetPercentile( + const FunctionCallbackInfo& args) { + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder()); + CHECK(args[0]->IsNumber()); + double percentile = args[0].As()->Value(); + args.GetReturnValue().Set(histogram->Percentile(percentile)); +} + +void HistogramBase::GetPercentiles( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder()); + CHECK(args[0]->IsMap()); + Local map = args[0].As(); + histogram->Percentiles([map, env](double key, double value) { + map->Set( + env->context(), + Number::New(env->isolate(), key), + Number::New(env->isolate(), value)).IsEmpty(); + }); +} + +void HistogramBase::DoReset(const FunctionCallbackInfo& args) { + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder()); + histogram->ResetState(); +} + +BaseObjectPtr HistogramBase::New( + Environment* env, + int64_t lowest, + int64_t highest, + int figures) { + CHECK_LE(lowest, highest); + CHECK_GT(figures, 0); + v8::Local obj; + auto tmpl = env->histogram_instance_template(); + if (!tmpl->NewInstance(env->context()).ToLocal(&obj)) + return {}; + + return MakeDetachedBaseObject( + env, obj, lowest, highest, figures); +} + +void HistogramBase::Initialize(Environment* env) { + // Guard against multiple initializations + if (!env->histogram_instance_template().IsEmpty()) + return; + + Local histogram = FunctionTemplate::New(env->isolate()); + Local classname = FIXED_ONE_BYTE_STRING(env->isolate(), "Histogram"); + histogram->SetClassName(classname); + + Local histogramt = + histogram->InstanceTemplate(); + + histogramt->SetInternalFieldCount(1); + env->SetProtoMethod(histogram, "exceeds", HistogramBase::GetExceeds); + env->SetProtoMethod(histogram, "min", HistogramBase::GetMin); + env->SetProtoMethod(histogram, "max", HistogramBase::GetMax); + env->SetProtoMethod(histogram, "mean", HistogramBase::GetMean); + env->SetProtoMethod(histogram, "stddev", HistogramBase::GetStddev); + env->SetProtoMethod(histogram, "percentile", HistogramBase::GetPercentile); + env->SetProtoMethod(histogram, "percentiles", HistogramBase::GetPercentiles); + env->SetProtoMethod(histogram, "reset", HistogramBase::DoReset); + + env->set_histogram_instance_template(histogramt); +} + +} // namespace node diff --git a/src/histogram.h b/src/histogram.h index eb94af5da2a997..e92c31c4724ac6 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -4,15 +4,24 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include "hdr_histogram.h" +#include "base_object.h" +#include "util.h" + #include +#include #include namespace node { +constexpr int kDefaultHistogramFigures = 3; + class Histogram { public: - inline Histogram(int64_t lowest, int64_t highest, int figures = 3); - inline virtual ~Histogram(); + Histogram( + int64_t lowest = std::numeric_limits::min(), + int64_t highest = std::numeric_limits::max(), + int figures = kDefaultHistogramFigures); + virtual ~Histogram() = default; inline bool Record(int64_t value); inline void Reset(); @@ -21,14 +30,65 @@ class Histogram { inline double Mean(); inline double Stddev(); inline double Percentile(double percentile); - inline void Percentiles(std::function fn); + + // Iterator is a function type that takes two doubles as argument, one for + // percentile and one for the value at that percentile. + template + inline void Percentiles(Iterator&& fn); size_t GetMemorySize() const { - return hdr_get_memory_size(histogram_); + return hdr_get_memory_size(histogram_.get()); } private: - hdr_histogram* histogram_; + using HistogramPointer = DeleteFnPtr; + HistogramPointer histogram_; +}; + +class HistogramBase : public BaseObject, public Histogram { + public: + virtual ~HistogramBase() = default; + + virtual void TraceDelta(int64_t delta) {} + virtual void TraceExceeds(int64_t delta) {} + + inline bool RecordDelta(); + inline void ResetState(); + + int64_t Exceeds() const { return exceeds_; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(HistogramBase) + SET_SELF_SIZE(HistogramBase) + + static void GetMin(const v8::FunctionCallbackInfo& args); + static void GetMax(const v8::FunctionCallbackInfo& args); + static void GetMean(const v8::FunctionCallbackInfo& args); + static void GetExceeds(const v8::FunctionCallbackInfo& args); + static void GetStddev(const v8::FunctionCallbackInfo& args); + static void GetPercentile( + const v8::FunctionCallbackInfo& args); + static void GetPercentiles( + const v8::FunctionCallbackInfo& args); + static void DoReset(const v8::FunctionCallbackInfo& args); + static void Initialize(Environment* env); + + static BaseObjectPtr New( + Environment* env, + int64_t lowest = std::numeric_limits::min(), + int64_t highest = std::numeric_limits::max(), + int figures = kDefaultHistogramFigures); + + HistogramBase( + Environment* env, + v8::Local wrap, + int64_t lowest = std::numeric_limits::min(), + int64_t highest = std::numeric_limits::max(), + int figures = kDefaultHistogramFigures); + + private: + int64_t exceeds_ = 0; + uint64_t prev_ = 0; }; } // namespace node diff --git a/src/node_perf.h b/src/node_perf.h index 4f5ca93f223289..ac65533a772e36 100644 --- a/src/node_perf.h +++ b/src/node_perf.h @@ -161,7 +161,7 @@ class ELDHistogram : public HandleWrap, public Histogram { exceeds_ = 0; prev_ = 0; } - int64_t Exceeds() { return exceeds_; } + int64_t Exceeds() const { return exceeds_; } void MemoryInfo(MemoryTracker* tracker) const override { tracker->TrackFieldWithSize("histogram", GetMemorySize());