mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
perf_hooks: add histogram.snapshot()
Cloning a histogram with `structuredClone()` or `postMessage()` shares the native histogram instead of copying it. Capturing the state of a histogram at a point in time, while other code keeps recording into it, requires serializing it with `export()` and parsing the result with `importHistogram()`. Add `histogram.snapshot()`, which returns a new, independent `Histogram` containing a copy of the histogram's configuration, recorded values, `exceeds` count, and EWMA state, without the serialization round trip. Values cannot be recorded into the returned histogram. The method is available on all histograms, including `RecordableHistogram` and `ELDHistogram` instances. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: OpenCode PR-URL: https://github.com/nodejs/node/pull/66099 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
1 parent
5af276ec30
commit
b9e257f5cb
7 files changed
+271
-10
No files matched your search
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const common = require('../common.js');
|
||||
const { createHistogram } = require('perf_hooks');
|
||||
|
||||
const bench = common.createBenchmark(main, {
|
||||
n: [1e3],
|
||||
highest: [1e6, Number.MAX_SAFE_INTEGER],
|
||||
figures: [2, 3],
|
||||
});
|
||||
|
||||
let snapshot;
|
||||
|
||||
function main({ n, highest, figures }) {
|
||||
const histogram = createHistogram({ highest, figures });
|
||||
for (let i = 1; i <= 1e4; i++) histogram.record(i);
|
||||
|
||||
bench.start();
|
||||
for (let i = 0; i < n; i++) snapshot = histogram.snapshot();
|
||||
bench.end(n);
|
||||
|
||||
assert.strictEqual(snapshot.count, 1e4);
|
||||
}
|
||||
@@ -2689,6 +2689,38 @@ distribution. A positive value indicates a right-skewed distribution
|
||||
(longer right tail, common for latency data); a negative value
|
||||
indicates a left-skewed distribution.
|
||||
|
||||
### `histogram.snapshot()`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* Returns: {Histogram}
|
||||
|
||||
Returns a new, independent {Histogram} containing a copy of this histogram's
|
||||
current state: its configuration, recorded values, `exceeds` count, and EWMA
|
||||
state. Values recorded into this histogram after this method returns, and later
|
||||
calls to `reset()`, do not change the returned histogram. This provides a stable
|
||||
view of a histogram that is still recording, such as an enabled {ELDHistogram}.
|
||||
|
||||
Values cannot be recorded into the returned histogram. Taking a snapshot copies
|
||||
every bucket, so both its time and memory cost depend on the histogram's
|
||||
`lowest`, `highest`, and `figures` configuration rather than on the number of
|
||||
recorded values.
|
||||
|
||||
```js
|
||||
const { monitorEventLoopDelay } = require('node:perf_hooks');
|
||||
|
||||
const histogram = monitorEventLoopDelay();
|
||||
histogram.enable();
|
||||
|
||||
setTimeout(() => {
|
||||
const snapshot = histogram.snapshot();
|
||||
console.log(snapshot.percentile(99));
|
||||
histogram.disable();
|
||||
}, 1000);
|
||||
```
|
||||
|
||||
### `histogram.stddev`
|
||||
|
||||
<!-- YAML
|
||||
|
||||
@@ -679,6 +679,18 @@ class Histogram {
|
||||
this[kHandle]?.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new, independent histogram containing a copy of this
|
||||
* histogram's current state. Values cannot be recorded into the returned
|
||||
* histogram.
|
||||
* @returns {Histogram}
|
||||
*/
|
||||
snapshot() {
|
||||
if (!isHistogram(this))
|
||||
throw new ERR_INVALID_THIS('Histogram');
|
||||
return new ClonedHistogram(this[kHandle].snapshot());
|
||||
}
|
||||
|
||||
[kClone]() {
|
||||
const handle = this[kHandle];
|
||||
return {
|
||||
|
||||
+59
-10
@@ -76,6 +76,51 @@ std::shared_ptr<Histogram> Histogram::Create(const Options& options) {
|
||||
return std::make_shared<Histogram>(HistogramPointer(histogram), options);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Copies the recorded data of `source` into `target`, which must have been
|
||||
// initialized with the same layout. The caller must hold a lock that prevents
|
||||
// `source` from being modified during the copy.
|
||||
void CopyRecordedData(hdr_histogram* target, const hdr_histogram* source) {
|
||||
CHECK_EQ(target->counts_len, source->counts_len);
|
||||
target->min_value = source->min_value;
|
||||
target->max_value = source->max_value;
|
||||
target->normalizing_index_offset = source->normalizing_index_offset;
|
||||
target->conversion_ratio = source->conversion_ratio;
|
||||
target->total_count = source->total_count;
|
||||
std::memcpy(target->counts,
|
||||
source->counts,
|
||||
source->counts_len * sizeof(*source->counts));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<Histogram> Histogram::Clone() const {
|
||||
// The layout is fixed when the histogram is created, so the copy can be
|
||||
// allocated without holding the lock.
|
||||
hdr_histogram* copy;
|
||||
if (hdr_init(histogram_->lowest_discernible_value,
|
||||
histogram_->highest_trackable_value,
|
||||
histogram_->significant_figures,
|
||||
©) != 0) {
|
||||
return {};
|
||||
}
|
||||
auto clone = std::make_shared<Histogram>(HistogramPointer(copy), Options{});
|
||||
|
||||
// Every member that holds recorded or statistical state must be copied
|
||||
// here. The recorded snapshot cache is not copied; the clone builds its own
|
||||
// on demand.
|
||||
RwLock::ScopedReadLock lock(mutex_);
|
||||
CopyRecordedData(clone->histogram_.get(), histogram_.get());
|
||||
clone->prev_ = prev_;
|
||||
clone->exceeds_ = exceeds_;
|
||||
clone->ewma_alpha_ = ewma_alpha_;
|
||||
clone->ewma_mean_ = ewma_mean_;
|
||||
clone->ewma_variance_ = ewma_variance_;
|
||||
clone->ewma_initialized_ = ewma_initialized_;
|
||||
clone->threshold_ = threshold_;
|
||||
clone->ewma_error_rate_ = ewma_error_rate_;
|
||||
return clone;
|
||||
}
|
||||
|
||||
void Histogram::MemoryInfo(MemoryTracker* tracker) const {
|
||||
tracker->TrackFieldWithSize("histogram", GetMemorySize());
|
||||
tracker->TrackFieldWithSize("qrde_snapshot",
|
||||
@@ -158,16 +203,7 @@ Histogram::RecordedSnapshotSource Histogram::GetRecordedSnapshotSource(
|
||||
return source;
|
||||
}
|
||||
|
||||
CHECK_EQ(source.histogram->counts_len, histogram_->counts_len);
|
||||
source.histogram->min_value = histogram_->min_value;
|
||||
source.histogram->max_value = histogram_->max_value;
|
||||
source.histogram->normalizing_index_offset =
|
||||
histogram_->normalizing_index_offset;
|
||||
source.histogram->conversion_ratio = histogram_->conversion_ratio;
|
||||
source.histogram->total_count = histogram_->total_count;
|
||||
std::memcpy(source.histogram->counts,
|
||||
histogram_->counts,
|
||||
histogram_->counts_len * sizeof(*histogram_->counts));
|
||||
CopyRecordedData(source.histogram.get(), histogram_.get());
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -1926,6 +1962,7 @@ void HistogramImpl::AddMethods(Isolate* isolate, Local<FunctionTemplate> tmpl) {
|
||||
GetEwmaErrorRate,
|
||||
&fast_get_ewma_error_rate_);
|
||||
SetProtoMethodNoSideEffect(isolate, tmpl, "export", DoExport);
|
||||
SetProtoMethodNoSideEffect(isolate, tmpl, "snapshot", DoSnapshot);
|
||||
SetFastMethod(isolate, instance, "reset", DoReset, &fast_reset_);
|
||||
}
|
||||
|
||||
@@ -1975,6 +2012,7 @@ void HistogramImpl::RegisterExternalReferences(
|
||||
registry->Register(GetEwmaStddev);
|
||||
registry->Register(GetEwmaErrorRate);
|
||||
registry->Register(DoExport);
|
||||
registry->Register(DoSnapshot);
|
||||
registry->Register(fast_get_ewma_mean_);
|
||||
registry->Register(fast_get_ewma_stddev_);
|
||||
registry->Register(fast_get_ewma_error_rate_);
|
||||
@@ -3108,6 +3146,17 @@ void HistogramImpl::DoImport(const FunctionCallbackInfo<Value>& args) {
|
||||
args.GetReturnValue().Set(obj);
|
||||
}
|
||||
|
||||
void HistogramImpl::DoSnapshot(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
|
||||
std::shared_ptr<Histogram> snapshot = (*histogram)->Clone();
|
||||
if (!snapshot) return THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
|
||||
BaseObjectPtr<HistogramBase> result =
|
||||
HistogramBase::Create(env, std::move(snapshot));
|
||||
if (result) args.GetReturnValue().Set(result->object());
|
||||
}
|
||||
|
||||
void HistogramImpl::GetPercentilesAt(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
|
||||
|
||||
@@ -61,6 +61,10 @@ class Histogram : public MemoryRetainer {
|
||||
// Factory method that returns nullptr on hdr_init failure.
|
||||
static std::shared_ptr<Histogram> Create(const Options& options);
|
||||
|
||||
// Returns an independent copy of this histogram's current state, or nullptr
|
||||
// if the copy cannot be allocated.
|
||||
std::shared_ptr<Histogram> Clone() const;
|
||||
|
||||
Histogram(HistogramPointer histogram, const Options& options);
|
||||
virtual ~Histogram() = default;
|
||||
|
||||
@@ -237,6 +241,7 @@ class HistogramImpl {
|
||||
static void GetEwmaErrorRate(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void DoExport(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void DoImport(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void DoSnapshot(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
static void FastReset(v8::Local<v8::Value> receiver);
|
||||
static double FastGetCount(v8::Local<v8::Value> receiver);
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const { setTimeout: delay } = require('timers/promises');
|
||||
const { inspect } = require('util');
|
||||
const {
|
||||
createHistogram,
|
||||
monitorEventLoopDelay,
|
||||
} = require('perf_hooks');
|
||||
|
||||
function assertSameState(actual, expected) {
|
||||
assert.strictEqual(actual.countBigInt, expected.countBigInt);
|
||||
assert.strictEqual(actual.minBigInt, expected.minBigInt);
|
||||
assert.strictEqual(actual.maxBigInt, expected.maxBigInt);
|
||||
assert.strictEqual(actual.exceedsBigInt, expected.exceedsBigInt);
|
||||
assert.strictEqual(actual.mean, expected.mean);
|
||||
assert.strictEqual(actual.stddev, expected.stddev);
|
||||
assert.strictEqual(actual.ewmaMean, expected.ewmaMean);
|
||||
assert.strictEqual(actual.ewmaStddev, expected.ewmaStddev);
|
||||
assert.strictEqual(actual.ewmaErrorRate, expected.ewmaErrorRate);
|
||||
assert.deepStrictEqual(actual.percentilesBigInt, expected.percentilesBigInt);
|
||||
// The export format also covers the layout, the raw min and max values,
|
||||
// every bucket count, and the EWMA configuration.
|
||||
assert.deepStrictEqual(actual.export(), expected.export());
|
||||
}
|
||||
|
||||
function assertReadOnly(snapshot) {
|
||||
assert.strictEqual(snapshot.constructor.name, 'Histogram');
|
||||
assert.strictEqual(inspect(snapshot, { depth: -1 }), '[Histogram]');
|
||||
for (const name of [
|
||||
'record',
|
||||
'recordDelta',
|
||||
'recordCorrected',
|
||||
'add',
|
||||
'subtract',
|
||||
'enable',
|
||||
'disable',
|
||||
]) {
|
||||
assert.strictEqual(snapshot[name], undefined);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const histogram = createHistogram({
|
||||
lowest: 1,
|
||||
highest: 1000,
|
||||
figures: 2,
|
||||
halfLife: 8,
|
||||
threshold: 50,
|
||||
});
|
||||
for (let i = 1; i <= 100; i++) histogram.record(i * 7);
|
||||
histogram.record(1e6);
|
||||
assert.strictEqual(histogram.exceeds, 1);
|
||||
assert.ok(histogram.ewmaErrorRate > 0);
|
||||
|
||||
const snapshot = histogram.snapshot();
|
||||
assert.notStrictEqual(snapshot, histogram);
|
||||
assertReadOnly(snapshot);
|
||||
assertSameState(snapshot, histogram);
|
||||
assert.strictEqual(histogram.ksTest(snapshot), 0);
|
||||
|
||||
// A snapshot can be snapshotted and cloned.
|
||||
const nested = snapshot.snapshot();
|
||||
assertReadOnly(nested);
|
||||
assertSameState(nested, snapshot);
|
||||
assertSameState(structuredClone(snapshot), snapshot);
|
||||
|
||||
// Changes to the source histogram do not change the snapshot.
|
||||
const exported = snapshot.export();
|
||||
histogram.record(3);
|
||||
histogram.recordCorrected(900, 100);
|
||||
histogram.record(1e6);
|
||||
assert.deepStrictEqual(snapshot.export(), exported);
|
||||
assert.strictEqual(snapshot.count, 100);
|
||||
assert.strictEqual(snapshot.exceeds, 1);
|
||||
|
||||
histogram.reset();
|
||||
assert.strictEqual(histogram.count, 0);
|
||||
assert.deepStrictEqual(snapshot.export(), exported);
|
||||
|
||||
// Resetting a snapshot does not change the source histogram or other
|
||||
// snapshots.
|
||||
histogram.record(5);
|
||||
const current = histogram.snapshot();
|
||||
snapshot.reset();
|
||||
assert.strictEqual(snapshot.count, 0);
|
||||
assert.strictEqual(histogram.count, 1);
|
||||
assert.strictEqual(current.count, 1);
|
||||
assert.deepStrictEqual(nested.export(), exported);
|
||||
}
|
||||
|
||||
{
|
||||
// Empty histograms, with and without EWMA state.
|
||||
for (const options of [undefined, { halfLife: 10, threshold: 1 }]) {
|
||||
const histogram = createHistogram(options);
|
||||
const snapshot = histogram.snapshot();
|
||||
assertReadOnly(snapshot);
|
||||
assert.strictEqual(snapshot.count, 0);
|
||||
assert.strictEqual(snapshot.minBigInt, 9223372036854775807n);
|
||||
assert.strictEqual(snapshot.maxBigInt, 0n);
|
||||
assertSameState(snapshot, histogram);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const histogram = createHistogram();
|
||||
assert.throws(() => histogram.snapshot.call({}), {
|
||||
code: 'ERR_INVALID_THIS',
|
||||
});
|
||||
}
|
||||
|
||||
async function testEventLoopDelay(options) {
|
||||
const histogram = monitorEventLoopDelay(options);
|
||||
histogram.enable();
|
||||
while (histogram.count < 3) await delay(1);
|
||||
|
||||
// Samples are only recorded while the event loop is running, so none can
|
||||
// be added while this code runs synchronously.
|
||||
const snapshot = histogram.snapshot();
|
||||
assertReadOnly(snapshot);
|
||||
assertSameState(snapshot, histogram);
|
||||
|
||||
const exported = snapshot.export();
|
||||
const count = histogram.count;
|
||||
while (histogram.count === count) await delay(1);
|
||||
assert.deepStrictEqual(snapshot.export(), exported);
|
||||
|
||||
histogram.disable();
|
||||
histogram.reset();
|
||||
assert.strictEqual(histogram.count, 0);
|
||||
assert.deepStrictEqual(snapshot.export(), exported);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await testEventLoopDelay({ resolution: 1 });
|
||||
await testEventLoopDelay({ samplePerIteration: true });
|
||||
})().then(common.mustCall());
|
||||
+1
@@ -41,6 +41,7 @@ declare namespace InternalPerformanceBinding {
|
||||
ewmaMean(): number;
|
||||
ewmaStddev(): number;
|
||||
ewmaErrorRate(): number;
|
||||
snapshot(): Histogram;
|
||||
}
|
||||
|
||||
interface ELDHistogram extends HistogramBase {
|
||||
|
||||
Reference in new issue
Block a user