From db47d0a2f8380d82ebc6ec8c454e12947a189c9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:09:59 +0200 Subject: [PATCH 1/7] build(deps-dev): bump semver from 7.8.4 to 7.8.5 in the patch-updates group across 1 directory (#355) Bumps the patch-updates group with 1 update in the / directory: [semver](https://github.com/npm/node-semver). Updates `semver` from 7.8.4 to 7.8.5 - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v7.8.4...v7.8.5) --- updated-dependencies: - dependency-name: semver dependency-version: 7.8.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: patch-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84fbda1a..7dd8939d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "mocha": "^11.7.6", "nan": "^2.27.0", "nyc": "^18.0.0", - "semver": "^7.8.1", + "semver": "^7.8.5", "sinon": "^22.0.0", "source-map-support": "^0.5.21", "tmp": "0.2.7", @@ -5641,9 +5641,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index c0f35521..b0183af7 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "mocha": "^11.7.6", "nan": "^2.27.0", "nyc": "^18.0.0", - "semver": "^7.8.1", + "semver": "^7.8.5", "sinon": "^22.0.0", "source-map-support": "^0.5.21", "tmp": "0.2.7", From 865c96cca024e700fbe4ec23465deb2232a6d207 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Tue, 7 Jul 2026 19:51:45 +0200 Subject: [PATCH 2/7] fix: opt out of node-gyp on install via gypfile:false (#367) Dropping the no-op install script in #363 stopped Yarn Berry's YN0007 warning, but it let npm's publish-time normalization take over: with binding.gyp present in the dev tree (and excluded from the tarball) and no install/preinstall script, npm synthesizes "gypfile": true and "install": "node-gyp rebuild" into the published manifest. Consumers on npm then run node-gyp rebuild against a binding.gyp that isn't in the package and the install fails, which shipped broken in 5.16.0. Setting "gypfile": false suppresses that implicit hook while keeping the manifest free of lifecycle scripts, so both npm and Yarn Berry install the prebuilt binaries without a build step. The regression test now also pins gypfile:false alongside the no-lifecycle-scripts assertion. Fixes: https://github.com/DataDog/pprof-nodejs/pull/364#pullrequestreview-4638668974 --- package.json | 1 + ts/test/test-no-build-scripts.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b0183af7..509d4b37 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "main": "out/src/index.js", "types": "out/src/index.d.ts", + "gypfile": false, "scripts": { "build:asan": "node-gyp configure build --jobs=max --address_sanitizer", "build:tsan": "node-gyp configure build --jobs=max --thread_sanitizer", diff --git a/ts/test/test-no-build-scripts.ts b/ts/test/test-no-build-scripts.ts index 4aee2b12..cb7ba35f 100644 --- a/ts/test/test-no-build-scripts.ts +++ b/ts/test/test-no-build-scripts.ts @@ -3,12 +3,23 @@ import * as fs from 'fs'; import * as path from 'path'; describe('package manifest', () => { + const manifest = path.join(__dirname, '..', '..', 'package.json'); + const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')); + it('declares no npm build lifecycle scripts (Yarn Berry YN0007)', () => { - const manifest = path.join(__dirname, '..', '..', 'package.json'); - const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')); const scripts = pkg.scripts || {}; const hooks = ['preinstall', 'install', 'postinstall']; const present = hooks.filter(name => scripts[name] !== undefined); assert.deepStrictEqual(present, []); }); + + it('opts out of node-gyp on install via gypfile:false (npm implicit rebuild)', () => { + // binding.gyp lives in the dev tree (excluded from the published tarball). + // Without gypfile:false, npm's publish-time normalization synthesizes + // "gypfile": true and "install": "node-gyp rebuild" into the registry + // manifest, so consumers run node-gyp rebuild against a missing + // binding.gyp and the install fails. Pinning gypfile:false suppresses + // that hook while keeping the manifest free of lifecycle scripts. + assert.strictEqual(pkg.gypfile, false); + }); }); From 1d5936e35e6677f2b372123259e60f690ef9d491 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:43:47 +0000 Subject: [PATCH 3/7] build(deps-dev): bump the minor-updates group with 2 updates (#368) Bumps the minor-updates group with 2 updates: [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n) and [nan](https://github.com/nodejs/nan). Updates `eslint-plugin-n` from 18.1.0 to 18.2.1 - [Release notes](https://github.com/eslint-community/eslint-plugin-n/releases) - [Changelog](https://github.com/eslint-community/eslint-plugin-n/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint-community/eslint-plugin-n/compare/v18.1.0...v18.2.1) Updates `nan` from 2.27.0 to 2.28.0 - [Changelog](https://github.com/nodejs/nan/blob/main/CHANGELOG.md) - [Commits](https://github.com/nodejs/nan/compare/v2.27.0...v2.28.0) --- updated-dependencies: - dependency-name: eslint-plugin-n dependency-version: 18.2.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-updates - dependency-name: nan dependency-version: 2.28.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7dd8939d..2196e4e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,11 @@ "clang-format": "^1.8.0", "codecov": "^3.8.3", "deep-copy": "^1.4.2", - "eslint-plugin-n": "^18.0.1", + "eslint-plugin-n": "^18.2.1", "gts": "^7.0.0", "js-green-licenses": "^4.0.0", "mocha": "^11.7.6", - "nan": "^2.27.0", + "nan": "^2.28.0", "nyc": "^18.0.0", "semver": "^7.8.5", "sinon": "^22.0.0", @@ -2245,9 +2245,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-18.1.0.tgz", - "integrity": "sha512-hkUm9EtnFV2h2fE16jNVUfCVUqvPzI7fGLsFdun5lFt/pbmf2kCgDx6ymi9rx+NCUSggBmurJCZOfG20JBs/kg==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-18.2.1.tgz", + "integrity": "sha512-aO3C9//yq8JIvYOi/T+jPvcZ9hZzpwzbR8esrYpFtgE9vpbyM8kn42AQOtIqYspVmpaSWr8X+nrlQuAJYxXAaw==", "dev": true, "license": "MIT", "dependencies": { @@ -4290,9 +4290,9 @@ "license": "ISC" }, "node_modules/nan": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", - "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 509d4b37..0b99b577 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,11 @@ "clang-format": "^1.8.0", "codecov": "^3.8.3", "deep-copy": "^1.4.2", - "eslint-plugin-n": "^18.0.1", + "eslint-plugin-n": "^18.2.1", "gts": "^7.0.0", "js-green-licenses": "^4.0.0", "mocha": "^11.7.6", - "nan": "^2.27.0", + "nan": "^2.28.0", "nyc": "^18.0.0", "semver": "^7.8.5", "sinon": "^22.0.0", From 6df84aa0c84562cd58abde3af9b21f3de1e7124d Mon Sep 17 00:00:00 2001 From: Ilyas Shabi Date: Fri, 10 Jul 2026 13:28:52 +0200 Subject: [PATCH 4/7] [heap] Restore original heap size (#371) * restore original heap size --- bindings/profilers/heap.cc | 5 ++ ts/test/oom-restore-heap-limit.ts | 85 +++++++++++++++++++++++++++++++ ts/test/test-heap-profiler.ts | 30 +++++++++++ 3 files changed, 120 insertions(+) create mode 100644 ts/test/oom-restore-heap-limit.ts diff --git a/bindings/profilers/heap.cc b/bindings/profilers/heap.cc index f7aaf774..e4ee8cf3 100644 --- a/bindings/profilers/heap.cc +++ b/bindings/profilers/heap.cc @@ -95,6 +95,11 @@ struct HeapProfilerState { } if (isolate) { isolate->AddNearHeapLimitCallback(&NearHeapLimit, nullptr); + // Restore the original heap limit once live old-generation usage falls + // below 90% of the original limit. The threshold controls when V8 + // restores the limit, not the restored limit size. + constexpr double kHeapLimitRestoreThreshold = 0.90; + isolate->AutomaticallyRestoreInitialHeapLimit(kHeapLimitRestoreThreshold); callbackInstalled = true; } } diff --git a/ts/test/oom-restore-heap-limit.ts b/ts/test/oom-restore-heap-limit.ts new file mode 100644 index 00000000..c053a03c --- /dev/null +++ b/ts/test/oom-restore-heap-limit.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as v8 from 'v8'; + +import {heap} from '../src/index'; + +const MB = 1024 * 1024; +const LIMIT_TOLERANCE = 16 * MB; +const CHUNK_SIZE = 4 * MB; +const gc = (global as typeof globalThis & {gc?: () => void}).gc; + +function heapLimit() { + return v8.getHeapStatistics().heap_size_limit; +} + +function allocateChunk() { + const chunk = new Array(CHUNK_SIZE / 8); + for (let i = 0; i < chunk.length; i++) { + chunk[i] = i + 0.1; + } + return chunk; +} + +async function main() { + if (!gc) { + throw new Error('This test must be run with --expose-gc.'); + } + + heap.start(MB, 64); + try { + heap.monitorOutOfMemory(64 * MB, 1, false); + + const initialLimit = heapLimit(); + const retained: number[][] = []; + + while ( + heapLimit() <= initialLimit + LIMIT_TOLERANCE && + retained.length < 64 + ) { + retained.push(allocateChunk()); + } + + const increasedLimit = heapLimit(); + if (increasedLimit <= initialLimit + LIMIT_TOLERANCE) { + throw new Error(`Heap limit did not increase from ${initialLimit}.`); + } + + retained.length = 0; + for (let i = 0; i < 100; i++) { + gc(); + if (heapLimit() <= initialLimit + LIMIT_TOLERANCE) { + return; + } + await new Promise(resolve => setTimeout(resolve, 20)); + } + + throw new Error( + `Heap limit increased to ${increasedLimit} but did not restore to ` + + `${initialLimit}.`, + ); + } finally { + heap.stop(); + } +} + +main().catch(err => { + console.error(err.stack || err.message); + process.exitCode = 1; +}); diff --git a/ts/test/test-heap-profiler.ts b/ts/test/test-heap-profiler.ts index 34d58ebe..2bb2c3e4 100644 --- a/ts/test/test-heap-profiler.ts +++ b/ts/test/test-heap-profiler.ts @@ -331,6 +331,36 @@ describe('HeapProfiler', () => { }); describe('OOMMonitoring', () => { + it('should restore heap limit after v8 recovers from OOM', async function () { + this.timeout(30000); + + const proc = fork(path.join(__dirname, 'oom-restore-heap-limit.js'), { + execArgv: ['--expose-gc', '--max-old-space-size=64'], + silent: true, + }); + let output = ''; + + proc.stdout?.on('data', chunk => { + output += chunk; + }); + proc.stderr?.on('data', chunk => { + output += chunk; + }); + + await new Promise((resolve, reject) => { + proc.on('error', reject); + proc.on('exit', code => { + if (code === 0) { + resolve(); + } else { + reject( + new Error(`oom-restore-heap-limit exited with ${code}\n${output}`), + ); + } + }); + }); + }); + it('should call external process upon OOM', async function () { // this test is very slow on some configs (asan/valgrind) this.timeout(20000); From 3dbd0813934cb327d230f8d7b39d9b2cd8fb44c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:09:23 +0200 Subject: [PATCH 5/7] build(deps-dev): bump @types/node from 25.9.2 to 26.1.0 (#370) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.2 to 26.1.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 26.1.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2196e4e8..04269759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.1", - "@types/node": "25.9.2", + "@types/node": "26.1.0", "@types/semver": "^7.5.8", "@types/sinon": "^21.0.1", "@types/tmp": "^0.2.3", @@ -962,13 +962,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", - "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@types/normalize-package-data": { @@ -6322,9 +6322,9 @@ } }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 0b99b577..a67e13ae 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.1", - "@types/node": "25.9.2", + "@types/node": "26.1.0", "@types/semver": "^7.5.8", "@types/sinon": "^21.0.1", "@types/tmp": "^0.2.3", From 4eb840849d5cb5829a3178637d65dfd85cc81e0c Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Tue, 14 Jul 2026 14:00:17 +0200 Subject: [PATCH 6/7] otel-thread-ctx: address review feedback from #347 (#366) * Address review feedback from PR #347 Four independently-flagged concerns: - Accept optional third `attributes` arg in ThreadContext constructor. The TS ThreadContextCtor.new type declares `attributes` optional, but the C++ side hard-errored on `args.Length() != 3`. Loosen to accept 2 or 3 args; EncodeAttrs already handled undefined/null attrs_val correctly. - Fold `cleanup_registered` into `undefined_addr`. Uses the field's zero / non-zero state as the 'already-registered' flag: it starts at zero, ResetDiscoveryStruct clears it back to zero (so a re-init on the same thread would re-register), and any real V8 undefined singleton address is non-zero. Removes the separate thread_local static. - Coerce attribute values to strings in a pre-pass in EncodeAttrs before writing to the output buffer. Value->ToString may execute user JS (custom toString methods) which could re-enter into the ThreadContext via appendAttributes and interleave with our writes. Separating the coerce phase from the encode phase keeps the encode phase re-entrancy-free. - Make the 'enterWithContext attaches the record to the current async scope' test callback `async` and `await tcRun(...)`. Previously the callback returned a promise via `void tcRun(...)` and the promise's inner `.then` assertion could fire an unhandled rejection instead of a test failure. * Explicitly guard against reentrant Append calls. Also undo the two-phase encoding loop that was the previous (insufficient) attempt at fixing the reentrancy. --- bindings/otel-thread-ctx.cc | 56 +++++++++++++++++++++++++++------ ts/test/test-otel-thread-ctx.ts | 4 +-- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/bindings/otel-thread-ctx.cc b/bindings/otel-thread-ctx.cc index db76c16b..574a87cf 100644 --- a/bindings/otel-thread-ctx.cc +++ b/bindings/otel-thread-ctx.cc @@ -31,6 +31,8 @@ #include "otel-thread-ctx.hh" +#include "defer.hh" + #include #include #include @@ -225,6 +227,16 @@ class CtxWrap : public ObjectWrap { // attribute had to be dropped because it would have pushed attrs_data // past MAX_ATTRS_DATA_SIZE. bool truncated_; + // Reentrancy guard for Append(). EncodeAttrs calls ToString on each + // attribute value, which can execute user JS (e.g. a custom + // `toString`) that in turn calls `appendAttributes` on the same + // ThreadContext. A reentrant Append would mutate attrs_data_size out + // from under the outer call's `current_used` snapshot, causing the + // outer memcpy to overwrite the reentrant call's bytes and the outer + // attrs_data_size write to shrink the record. We reject the reentrant + // call instead. New() doesn't need the guard because a freshly constructed + // CtxWrap isn't observable to JS until New() returns. + bool encoding_; }; // Pin the offset of `record_` — the field the reader walks to from the @@ -247,7 +259,10 @@ CtxWrap::~CtxWrap() { } CtxWrap::CtxWrap(OtelThreadCtxRecord* record, size_t capacity, bool truncated) - : record_(record), capacity_(capacity), truncated_(truncated) {} + : record_(record), + capacity_(capacity), + truncated_(truncated), + encoding_(false) {} // Copy exactly `expected_bytes` bytes out of a JS Uint8Array (or subclass // such as Buffer) into `out`. Returns false if the value isn't a @@ -289,6 +304,7 @@ bool CtxWrap::EncodeAttrs(Isolate* isolate, isolate->ThrowError("attributes array length must not exceed 256"); return false; } + // Reserve a conservative upper bound; reallocations are cheap but // unnecessary for the typical small attribute set. out->reserve(out->size() + n * 4); @@ -297,7 +313,6 @@ bool CtxWrap::EncodeAttrs(Isolate* isolate, if (!attrs->Get(context, i).ToLocal(&val_val)) return false; // null / undefined / array holes mean "no value at this key index". if (val_val->IsUndefined() || val_val->IsNull()) continue; - Local v; if (!val_val->ToString(context).ToLocal(&v)) return false; #if NODE_MAJOR_VERSION >= 24 @@ -356,9 +371,10 @@ void CtxWrap::New(const FunctionCallbackInfo& args) { isolate->ThrowError("ThreadContext must be called with `new`"); return; } - if (args.Length() != 3) { + if (args.Length() < 2 || args.Length() > 3) { isolate->ThrowError( - "ThreadContext expects 3 arguments: traceId, spanId, attributes"); + "ThreadContext expects 2 or 3 arguments: traceId, spanId, " + "attributes?"); return; } @@ -438,6 +454,22 @@ void CtxWrap::Append(const FunctionCallbackInfo& args) { return; } + // Reject reentrant Append on the same wrap. EncodeAttrs' `ToString` + // below can execute user JS, and if that JS calls `appendAttributes` + // on this same ThreadContext, the reentrant call would grow + // attrs_data_size out from under the outer call's `current_used` + // snapshot, causing the outer memcpy to overwrite the reentrant call's + // bytes and the outer attrs_data_size write to shrink the record. + if (self->encoding_) { + isolate->ThrowError( + "reentrant appendAttributes on the same ThreadContext is not allowed"); + return; + } + self->encoding_ = true; + defer { + self->encoding_ = false; + }; + const size_t current_used = self->record_->attrs_data_size; std::vector appended; bool truncated = false; @@ -581,8 +613,6 @@ void ResetDiscoveryStruct(void* /*arg*/) { } void StoreAls(const FunctionCallbackInfo& args) { - static thread_local bool cleanup_registered = false; - Isolate* isolate = args.GetIsolate(); if (!args[0]->IsObject()) { isolate->ThrowError("First argument must be the AsyncLocalStorage object."); @@ -604,15 +634,21 @@ void StoreAls(const FunctionCallbackInfo& args) { // addon compiles on the older Node versions the package supports. otel_thread_ctx_nodejs_v1.cped_slot = nullptr; #endif + // `undefined_addr == 0` doubles as the "not yet initialized on this + // isolate" flag: it starts at zero (thread-local zero-init), any real + // V8 undefined singleton address is non-zero, and ResetDiscoveryStruct + // clears it back to zero — so a subsequent StoreAls (e.g. isolate + // tear-down then re-init on the same thread) re-registers the cleanup + // hook. Register BEFORE the write so the flag transition is the last + // observable step. + if (otel_thread_ctx_nodejs_v1.undefined_addr == 0) { + node::AddEnvironmentCleanupHook(isolate, ResetDiscoveryStruct, nullptr); + } // Cache the per-isolate undefined singleton's tagged address. Undefined // is a read-only-roots heap object, never moves, so a cached numeric // address is fine — no Global<> tracking needed. otel_thread_ctx_nodejs_v1.undefined_addr = reinterpret_cast(*v8::Undefined(isolate)); - if (!cleanup_registered) { - node::AddEnvironmentCleanupHook(isolate, ResetDiscoveryStruct, nullptr); - cleanup_registered = true; - } } // Without a function that explicitly reads the TLS variable, on x86 the diff --git a/ts/test/test-otel-thread-ctx.ts b/ts/test/test-otel-thread-ctx.ts index adad1841..070cf5c0 100644 --- a/ts/test/test-otel-thread-ctx.ts +++ b/ts/test/test-otel-thread-ctx.ts @@ -424,8 +424,8 @@ function captureBytes(opts: { }); describe('enterWithContext', () => { - it('attaches the record to the current async scope', () => { - void tcRun( + it('attaches the record to the current async scope', async () => { + await tcRun( () => { strictAssert.deepEqual( decodeHeader(_currentRecordBytes()!).spanId, From 8c1ae304431a447a142086171e2c262309072ccf Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Wed, 15 Jul 2026 14:15:43 +0200 Subject: [PATCH 7/7] v5.16.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04269759..92895625 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@datadog/pprof", - "version": "5.16.0", + "version": "5.16.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@datadog/pprof", - "version": "5.16.0", + "version": "5.16.1", "license": "Apache-2.0", "dependencies": { "node-gyp-build": "^4.8.4", diff --git a/package.json b/package.json index a67e13ae..6d60302a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/pprof", - "version": "5.16.0", + "version": "5.16.1", "description": "pprof support for Node.js", "repository": { "type": "git",