perf_hooks: add histogram.diff() · nodejs/node@1fef8f5 · GitHub
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

‎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

0 commit comments

Comments
 (0)