perf,src: add HistogramBase and internal/histogram.js

Separating this out from the QUIC PR to allow it to be separately
reviewed. The QUIC implementation makes use of the hdr_histogram
for dynamic performance monitoring. This introduces a BaseObject
class that allows the internal histograms to be accessed on the
JavaScript side and adds a generic Histogram class that will be
used by both QUIC and perf_hooks (for the event loop delay
monitoring).

Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: https://github.com/nodejs/node/pull/31988
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
James M Snell 2020-02-27 13:14:38 -08:00
parent 0fac393d26
commit eb2fe5ff90
8 changed files with 355 additions and 74 deletions

94
lib/internal/histogram.js Normal file
View File

@ -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,
};

View File

@ -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 = {}) {

View File

@ -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',

View File

@ -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) \

View File

@ -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<double>(
hdr_value_at_percentile(histogram_.get(), percentile));
}
inline void Histogram::Percentiles(std::function<void(double, double)> fn) {
template <typename Iterator>
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<double>(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

141
src/histogram.cc Normal file
View File

@ -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<v8::Object> 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<Value>& args) {
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder());
double value = static_cast<double>(histogram->Min());
args.GetReturnValue().Set(value);
}
void HistogramBase::GetMax(const FunctionCallbackInfo<Value>& args) {
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder());
double value = static_cast<double>(histogram->Max());
args.GetReturnValue().Set(value);
}
void HistogramBase::GetMean(const FunctionCallbackInfo<Value>& args) {
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder());
args.GetReturnValue().Set(histogram->Mean());
}
void HistogramBase::GetExceeds(const FunctionCallbackInfo<Value>& args) {
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder());
double value = static_cast<double>(histogram->Exceeds());
args.GetReturnValue().Set(value);
}
void HistogramBase::GetStddev(const FunctionCallbackInfo<Value>& args) {
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder());
args.GetReturnValue().Set(histogram->Stddev());
}
void HistogramBase::GetPercentile(
const FunctionCallbackInfo<Value>& args) {
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder());
CHECK(args[0]->IsNumber());
double percentile = args[0].As<Number>()->Value();
args.GetReturnValue().Set(histogram->Percentile(percentile));
}
void HistogramBase::GetPercentiles(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder());
CHECK(args[0]->IsMap());
Local<Map> map = args[0].As<Map>();
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<Value>& args) {
HistogramBase* histogram;
ASSIGN_OR_RETURN_UNWRAP(&histogram, args.Holder());
histogram->ResetState();
}
BaseObjectPtr<HistogramBase> HistogramBase::New(
Environment* env,
int64_t lowest,
int64_t highest,
int figures) {
CHECK_LE(lowest, highest);
CHECK_GT(figures, 0);
v8::Local<v8::Object> obj;
auto tmpl = env->histogram_instance_template();
if (!tmpl->NewInstance(env->context()).ToLocal(&obj))
return {};
return MakeDetachedBaseObject<HistogramBase>(
env, obj, lowest, highest, figures);
}
void HistogramBase::Initialize(Environment* env) {
// Guard against multiple initializations
if (!env->histogram_instance_template().IsEmpty())
return;
Local<FunctionTemplate> histogram = FunctionTemplate::New(env->isolate());
Local<String> classname = FIXED_ONE_BYTE_STRING(env->isolate(), "Histogram");
histogram->SetClassName(classname);
Local<ObjectTemplate> 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

View File

@ -4,15 +4,24 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "hdr_histogram.h"
#include "base_object.h"
#include "util.h"
#include <functional>
#include <limits>
#include <map>
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<int64_t>::min(),
int64_t highest = std::numeric_limits<int64_t>::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<void(double, double)> fn);
// Iterator is a function type that takes two doubles as argument, one for
// percentile and one for the value at that percentile.
template <typename Iterator>
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<hdr_histogram, hdr_close>;
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<v8::Value>& args);
static void GetMax(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetMean(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetExceeds(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetStddev(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetPercentile(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetPercentiles(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void DoReset(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Initialize(Environment* env);
static BaseObjectPtr<HistogramBase> New(
Environment* env,
int64_t lowest = std::numeric_limits<int64_t>::min(),
int64_t highest = std::numeric_limits<int64_t>::max(),
int figures = kDefaultHistogramFigures);
HistogramBase(
Environment* env,
v8::Local<v8::Object> wrap,
int64_t lowest = std::numeric_limits<int64_t>::min(),
int64_t highest = std::numeric_limits<int64_t>::max(),
int figures = kDefaultHistogramFigures);
private:
int64_t exceeds_ = 0;
uint64_t prev_ = 0;
};
} // namespace node

View File

@ -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());