Commit b9e257f5cb0 for nodejs

commit b9e257f5cb0c7b8067869a1dffdf63e8eaf134f9
Author: James M Snell <jasnell@gmail.com>
Date:   Fri Sep 18 02:11:33 2026 +0000

    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>

diff --git a/benchmark/perf_hooks/histogram-snapshot.js b/benchmark/perf_hooks/histogram-snapshot.js
new file mode 100644
index 00000000000..79b13fecb0f
--- /dev/null
+++ b/benchmark/perf_hooks/histogram-snapshot.js
@@ -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);
+}
diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md
index fa7b9f7ba5a..b4a9304dc27 100644
--- a/doc/api/perf_hooks.md
+++ b/doc/api/perf_hooks.md
@@ -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
diff --git a/lib/internal/histogram.js b/lib/internal/histogram.js
index 1cc787404fd..8983289e075 100644
--- a/lib/internal/histogram.js
+++ b/lib/internal/histogram.js
@@ -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 {
diff --git a/src/histogram.cc b/src/histogram.cc
index 1b9560bea95..765397be2e9 100644
--- a/src/histogram.cc
+++ b/src/histogram.cc
@@ -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,
+               &copy) != 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());
diff --git a/src/histogram.h b/src/histogram.h
index bc72fa36e10..f526e9cb1cc 100644
--- a/src/histogram.h
+++ b/src/histogram.h
@@ -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);
diff --git a/test/parallel/test-perf-hooks-histogram-snapshot.js b/test/parallel/test-perf-hooks-histogram-snapshot.js
new file mode 100644
index 00000000000..ab554373bbc
--- /dev/null
+++ b/test/parallel/test-perf-hooks-histogram-snapshot.js
@@ -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());
diff --git a/typings/internalBinding/performance.d.ts b/typings/internalBinding/performance.d.ts
index da9af2df2cb..043be9e7118 100644
--- a/typings/internalBinding/performance.d.ts
+++ b/typings/internalBinding/performance.d.ts
@@ -41,6 +41,7 @@ declare namespace InternalPerformanceBinding {
     ewmaMean(): number;
     ewmaStddev(): number;
     ewmaErrorRate(): number;
+    snapshot(): Histogram;
   }

   interface ELDHistogram extends HistogramBase {