Skip to content

Commit 72d1b40

Browse files
jasnelladuh95
authored andcommitted
perf_hooks: add CBOR export/import for histogram exchange
Provide a binary export/import mechanism for histograms. Since there is no standard interchange format for histograms, using CBOR is meant to make the format as platform/runtime agnostic as possible while producing a compact/efficient result. No new dependency is introduced, we just encode the CBOR directly. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode/Opus PR-URL: #65434 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3e4fc50 commit 72d1b40

6 files changed

Lines changed: 769 additions & 20 deletions

File tree

‎doc/api/perf_hooks.md‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,34 @@ added:
16351635

16361636
Returns a {RecordableHistogram}.
16371637

1638+
## `perf_hooks.importHistogram(data)`
1639+
1640+
<!-- YAML
1641+
added: REPLACEME
1642+
-->
1643+
1644+
* `data` {Uint8Array} A CBOR-encoded histogram previously produced by
1645+
[`histogram.export()`][].
1646+
* Returns: {RecordableHistogram}
1647+
1648+
Reconstructs a histogram from a CBOR-encoded `Uint8Array`. The returned
1649+
histogram is a full {RecordableHistogram} with all bucket data, configuration,
1650+
and EWMA state restored. New values can be recorded into it.
1651+
1652+
```js
1653+
const { createHistogram, importHistogram } = require('node:perf_hooks');
1654+
1655+
const h = createHistogram();
1656+
for (let i = 1; i <= 1000; i++) h.record(i);
1657+
1658+
// Serialize and reconstruct
1659+
const data = h.export();
1660+
const h2 = importHistogram(data);
1661+
1662+
console.log(h2.count); // 1000
1663+
console.log(h2.percentile(99)); // Same as h.percentile(99)
1664+
```
1665+
16381666
## `perf_hooks.eventLoopUtilization([utilization1[, utilization2]])`
16391667

16401668
<!-- YAML
@@ -1961,6 +1989,43 @@ added:
19611989
The number of times the event loop delay exceeded the maximum 1 hour event
19621990
loop delay threshold.
19631991

1992+
### `histogram.export()`
1993+
1994+
<!-- YAML
1995+
added: REPLACEME
1996+
-->
1997+
1998+
* Returns: {Uint8Array}
1999+
2000+
Serializes the histogram to a [CBOR][]-encoded (RFC 8949) `Uint8Array`
2001+
suitable for transmission or persistent storage. The encoding uses a
2002+
delta-encoded sparse representation of the bucket counts, so the output size
2003+
scales with the number of distinct recorded values rather than the total
2004+
bucket count.
2005+
2006+
The output includes all histogram configuration, bucket data, and EWMA
2007+
state (when enabled). It can be reconstructed into a new histogram using
2008+
[`perf_hooks.importHistogram()`][].
2009+
2010+
The CBOR payload is a map with integer keys:
2011+
2012+
| Key | Type | Field |
2013+
| --- | ------- | --------------------------------------------- |
2014+
| 0 | uint | Format version (currently 1) |
2015+
| 1 | uint | Lowest discernible value |
2016+
| 2 | uint | Highest trackable value |
2017+
| 3 | uint | Significant figures |
2018+
| 4 | uint | Total count |
2019+
| 5 | uint | Min value |
2020+
| 6 | uint | Max value |
2021+
| 7 | uint | Normalizing index offset |
2022+
| 8 | float64 | Conversion ratio |
2023+
| 9 | uint | Counts array length |
2024+
| 10 | array | Delta-encoded sparse counts `[delta, c, ...]` |
2025+
| 11 | map | EWMA state (omitted when disabled) |
2026+
2027+
Any standard CBOR decoder can parse the output.
2028+
19642029
### `histogram.ewmaMean`
19652030

19662031
<!-- YAML
@@ -2836,6 +2901,7 @@ dns.promises.resolve('localhost');
28362901
```
28372902

28382903
[Async Hooks]: async_hooks.md
2904+
[CBOR]: https://www.rfc-editor.org/rfc/rfc8949
28392905
[Cliff's delta]: https://en.wikipedia.org/wiki/Effect_size#Cliff's_delta
28402906
[Cohen's d]: https://en.wikipedia.org/wiki/Effect_size#Cohen's_d
28412907
[Fetch Response Body Info]: https://fetch.spec.whatwg.org/#response-body-info
@@ -2850,7 +2916,9 @@ dns.promises.resolve('localhost');
28502916
[Worker threads]: worker_threads.md#worker-threads
28512917
[`'exit'`]: process.md#event-exit
28522918
[`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options
2919+
[`histogram.export()`]: #histogramexport
28532920
[`perf_hooks.eventLoopUtilization()`]: #perf_hookseventlooputilizationutilization1-utilization2
2921+
[`perf_hooks.importHistogram()`]: #perf_hooksimporthistogramdata
28542922
[`perf_hooks.monitorEventLoopDelay()`]: #perf_hooksmonitoreventloopdelayoptions
28552923
[`perf_hooks.timerify()`]: #perf_hookstimerifyfn-options
28562924
[`process.hrtime()`]: process.md#processhrtimetime

‎lib/internal/histogram.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const {
3838
validateObject,
3939
} = require('internal/validators');
4040

41+
const {
42+
isUint8Array,
43+
} = require('internal/util/types');
44+
4145
const kDestroy = Symbol('kDestroy');
4246
const kHandle = Symbol('kHandle');
4347
const kRecordable = Symbol('kRecordable');
@@ -213,6 +217,18 @@ class Histogram {
213217
return this[kHandle]?.exceedsBigInt();
214218
}
215219

220+
/**
221+
* Serializes the histogram to a CBOR-encoded Uint8Array suitable for
222+
* transmission or storage. The data can be reconstructed into a new
223+
* histogram using `importHistogram()`.
224+
* @returns {Uint8Array}
225+
*/
226+
export() {
227+
if (!isHistogram(this))
228+
throw new ERR_INVALID_THIS('Histogram');
229+
return this[kHandle]?.export();
230+
}
231+
216232
/**
217233
* Returns the exponentially weighted moving average of recorded values.
218234
* Only active when the histogram was created with a `halfLife` option.
@@ -755,6 +771,18 @@ function createHistogram(options = kEmptyObject) {
755771
new _Histogram(lowest, highest, figures, halfLife, threshold));
756772
}
757773

774+
/**
775+
* Reconstructs a histogram from a CBOR-encoded Uint8Array previously
776+
* produced by `histogram.export()`.
777+
* @param {Uint8Array} data
778+
* @returns {RecordableHistogram}
779+
*/
780+
function importHistogram(data) {
781+
if (!isUint8Array(data))
782+
throw new ERR_INVALID_ARG_TYPE('data', 'Uint8Array', data);
783+
return createRecordableHistogram(_Histogram.import(data));
784+
}
785+
758786
module.exports = {
759787
Histogram,
760788
RecordableHistogram,
@@ -765,4 +793,5 @@ module.exports = {
765793
kHandle,
766794
kSkipThrow,
767795
createHistogram,
796+
importHistogram,
768797
};

‎lib/perf_hooks.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525

2626
const {
2727
createHistogram,
28+
importHistogram,
2829
} = require('internal/histogram');
2930

3031
const monitorEventLoopDelay = require('internal/perf/event_loop_delay');
@@ -43,6 +44,7 @@ module.exports = {
4344
eventLoopUtilization,
4445
timerify,
4546
createHistogram,
47+
importHistogram,
4648
performance,
4749
};
4850

0 commit comments

Comments
 (0)