Skip to content

Commit 32878db

Browse files
fibibotlunadogbotbartlomieju
authored
feat(ext/node): add createHistogram to node:perf_hooks (#34003)
Adds the missing `createHistogram(options)` export to `node:perf_hooks`, along with the `Histogram` / `RecordableHistogram` classes Node.js exposes from this module. The following Node compat tests are now enabled and passing: - `parallel/test-perf-hooks-histogram.js` - `parallel/test-perf-hooks-histogram-fast-calls.js` - `parallel/test-perf-hooks-timerify-histogram-async.mjs` - `parallel/test-perf-hooks-timerify-histogram-sync.mjs` Closes #34001 --------- Co-authored-by: lunadogbot <lunadogbot@users.noreply.github.com> Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
1 parent 52627b5 commit 32878db

10 files changed

Lines changed: 739 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ fqdn = "0.5"
223223
futures = "0.3.31"
224224
glob = "0.3.1"
225225
h2 = "0.4.6"
226+
hdrhistogram = "7.5"
226227
hickory-proto = "0.25.2"
227228
hickory-resolver = { version = "0.25.2", features = ["tokio", "serde"] }
228229
hickory-server = "0.25.2"

cli/tsc/dts/node/perf_hooks.d.cts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ declare module "perf_hooks" {
756756
* The maximum recorded event loop delay.
757757
* v17.4.0, v16.14.0
758758
*/
759-
readonly maxBigInt: number;
759+
readonly maxBigInt: bigint;
760760
/**
761761
* The mean of the recorded event loop delays.
762762
* @since v11.10.0

ext/node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ deno_process.workspace = true
4141
deno_tls.workspace = true
4242
deno_whoami.workspace = true
4343
h2.workspace = true
44+
hdrhistogram.workspace = true
4445
http.workspace = true
4546
http-body-util.workspace = true
4647
hyper.workspace = true

ext/node/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ deno_core::extension!(deno_node,
391391
],
392392
objects = [
393393
ops::perf_hooks::EldHistogram,
394+
ops::perf_hooks::BaseHistogram,
394395
ops::handle_wrap::AsyncWrap,
395396
ops::handle_wrap::HandleWrap,
396397
ops::wasi::WasiContext,

ext/node/ops/perf_hooks.rs

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ use std::cell::RefCell;
55

66
use deno_core::GarbageCollected;
77
use deno_core::op2;
8+
use hdrhistogram::Histogram;
9+
10+
const EMPTY_HISTOGRAM_MIN: u64 = i64::MAX as u64;
811

912
#[derive(Debug, thiserror::Error, deno_error::JsError)]
1013
pub enum PerfHooksError {
1114
#[class(generic)]
1215
#[error(transparent)]
1316
TokioEld(#[from] tokio_eld::Error),
17+
#[class(generic)]
18+
#[error(transparent)]
19+
HistogramCreation(#[from] hdrhistogram::errors::CreationError),
1420
}
1521

1622
pub struct EldHistogram {
@@ -147,3 +153,280 @@ impl EldHistogram {
147153
self.eld.borrow().stdev()
148154
}
149155
}
156+
157+
// Backs the user-facing `RecordableHistogram` returned by
158+
// `perf_hooks.createHistogram()`. Wraps an `hdrhistogram::Histogram<u64>`
159+
// configured with caller-supplied bounds, plus the bookkeeping needed for
160+
// `recordDelta()` and the `exceeds` counter (incremented when a recorded
161+
// value overflows the histogram's `highest` bound).
162+
pub struct BaseHistogram {
163+
inner: RefCell<Histogram<u64>>,
164+
highest: u64,
165+
exceeds: Cell<u64>,
166+
added_out_of_range: Cell<u64>,
167+
prev_delta_ns: Cell<Option<u64>>,
168+
}
169+
170+
// SAFETY: we're sure this can be GCed
171+
unsafe impl GarbageCollected for BaseHistogram {
172+
fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}
173+
174+
fn get_name(&self) -> &'static std::ffi::CStr {
175+
c"BaseHistogram"
176+
}
177+
}
178+
179+
fn now_ns() -> u64 {
180+
// Match Node's `process.hrtime` clock domain — monotonic nanoseconds.
181+
use std::time::Instant;
182+
thread_local! {
183+
static ORIGIN: Instant = Instant::now();
184+
}
185+
ORIGIN.with(|origin| origin.elapsed().as_nanos() as u64)
186+
}
187+
188+
#[op2]
189+
impl BaseHistogram {
190+
// Creates a `RecordableHistogram` with the given bounds and significant
191+
// figures. Mirrors the behavior of Node's `createHistogram(options)`.
192+
//
193+
// Caller is responsible for validating bounds; this just forwards them to
194+
// `hdrhistogram::Histogram::new_with_bounds`.
195+
#[constructor]
196+
#[cppgc]
197+
pub fn new(
198+
#[bigint] lowest: u64,
199+
#[bigint] highest: u64,
200+
#[smi] figures: u32,
201+
) -> Result<BaseHistogram, PerfHooksError> {
202+
let inner =
203+
Histogram::<u64>::new_with_bounds(lowest, highest, figures as u8)?;
204+
Ok(BaseHistogram {
205+
inner: RefCell::new(inner),
206+
highest,
207+
exceeds: Cell::new(0),
208+
added_out_of_range: Cell::new(0),
209+
prev_delta_ns: Cell::new(None),
210+
})
211+
}
212+
213+
// Records a value into the histogram. If the value exceeds the configured
214+
// `highest`, increments the `exceeds` counter instead of erroring.
215+
#[fast]
216+
fn record(&self, #[bigint] value: u64) {
217+
if value > self.highest {
218+
self.exceeds.set(self.exceeds.get().saturating_add(1));
219+
return;
220+
}
221+
let mut h = self.inner.borrow_mut();
222+
if h.record(value).is_err() {
223+
self.exceeds.set(self.exceeds.get().saturating_add(1));
224+
}
225+
}
226+
227+
// Records the nanoseconds elapsed since the previous call to recordDelta.
228+
// The first call seeds the timestamp without recording (matches Node).
229+
#[fast]
230+
fn record_delta(&self) {
231+
let now = now_ns();
232+
if let Some(prev) = self.prev_delta_ns.get() {
233+
let delta = now.saturating_sub(prev);
234+
if delta > self.highest {
235+
self.exceeds.set(self.exceeds.get().saturating_add(1));
236+
self.prev_delta_ns.set(Some(now));
237+
return;
238+
}
239+
let mut h = self.inner.borrow_mut();
240+
if h.record(delta).is_err() {
241+
self.exceeds.set(self.exceeds.get().saturating_add(1));
242+
}
243+
}
244+
self.prev_delta_ns.set(Some(now));
245+
}
246+
247+
// Adds counts from another histogram into this one.
248+
#[fast]
249+
fn add(&self, #[cppgc] other: &BaseHistogram) {
250+
let other_h = other.inner.borrow();
251+
let mut h = self.inner.borrow_mut();
252+
let mut added_out_of_range = self.added_out_of_range.get();
253+
for v in other_h.iter_recorded() {
254+
if v.value_iterated_to() > self.highest
255+
|| h
256+
.record_n(v.value_iterated_to(), v.count_at_value())
257+
.is_err()
258+
{
259+
added_out_of_range =
260+
added_out_of_range.saturating_add(v.count_at_value());
261+
}
262+
}
263+
self.added_out_of_range.set(added_out_of_range);
264+
self
265+
.exceeds
266+
.set(self.exceeds.get().saturating_add(other.exceeds.get()));
267+
}
268+
269+
#[fast]
270+
fn reset(&self) {
271+
self.inner.borrow_mut().reset();
272+
self.exceeds.set(0);
273+
self.added_out_of_range.set(0);
274+
self.prev_delta_ns.set(None);
275+
}
276+
277+
#[fast]
278+
#[number]
279+
fn percentile(&self, percentile: f64) -> u64 {
280+
self.inner.borrow().value_at_percentile(percentile)
281+
}
282+
283+
#[fast]
284+
#[bigint]
285+
fn percentile_big_int(&self, percentile: f64) -> u64 {
286+
self.inner.borrow().value_at_percentile(percentile)
287+
}
288+
289+
// Returns the percentile distribution as a flat `[percentile, value, ...]`
290+
// array. The JS layer turns it into a `Map`. We iterate the recorded values
291+
// and emit one entry per distinct value.
292+
//
293+
// Values are bounded by `highest` (validated to fit in a JS safe integer by
294+
// the createHistogram caller), so emitting them as f64 is lossless.
295+
#[serde]
296+
fn percentiles(&self) -> Vec<f64> {
297+
let h = self.inner.borrow();
298+
let mut out = Vec::new();
299+
if h.is_empty() {
300+
out.push(100.0);
301+
out.push(0.0);
302+
return out;
303+
}
304+
out.push(0.0);
305+
out.push(h.min() as f64);
306+
if h.len() > 1 {
307+
let max = h.max();
308+
let mut percentile = 50.0;
309+
while percentile < 100.0 {
310+
let value = h.value_at_percentile(percentile);
311+
out.push(percentile);
312+
out.push(value as f64);
313+
if value >= max {
314+
break;
315+
}
316+
percentile += (100.0 - percentile) / 2.0;
317+
}
318+
}
319+
out.push(100.0);
320+
out.push(h.max() as f64);
321+
out
322+
}
323+
324+
// Same shape as `percentiles`; the JS layer re-wraps the value entries as
325+
// BigInt when exposing them through `percentilesBigInt`.
326+
#[serde]
327+
fn percentiles_big_int(&self) -> Vec<f64> {
328+
let h = self.inner.borrow();
329+
let mut out = Vec::new();
330+
if h.is_empty() {
331+
out.push(100.0);
332+
out.push(0.0);
333+
return out;
334+
}
335+
out.push(0.0);
336+
out.push(h.min() as f64);
337+
if h.len() > 1 {
338+
let max = h.max();
339+
let mut percentile = 50.0;
340+
while percentile < 100.0 {
341+
let value = h.value_at_percentile(percentile);
342+
out.push(percentile);
343+
out.push(value as f64);
344+
if value >= max {
345+
break;
346+
}
347+
percentile += (100.0 - percentile) / 2.0;
348+
}
349+
}
350+
out.push(100.0);
351+
out.push(h.max() as f64);
352+
out
353+
}
354+
355+
#[getter]
356+
#[number]
357+
fn count(&self) -> u64 {
358+
self
359+
.inner
360+
.borrow()
361+
.len()
362+
.saturating_add(self.added_out_of_range.get())
363+
}
364+
365+
#[getter]
366+
#[bigint]
367+
fn count_big_int(&self) -> u64 {
368+
self
369+
.inner
370+
.borrow()
371+
.len()
372+
.saturating_add(self.added_out_of_range.get())
373+
}
374+
375+
#[getter]
376+
#[number]
377+
fn min(&self) -> u64 {
378+
let h = self.inner.borrow();
379+
if h.is_empty() {
380+
EMPTY_HISTOGRAM_MIN
381+
} else {
382+
h.min()
383+
}
384+
}
385+
386+
#[getter]
387+
#[bigint]
388+
fn min_big_int(&self) -> u64 {
389+
let h = self.inner.borrow();
390+
if h.is_empty() {
391+
EMPTY_HISTOGRAM_MIN
392+
} else {
393+
h.min()
394+
}
395+
}
396+
397+
#[getter]
398+
#[number]
399+
fn max(&self) -> u64 {
400+
self.inner.borrow().max()
401+
}
402+
403+
#[getter]
404+
#[bigint]
405+
fn max_big_int(&self) -> u64 {
406+
self.inner.borrow().max()
407+
}
408+
409+
#[getter]
410+
fn mean(&self) -> f64 {
411+
let h = self.inner.borrow();
412+
if h.is_empty() { f64::NAN } else { h.mean() }
413+
}
414+
415+
#[getter]
416+
fn stddev(&self) -> f64 {
417+
let h = self.inner.borrow();
418+
if h.is_empty() { f64::NAN } else { h.stdev() }
419+
}
420+
421+
#[getter]
422+
#[number]
423+
fn exceeds(&self) -> u64 {
424+
self.exceeds.get()
425+
}
426+
427+
#[getter]
428+
#[bigint]
429+
fn exceeds_big_int(&self) -> u64 {
430+
self.exceeds.get()
431+
}
432+
}

0 commit comments

Comments
 (0)