Skip to content

Commit 1fef8f5

Browse files
jasnelladuh95
authored andcommitted
perf_hooks: add histogram.diff()
Getting the values recorded during an interval requires calling `reset()`, which removes them for every other user of the histogram, or copying and subtracting histograms, which silently produces a wrong result when the source was reset in between. Add `histogram.diff(other)`, which returns a new read-only `Histogram` containing the values recorded after `other`, an earlier snapshot of the histogram, was taken. Neither histogram is changed. Unlike `subtract()`, it verifies that both histograms have the same layout, and it throws instead of clamping when `other` contains values that the histogram does not. Add `histogram.resetCount`, the number of calls to `reset()` and `subtract()`, which `snapshot()` copies. `diff()` throws when the counts differ, so a reset between two snapshots is detected even when every bucket has since grown past its previous count. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: OpenCode PR-URL: #66099 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent a6dfb3b commit 1fef8f5

7 files changed

Lines changed: 392 additions & 8 deletions

File tree

‎doc/api/perf_hooks.md‎

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,6 +2185,52 @@ added: v26.8.0
21852185
Returns the number of recorded values that fall within the equivalent
21862186
value range of the given value.
21872187

2188+
### `histogram.diff(other)`
2189+
2190+
<!-- YAML
2191+
added: REPLACEME
2192+
-->
2193+
2194+
* `other` {Histogram} An earlier snapshot of this histogram.
2195+
* Returns: {Histogram}
2196+
2197+
Returns a new {Histogram} containing the values recorded in this histogram after
2198+
`other` was taken. Neither histogram is changed. To get the values recorded
2199+
during each interval without calling `reset()`, compute each difference from a
2200+
snapshot and keep that snapshot as the baseline for the next interval:
2201+
2202+
```js
2203+
const { monitorEventLoopDelay } = require('node:perf_hooks');
2204+
2205+
const histogram = monitorEventLoopDelay();
2206+
histogram.enable();
2207+
let previous = histogram.snapshot();
2208+
2209+
setInterval(() => {
2210+
const current = histogram.snapshot();
2211+
// After a reset, use everything recorded since the reset.
2212+
const delta = current.resetCount === previous.resetCount ?
2213+
current.diff(previous) : current;
2214+
console.log(delta.percentile(99));
2215+
previous = current;
2216+
}, 10_000);
2217+
```
2218+
2219+
The `count`, `exceeds`, and bucket counts of the returned histogram are the
2220+
differences between the two histograms. Its `min` and `max` are computed from
2221+
the buckets of the difference, it has no EWMA state, and its `resetCount` is
2222+
`0`.
2223+
2224+
This method throws:
2225+
2226+
* `ERR_INVALID_ARG_VALUE` if `other` has a different `lowest`, `highest`, or
2227+
`figures` configuration.
2228+
* `ERR_INVALID_STATE` if values have been removed from this histogram since
2229+
`other` was taken, which is the case when the `resetCount` of the two
2230+
histograms differs.
2231+
* `ERR_INVALID_ARG_VALUE` if `other` contains values that are not in this
2232+
histogram, for example because the histograms were passed in the wrong order.
2233+
21882234
### `histogram.exceeds`
21892235

21902236
<!-- YAML
@@ -2640,7 +2686,21 @@ boundaries are equal has an infinite density.
26402686
added: v11.10.0
26412687
-->
26422688

2643-
Resets the collected histogram data.
2689+
Resets the collected histogram data and increments `histogram.resetCount`.
2690+
2691+
### `histogram.resetCount`
2692+
2693+
<!-- YAML
2694+
added: REPLACEME
2695+
-->
2696+
2697+
* Type: {number}
2698+
2699+
The number of times values have been removed from this histogram by `reset()`
2700+
or, for a {RecordableHistogram}, `subtract()`. A snapshot has the `resetCount`
2701+
of its source at the time it was taken, so comparing the `resetCount` of two
2702+
snapshots shows whether the source was reset between them. See
2703+
[`histogram.diff()`][].
26442704

26452705
### `histogram.skewness`
26462706

@@ -2838,7 +2898,7 @@ added: v26.8.0
28382898

28392899
Subtracts the values of `other` from this histogram. Both histograms should
28402900
have compatible configurations. Bucket counts that would become negative
2841-
are clamped to zero.
2901+
are clamped to zero. Increments `histogram.resetCount`.
28422902

28432903
## Class: `SlidingWindowHistogram`
28442904

@@ -3317,6 +3377,7 @@ dns.promises.resolve('localhost');
33173377
[Worker threads]: worker_threads.md#worker-threads
33183378
[`'exit'`]: process.md#event-exit
33193379
[`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options
3380+
[`histogram.diff()`]: #histogramdiffother
33203381
[`histogram.export()`]: #histogramexport
33213382
[`perf_hooks.createSlidingWindowHistogram()`]: #perf_hookscreateslidingwindowhistogramoptions
33223383
[`perf_hooks.eventLoopUtilization()`]: #perf_hookseventlooputilizationutilization1-utilization2

‎lib/internal/histogram.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,18 @@ class Histogram {
679679
this[kHandle]?.reset();
680680
}
681681

682+
/**
683+
* The number of times values have been removed from the histogram by
684+
* `reset()` or `subtract()`.
685+
* @readonly
686+
* @type {number}
687+
*/
688+
get resetCount() {
689+
if (!isHistogram(this))
690+
throw new ERR_INVALID_THIS('Histogram');
691+
return this[kHandle]?.resetCount();
692+
}
693+
682694
/**
683695
* Returns a new, independent histogram containing a copy of this
684696
* histogram's current state. Values cannot be recorded into the returned
@@ -691,6 +703,20 @@ class Histogram {
691703
return new ClonedHistogram(this[kHandle].snapshot());
692704
}
693705

706+
/**
707+
* Returns a new histogram containing the values recorded in this histogram
708+
* after `other`, an earlier snapshot of it, was taken.
709+
* @param {Histogram} other
710+
* @returns {Histogram}
711+
*/
712+
diff(other) {
713+
if (!isHistogram(this))
714+
throw new ERR_INVALID_THIS('Histogram');
715+
if (!isHistogram(other))
716+
throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other);
717+
return new ClonedHistogram(this[kHandle].diff(other[kHandle]));
718+
}
719+
694720
[kClone]() {
695721
const handle = this[kHandle];
696722
return {

‎src/histogram-inl.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ void Histogram::Reset() {
4242
RwLock::ScopedWriteLock lock(mutex_);
4343
hdr_reset(histogram_.get());
4444
InvalidateRecordedSnapshot();
45+
reset_count_++;
4546
exceeds_ = 0;
4647
prev_ = 0;
4748
ewma_mean_ = 0;
@@ -90,6 +91,11 @@ size_t Histogram::Exceeds() const {
9091
return exceeds_;
9192
}
9293

94+
uint64_t Histogram::ResetCount() const {
95+
RwLock::ScopedReadLock lock(mutex_);
96+
return reset_count_;
97+
}
98+
9399
int64_t Histogram::Min() const {
94100
RwLock::ScopedReadLock lock(mutex_);
95101
return hdr_min(histogram_.get());

‎src/histogram.cc‎

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,22 @@ void CopyRecordedData(hdr_histogram* target, const hdr_histogram* source) {
9393
}
9494
} // namespace
9595

96-
std::shared_ptr<Histogram> Histogram::Clone() const {
97-
// The layout is fixed when the histogram is created, so the copy can be
98-
// allocated without holding the lock.
99-
hdr_histogram* copy;
96+
std::shared_ptr<Histogram> Histogram::CreateWithSameLayout() const {
97+
// The layout is fixed when the histogram is created, so it can be read
98+
// without holding the lock.
99+
hdr_histogram* histogram;
100100
if (hdr_init(histogram_->lowest_discernible_value,
101101
histogram_->highest_trackable_value,
102102
histogram_->significant_figures,
103-
&copy) != 0) {
103+
&histogram) != 0) {
104104
return {};
105105
}
106-
auto clone = std::make_shared<Histogram>(HistogramPointer(copy), Options{});
106+
return std::make_shared<Histogram>(HistogramPointer(histogram), Options{});
107+
}
108+
109+
std::shared_ptr<Histogram> Histogram::Clone() const {
110+
std::shared_ptr<Histogram> clone = CreateWithSameLayout();
111+
if (!clone) return {};
107112

108113
// Every member that holds recorded or statistical state must be copied
109114
// here. The recorded snapshot cache is not copied; the clone builds its own
@@ -112,6 +117,7 @@ std::shared_ptr<Histogram> Histogram::Clone() const {
112117
CopyRecordedData(clone->histogram_.get(), histogram_.get());
113118
clone->prev_ = prev_;
114119
clone->exceeds_ = exceeds_;
120+
clone->reset_count_ = reset_count_;
115121
clone->ewma_alpha_ = ewma_alpha_;
116122
clone->ewma_mean_ = ewma_mean_;
117123
clone->ewma_variance_ = ewma_variance_;
@@ -121,6 +127,59 @@ std::shared_ptr<Histogram> Histogram::Clone() const {
121127
return clone;
122128
}
123129

130+
std::shared_ptr<Histogram> Histogram::Diff(const Histogram& other,
131+
DiffError* error) const {
132+
// Counts are subtracted index by index, so both histograms must map values
133+
// to the same indexes. None of these fields change after creation.
134+
if (!IsCompatible(other) || histogram_->normalizing_index_offset !=
135+
other.histogram_->normalizing_index_offset) {
136+
*error = DiffError::kIncompatible;
137+
return {};
138+
}
139+
140+
std::shared_ptr<Histogram> diff = CreateWithSameLayout();
141+
if (!diff) {
142+
*error = DiffError::kOutOfMemory;
143+
return {};
144+
}
145+
146+
// Only the recorded values and the exceeds count carry over. EWMA and timing
147+
// state cannot be subtracted.
148+
uint64_t reset_count;
149+
{
150+
RwLock::ScopedReadLock lock(mutex_);
151+
CopyRecordedData(diff->histogram_.get(), histogram_.get());
152+
diff->exceeds_ = exceeds_;
153+
reset_count = reset_count_;
154+
}
155+
156+
// `diff` is not shared yet, so only the lock of `other` is needed from here
157+
// on. Never holding both locks at once avoids lock ordering issues.
158+
RwLock::ScopedReadLock lock(other.mutex_);
159+
if (reset_count != other.reset_count_) {
160+
*error = DiffError::kReset;
161+
return {};
162+
}
163+
if (diff->exceeds_ < other.exceeds_) {
164+
*error = DiffError::kNotEarlier;
165+
return {};
166+
}
167+
168+
hdr_histogram* target = diff->histogram_.get();
169+
const hdr_histogram* source = other.histogram_.get();
170+
for (int32_t i = 0; i < target->counts_len; i++) {
171+
if (target->counts[i] < source->counts[i]) {
172+
*error = DiffError::kNotEarlier;
173+
return {};
174+
}
175+
target->counts[i] -= source->counts[i];
176+
}
177+
diff->exceeds_ -= other.exceeds_;
178+
hdr_reset_internal_counters(target);
179+
*error = DiffError::kNone;
180+
return diff;
181+
}
182+
124183
void Histogram::MemoryInfo(MemoryTracker* tracker) const {
125184
tracker->TrackFieldWithSize("histogram", GetMemorySize());
126185
tracker->TrackFieldWithSize("qrde_snapshot",
@@ -313,6 +372,7 @@ double Histogram::Subtract(const Histogram& other) {
313372
}
314373
hdr_reset_internal_counters(histogram_.get());
315374
InvalidateRecordedSnapshot();
375+
reset_count_++;
316376
exceeds_ = (exceeds_ > other.exceeds_) ? exceeds_ - other.exceeds_ : 0;
317377
return static_cast<double>(dropped);
318378
};
@@ -1963,6 +2023,8 @@ void HistogramImpl::AddMethods(Isolate* isolate, Local<FunctionTemplate> tmpl) {
19632023
&fast_get_ewma_error_rate_);
19642024
SetProtoMethodNoSideEffect(isolate, tmpl, "export", DoExport);
19652025
SetProtoMethodNoSideEffect(isolate, tmpl, "snapshot", DoSnapshot);
2026+
SetProtoMethodNoSideEffect(isolate, tmpl, "diff", DoDiff);
2027+
SetProtoMethodNoSideEffect(isolate, tmpl, "resetCount", GetResetCount);
19662028
SetFastMethod(isolate, instance, "reset", DoReset, &fast_reset_);
19672029
}
19682030

@@ -2013,6 +2075,8 @@ void HistogramImpl::RegisterExternalReferences(
20132075
registry->Register(GetEwmaErrorRate);
20142076
registry->Register(DoExport);
20152077
registry->Register(DoSnapshot);
2078+
registry->Register(DoDiff);
2079+
registry->Register(GetResetCount);
20162080
registry->Register(fast_get_ewma_mean_);
20172081
registry->Register(fast_get_ewma_stddev_);
20182082
registry->Register(fast_get_ewma_error_rate_);
@@ -3173,6 +3237,39 @@ void HistogramImpl::DoSnapshot(const FunctionCallbackInfo<Value>& args) {
31733237
if (result) args.GetReturnValue().Set(result->object());
31743238
}
31753239

3240+
void HistogramImpl::DoDiff(const FunctionCallbackInfo<Value>& args) {
3241+
Environment* env = Environment::GetCurrent(args);
3242+
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
3243+
HistogramImpl* other = HistogramImpl::FromJSObject(args[0]);
3244+
Histogram::DiffError error;
3245+
std::shared_ptr<Histogram> diff =
3246+
(*histogram)->Diff(*(other->histogram()), &error);
3247+
switch (error) {
3248+
case Histogram::DiffError::kNone:
3249+
break;
3250+
case Histogram::DiffError::kOutOfMemory:
3251+
return THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
3252+
case Histogram::DiffError::kIncompatible:
3253+
return THROW_ERR_INVALID_ARG_VALUE(
3254+
env, "other must have the same configuration as the histogram");
3255+
case Histogram::DiffError::kReset:
3256+
return THROW_ERR_INVALID_STATE(
3257+
env, "Values were removed from the histogram after other was taken");
3258+
case Histogram::DiffError::kNotEarlier:
3259+
return THROW_ERR_INVALID_ARG_VALUE(
3260+
env, "other contains values that are not in the histogram");
3261+
}
3262+
3263+
BaseObjectPtr<HistogramBase> result =
3264+
HistogramBase::Create(env, std::move(diff));
3265+
if (result) args.GetReturnValue().Set(result->object());
3266+
}
3267+
3268+
void HistogramImpl::GetResetCount(const FunctionCallbackInfo<Value>& args) {
3269+
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());
3270+
args.GetReturnValue().Set(static_cast<double>((*histogram)->ResetCount()));
3271+
}
3272+
31763273
void HistogramImpl::GetPercentilesAt(const FunctionCallbackInfo<Value>& args) {
31773274
Environment* env = Environment::GetCurrent(args);
31783275
HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This());

‎src/histogram.h‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ class Histogram : public MemoryRetainer {
6565
// if the copy cannot be allocated.
6666
std::shared_ptr<Histogram> Clone() const;
6767

68+
enum class DiffError {
69+
kNone,
70+
kOutOfMemory,
71+
// `other` has a different layout.
72+
kIncompatible,
73+
// Values were removed from this histogram after `other` was taken.
74+
kReset,
75+
// `other` contains values that this histogram does not.
76+
kNotEarlier,
77+
};
78+
79+
// Returns a new histogram containing the values recorded in this histogram
80+
// after `other`, an earlier copy of it, was taken. Returns nullptr and sets
81+
// `error` if the difference cannot be computed.
82+
std::shared_ptr<Histogram> Diff(const Histogram& other,
83+
DiffError* error) const;
84+
6885
Histogram(HistogramPointer histogram, const Options& options);
6986
virtual ~Histogram() = default;
7087

@@ -80,6 +97,7 @@ class Histogram : public MemoryRetainer {
8097
inline int64_t Percentile(double percentile) const;
8198
inline size_t Exceeds() const;
8299
inline size_t Count() const;
100+
inline uint64_t ResetCount() const;
83101

84102
inline uint64_t RecordDelta();
85103

@@ -165,10 +183,13 @@ class Histogram : public MemoryRetainer {
165183
inline void UpdateEwma(double value);
166184
inline void InvalidateRecordedSnapshot();
167185
size_t GetCachedRecordedSnapshotMemorySize() const;
186+
std::shared_ptr<Histogram> CreateWithSameLayout() const;
168187

169188
HistogramPointer histogram_;
170189
uint64_t prev_ = 0;
171190
size_t exceeds_ = 0;
191+
// Incremented whenever recorded values are removed by Reset() or Subtract().
192+
uint64_t reset_count_ = 0;
172193

173194
// EWMA state (active when ewma_alpha_ > 0)
174195
double ewma_alpha_ = 0;
@@ -242,6 +263,8 @@ class HistogramImpl {
242263
static void DoExport(const v8::FunctionCallbackInfo<v8::Value>& args);
243264
static void DoImport(const v8::FunctionCallbackInfo<v8::Value>& args);
244265
static void DoSnapshot(const v8::FunctionCallbackInfo<v8::Value>& args);
266+
static void DoDiff(const v8::FunctionCallbackInfo<v8::Value>& args);
267+
static void GetResetCount(const v8::FunctionCallbackInfo<v8::Value>& args);
245268

246269
static void FastReset(v8::Local<v8::Value> receiver);
247270
static double FastGetCount(v8::Local<v8::Value> receiver);

0 commit comments

Comments
 (0)