From aa67c74051eb125b82423b4394932f2ccfb934ab Mon Sep 17 00:00:00 2001 From: Svetlana Brennan <50715937+svetlanabrennan@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:47:27 -0600 Subject: [PATCH 1/9] feat: Added heap profiler --- THIRD_PARTY_NOTICES.md | 243 +++++++++++++++++++++++++++++--- lib/profiling/index.js | 5 + lib/profiling/profilers/heap.js | 42 ++++++ package.json | 3 +- third_party_manifest.json | 142 ++++++++++--------- 5 files changed, 353 insertions(+), 82 deletions(-) create mode 100644 lib/profiling/profilers/heap.js diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 8fc1b7fdba..eb8c09be9d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -81,6 +81,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic **[optionalDependencies](#optionalDependencies)** +* [@datadog/pprof](#datadogpprof) * [@newrelic/fn-inspect](#newrelicfn-inspect) * [@newrelic/native-metrics](#newrelicnative-metrics) * [@prisma/prisma-fmt-wasm](#prismaprisma-fmt-wasm) @@ -304,7 +305,7 @@ This product includes source derived from [@apm-js-collab/tracing-hooks](https:/ ### @grpc/grpc-js -This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.14.3](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.14.3)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.14.3/LICENSE): +This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.13.3](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.13.3)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.13.3/LICENSE): ``` Apache License @@ -722,7 +723,7 @@ This product includes source derived from [@grpc/proto-loader](https://github.co ### @newrelic/security-agent -This product includes source derived from [@newrelic/security-agent](https://github.com/newrelic/csec-node-agent) ([v3.0.0](https://github.com/newrelic/csec-node-agent/tree/v3.0.0)), distributed under the [UNKNOWN License](https://github.com/newrelic/csec-node-agent/blob/v3.0.0/LICENSE): +This product includes source derived from [@newrelic/security-agent](https://github.com/newrelic/csec-node-agent) ([v3.0.1](https://github.com/newrelic/csec-node-agent/tree/v3.0.1)), distributed under the [UNKNOWN License](https://github.com/newrelic/csec-node-agent/blob/v3.0.1/LICENSE): ``` ## New Relic Software License v1.0 @@ -1186,7 +1187,7 @@ This product includes source derived from [@opentelemetry/api](https://github.co ### @opentelemetry/core -This product includes source derived from [@opentelemetry/core](https://github.com/open-telemetry/opentelemetry-js) ([v2.2.0](https://github.com/open-telemetry/opentelemetry-js/tree/v2.2.0)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v2.2.0/LICENSE): +This product includes source derived from [@opentelemetry/core](https://github.com/open-telemetry/opentelemetry-js) ([v2.0.1](https://github.com/open-telemetry/opentelemetry-js/tree/v2.0.1)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v2.0.1/LICENSE): ``` Apache License @@ -1604,7 +1605,7 @@ This product includes source derived from [@opentelemetry/exporter-metrics-otlp- ### @opentelemetry/resources -This product includes source derived from [@opentelemetry/resources](https://github.com/open-telemetry/opentelemetry-js) ([v2.2.0](https://github.com/open-telemetry/opentelemetry-js/tree/v2.2.0)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v2.2.0/LICENSE): +This product includes source derived from [@opentelemetry/resources](https://github.com/open-telemetry/opentelemetry-js) ([v2.0.1](https://github.com/open-telemetry/opentelemetry-js/tree/v2.0.1)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v2.0.1/LICENSE): ``` Apache License @@ -2022,7 +2023,7 @@ This product includes source derived from [@opentelemetry/sdk-logs](https://gith ### @opentelemetry/sdk-metrics -This product includes source derived from [@opentelemetry/sdk-metrics](https://github.com/open-telemetry/opentelemetry-js) ([v2.2.0](https://github.com/open-telemetry/opentelemetry-js/tree/v2.2.0)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v2.2.0/LICENSE): +This product includes source derived from [@opentelemetry/sdk-metrics](https://github.com/open-telemetry/opentelemetry-js) ([v2.0.1](https://github.com/open-telemetry/opentelemetry-js/tree/v2.0.1)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v2.0.1/LICENSE): ``` Apache License @@ -2231,7 +2232,7 @@ This product includes source derived from [@opentelemetry/sdk-metrics](https://g ### @opentelemetry/sdk-trace-base -This product includes source derived from [@opentelemetry/sdk-trace-base](https://github.com/open-telemetry/opentelemetry-js) ([v2.2.0](https://github.com/open-telemetry/opentelemetry-js/tree/v2.2.0)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v2.2.0/LICENSE): +This product includes source derived from [@opentelemetry/sdk-trace-base](https://github.com/open-telemetry/opentelemetry-js) ([v2.0.1](https://github.com/open-telemetry/opentelemetry-js/tree/v2.0.1)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v2.0.1/LICENSE): ``` Apache License @@ -2529,7 +2530,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ### import-in-the-middle -This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.15.0](https://github.com/nodejs/import-in-the-middle/tree/v1.15.0)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.15.0/LICENSE): +This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.13.2](https://github.com/nodejs/import-in-the-middle/tree/v1.13.2)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.13.2/LICENSE): ``` Apache License @@ -2904,7 +2905,7 @@ SOFTWARE. ### semver -This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.7.3](https://github.com/npm/node-semver/tree/v7.7.3)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.7.3/LICENSE): +This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.7.2](https://github.com/npm/node-semver/tree/v7.7.2)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.7.2/LICENSE): ``` The ISC License @@ -2960,7 +2961,7 @@ SOFTWARE. ### @aws-sdk/client-s3 -This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.958.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.958.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.958.0/LICENSE): +This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.808.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.808.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.808.0/LICENSE): ``` Apache License @@ -3169,7 +3170,7 @@ This product includes source derived from [@aws-sdk/client-s3](https://github.co ### @aws-sdk/s3-request-presigner -This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.958.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.958.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.958.0/LICENSE): +This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.808.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.808.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.808.0/LICENSE): ``` Apache License @@ -4567,7 +4568,7 @@ THE SOFTWARE. ### aws-sdk -This product includes source derived from [aws-sdk](https://github.com/aws/aws-sdk-js) ([v2.1693.0](https://github.com/aws/aws-sdk-js/tree/v2.1693.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js/blob/v2.1693.0/LICENSE.txt): +This product includes source derived from [aws-sdk](https://github.com/aws/aws-sdk-js) ([v2.1692.0](https://github.com/aws/aws-sdk-js/tree/v2.1692.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js/blob/v2.1692.0/LICENSE.txt): ``` @@ -4968,7 +4969,7 @@ SOFTWARE. ### eslint-plugin-jsdoc -This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v50.8.0](https://github.com/gajus/eslint-plugin-jsdoc/tree/v50.8.0)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v50.8.0/LICENSE): +This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v50.6.14](https://github.com/gajus/eslint-plugin-jsdoc/tree/v50.6.14)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v50.6.14/LICENSE): ``` Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/) @@ -5000,7 +5001,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### eslint -This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v9.39.2](https://github.com/eslint/eslint/tree/v9.39.2)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v9.39.2/LICENSE): +This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v9.26.0](https://github.com/eslint/eslint/tree/v9.26.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v9.26.0/LICENSE): ``` Copyright OpenJS Foundation and other contributors, @@ -5027,7 +5028,7 @@ THE SOFTWARE. ### express -This product includes source derived from [express](https://github.com/expressjs/express) ([v5.2.1](https://github.com/expressjs/express/tree/v5.2.1)), distributed under the [MIT License](https://github.com/expressjs/express/blob/v5.2.1/LICENSE): +This product includes source derived from [express](https://github.com/expressjs/express) ([v5.1.0](https://github.com/expressjs/express/tree/v5.1.0)), distributed under the [MIT License](https://github.com/expressjs/express/blob/v5.1.0/LICENSE): ``` (The MIT License) @@ -5163,7 +5164,7 @@ SOFTWARE. ### jsdoc -This product includes source derived from [jsdoc](https://github.com/jsdoc/jsdoc) ([v4.0.5](https://github.com/jsdoc/jsdoc/tree/v4.0.5)), distributed under the [Apache-2.0 License](https://github.com/jsdoc/jsdoc/blob/v4.0.5/LICENSE.md): +This product includes source derived from [jsdoc](https://github.com/jsdoc/jsdoc) ([v4.0.4](https://github.com/jsdoc/jsdoc/tree/v4.0.4)), distributed under the [Apache-2.0 License](https://github.com/jsdoc/jsdoc/blob/v4.0.4/LICENSE.md): ``` # License @@ -5529,7 +5530,7 @@ SOFTWARE. ### protobufjs -This product includes source derived from [protobufjs](https://github.com/protobufjs/protobuf.js) ([v7.5.4](https://github.com/protobufjs/protobuf.js/tree/v7.5.4)), distributed under the [BSD-3-Clause License](https://github.com/protobufjs/protobuf.js/blob/v7.5.4/LICENSE): +This product includes source derived from [protobufjs](https://github.com/protobufjs/protobuf.js) ([v7.5.3](https://github.com/protobufjs/protobuf.js/tree/v7.5.3)), distributed under the [BSD-3-Clause License](https://github.com/protobufjs/protobuf.js/blob/v7.5.3/LICENSE): ``` This license applies to all parts of protobuf.js except those files @@ -5749,6 +5750,216 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## optionalDependencies +### @datadog/pprof + +This product includes source derived from [@datadog/pprof](https://github.com/DataDog/pprof-nodejs) ([v5.13.3](https://github.com/DataDog/pprof-nodejs/tree/v5.13.3)), distributed under the [Apache-2.0 License](https://github.com/DataDog/pprof-nodejs/blob/v5.13.3/LICENSE): + +``` + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +``` + ### @newrelic/fn-inspect This product includes source derived from [@newrelic/fn-inspect](https://github.com/newrelic-forks/node-fn-inspect) ([v4.4.0](https://github.com/newrelic-forks/node-fn-inspect/tree/v4.4.0)), distributed under the [MIT License](https://github.com/newrelic-forks/node-fn-inspect/blob/v4.4.0/LICENSE): diff --git a/lib/profiling/index.js b/lib/profiling/index.js index 16307b0508..21497535f9 100644 --- a/lib/profiling/index.js +++ b/lib/profiling/index.js @@ -5,6 +5,7 @@ 'use strict' const defaultLogger = require('#agentlib/logger.js').child({ component: 'profiling-manager' }) +const HeapProfiler = require('./profilers/heap') class ProfilingManager { constructor(agent, { logger = defaultLogger } = {}) { @@ -15,6 +16,10 @@ class ProfilingManager { // current no-op until we built out the profilers register() { + if (this.config.profiling.includes('heap')) { + const heapProfiler = new HeapProfiler() + this.profilers.push(heapProfiler) + } } start() { diff --git a/lib/profiling/profilers/heap.js b/lib/profiling/profilers/heap.js new file mode 100644 index 0000000000..7d1d78b82f --- /dev/null +++ b/lib/profiling/profilers/heap.js @@ -0,0 +1,42 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const BaseProfiler = require('../profilers/base') + +class HeapProfiler extends BaseProfiler { + constructor() { + super() + this.name = 'heap' + // tmp hard code these values for now + this.heapSampleIntervalBytes = 524288 + this.heapSampleStackDepth = 64 + this.pprofData = null + } + + get pprof() { + if (!this._pprof) { + this._pprof = require('@datadog/pprof') + } + return this._pprof + } + + start() { + this.pprof.heap.start(this.heapSampleIntervalBytes, this.heapSampleStackDepth) + } + + stop() { + this.pprof.heap.stop() + } + + async collect() { + const profile = await this.pprof.heap.profile() + const buf = await this.pprof.encode(profile) + this.pprofData = buf + } +} + +module.exports = HeapProfiler diff --git a/package.json b/package.json index 2fd3dc1c07..7a24a654a6 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,8 @@ "optionalDependencies": { "@newrelic/fn-inspect": "^4.4.0", "@newrelic/native-metrics": "^12.0.0", - "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085" + "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085", + "@datadog/pprof": "^5.13.3" }, "devDependencies": { "@aws-sdk/client-s3": "^3.556.0", diff --git a/third_party_manifest.json b/third_party_manifest.json index 5ef5c05170..9b32859f9f 100644 --- a/third_party_manifest.json +++ b/third_party_manifest.json @@ -1,9 +1,21 @@ { - "lastUpdated": "Tue Jan 06 2026 16:21:17 GMT+0530 (India Standard Time)", + "lastUpdated": "Wed Feb 11 2026 12:47:30 GMT-0600 (Central Standard Time)", "projectName": "New Relic Node Agent", "projectUrl": "https://github.com/newrelic/node-newrelic", "includeOptDeps": true, "optionalDependencies": { + "@datadog/pprof@5.13.3": { + "name": "@datadog/pprof", + "version": "5.13.3", + "range": "^5.13.3", + "licenses": "Apache-2.0", + "repoUrl": "https://github.com/DataDog/pprof-nodejs", + "versionedRepoUrl": "https://github.com/DataDog/pprof-nodejs/tree/v5.13.3", + "licenseFile": "node_modules/@datadog/pprof/LICENSE", + "licenseUrl": "https://github.com/DataDog/pprof-nodejs/blob/v5.13.3/LICENSE", + "licenseTextSource": "file", + "publisher": "Google Inc." + }, "@newrelic/fn-inspect@4.4.0": { "name": "@newrelic/fn-inspect", "version": "4.4.0", @@ -55,15 +67,15 @@ "licenseUrl": "https://github.com/apm-js-collab/tracing-hooks/blob/v0.3.1/LICENSE", "licenseTextSource": "file" }, - "@grpc/grpc-js@1.14.3": { + "@grpc/grpc-js@1.13.3": { "name": "@grpc/grpc-js", - "version": "1.14.3", + "version": "1.13.3", "range": "^1.13.2", "licenses": "Apache-2.0", "repoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", - "versionedRepoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.14.3", + "versionedRepoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.13.3", "licenseFile": "node_modules/@grpc/grpc-js/LICENSE", - "licenseUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.14.3/LICENSE", + "licenseUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.13.3/LICENSE", "licenseTextSource": "file", "publisher": "Google Inc." }, @@ -79,15 +91,15 @@ "licenseTextSource": "file", "publisher": "Google Inc." }, - "@newrelic/security-agent@3.0.0": { + "@newrelic/security-agent@3.0.1": { "name": "@newrelic/security-agent", - "version": "3.0.0", + "version": "3.0.1", "range": "^3.0.0", "licenses": "UNKNOWN", "repoUrl": "https://github.com/newrelic/csec-node-agent", - "versionedRepoUrl": "https://github.com/newrelic/csec-node-agent/tree/v3.0.0", + "versionedRepoUrl": "https://github.com/newrelic/csec-node-agent/tree/v3.0.1", "licenseFile": "node_modules/@newrelic/security-agent/LICENSE", - "licenseUrl": "https://github.com/newrelic/csec-node-agent/blob/v3.0.0/LICENSE", + "licenseUrl": "https://github.com/newrelic/csec-node-agent/blob/v3.0.1/LICENSE", "licenseTextSource": "file", "publisher": "newrelic" }, @@ -115,15 +127,15 @@ "licenseTextSource": "file", "publisher": "OpenTelemetry Authors" }, - "@opentelemetry/core@2.2.0": { + "@opentelemetry/core@2.0.1": { "name": "@opentelemetry/core", - "version": "2.2.0", + "version": "2.0.1", "range": "^2.0.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/open-telemetry/opentelemetry-js", - "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v2.2.0", + "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v2.0.1", "licenseFile": "node_modules/@opentelemetry/core/LICENSE", - "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v2.2.0/LICENSE", + "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v2.0.1/LICENSE", "licenseTextSource": "file", "publisher": "OpenTelemetry Authors" }, @@ -139,15 +151,15 @@ "licenseTextSource": "file", "publisher": "OpenTelemetry Authors" }, - "@opentelemetry/resources@2.2.0": { + "@opentelemetry/resources@2.0.1": { "name": "@opentelemetry/resources", - "version": "2.2.0", + "version": "2.0.1", "range": "^2.0.1", "licenses": "Apache-2.0", "repoUrl": "https://github.com/open-telemetry/opentelemetry-js", - "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v2.2.0", + "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v2.0.1", "licenseFile": "node_modules/@opentelemetry/resources/LICENSE", - "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v2.2.0/LICENSE", + "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v2.0.1/LICENSE", "licenseTextSource": "file", "publisher": "OpenTelemetry Authors" }, @@ -163,27 +175,27 @@ "licenseTextSource": "file", "publisher": "OpenTelemetry Authors" }, - "@opentelemetry/sdk-metrics@2.2.0": { + "@opentelemetry/sdk-metrics@2.0.1": { "name": "@opentelemetry/sdk-metrics", - "version": "2.2.0", + "version": "2.0.1", "range": "^2.0.1", "licenses": "Apache-2.0", "repoUrl": "https://github.com/open-telemetry/opentelemetry-js", - "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v2.2.0", + "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v2.0.1", "licenseFile": "node_modules/@opentelemetry/sdk-metrics/LICENSE", - "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v2.2.0/LICENSE", + "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v2.0.1/LICENSE", "licenseTextSource": "file", "publisher": "OpenTelemetry Authors" }, - "@opentelemetry/sdk-trace-base@2.2.0": { + "@opentelemetry/sdk-trace-base@2.0.1": { "name": "@opentelemetry/sdk-trace-base", - "version": "2.2.0", + "version": "2.0.1", "range": "^2.0.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/open-telemetry/opentelemetry-js", - "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v2.2.0", + "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v2.0.1", "licenseFile": "node_modules/@opentelemetry/sdk-trace-base/LICENSE", - "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v2.2.0/LICENSE", + "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v2.0.1/LICENSE", "licenseTextSource": "file", "publisher": "OpenTelemetry Authors" }, @@ -227,15 +239,15 @@ "email": "nathan@tootallnate.net", "url": "http://n8.io/" }, - "import-in-the-middle@1.15.0": { + "import-in-the-middle@1.13.2": { "name": "import-in-the-middle", - "version": "1.15.0", + "version": "1.13.2", "range": "^1.13.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/nodejs/import-in-the-middle", - "versionedRepoUrl": "https://github.com/nodejs/import-in-the-middle/tree/v1.15.0", + "versionedRepoUrl": "https://github.com/nodejs/import-in-the-middle/tree/v1.13.2", "licenseFile": "node_modules/import-in-the-middle/LICENSE", - "licenseUrl": "https://github.com/nodejs/import-in-the-middle/blob/v1.15.0/LICENSE", + "licenseUrl": "https://github.com/nodejs/import-in-the-middle/blob/v1.13.2/LICENSE", "licenseTextSource": "file", "publisher": "Bryan English", "email": "bryan.english@datadoghq.com" @@ -306,15 +318,15 @@ "email": "w@tson.dk", "url": "https://twitter.com/wa7son" }, - "semver@7.7.3": { + "semver@7.7.2": { "name": "semver", - "version": "7.7.3", + "version": "7.7.2", "range": "^7.5.2", "licenses": "ISC", "repoUrl": "https://github.com/npm/node-semver", - "versionedRepoUrl": "https://github.com/npm/node-semver/tree/v7.7.3", + "versionedRepoUrl": "https://github.com/npm/node-semver/tree/v7.7.2", "licenseFile": "node_modules/semver/LICENSE", - "licenseUrl": "https://github.com/npm/node-semver/blob/v7.7.3/LICENSE", + "licenseUrl": "https://github.com/npm/node-semver/blob/v7.7.2/LICENSE", "licenseTextSource": "file", "publisher": "GitHub Inc." }, @@ -333,28 +345,28 @@ } }, "devDependencies": { - "@aws-sdk/client-s3@3.958.0": { + "@aws-sdk/client-s3@3.808.0": { "name": "@aws-sdk/client-s3", - "version": "3.958.0", + "version": "3.808.0", "range": "^3.556.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js-v3", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.958.0", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.808.0", "licenseFile": "node_modules/@aws-sdk/client-s3/LICENSE", - "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.958.0/LICENSE", + "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.808.0/LICENSE", "licenseTextSource": "file", "publisher": "AWS SDK for JavaScript Team", "url": "https://aws.amazon.com/javascript/" }, - "@aws-sdk/s3-request-presigner@3.958.0": { + "@aws-sdk/s3-request-presigner@3.808.0": { "name": "@aws-sdk/s3-request-presigner", - "version": "3.958.0", + "version": "3.808.0", "range": "^3.556.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js-v3", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.958.0", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.808.0", "licenseFile": "node_modules/@aws-sdk/s3-request-presigner/LICENSE", - "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.958.0/LICENSE", + "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.808.0/LICENSE", "licenseTextSource": "file", "publisher": "AWS SDK for JavaScript Team", "url": "https://aws.amazon.com/javascript/" @@ -483,15 +495,15 @@ "licenseTextSource": "file", "publisher": "Caolan McMahon" }, - "aws-sdk@2.1693.0": { + "aws-sdk@2.1692.0": { "name": "aws-sdk", - "version": "2.1693.0", + "version": "2.1692.0", "range": "^2.1604.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js/tree/v2.1693.0", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js/tree/v2.1692.0", "licenseFile": "node_modules/aws-sdk/LICENSE.txt", - "licenseUrl": "https://github.com/aws/aws-sdk-js/blob/v2.1693.0/LICENSE.txt", + "licenseUrl": "https://github.com/aws/aws-sdk-js/blob/v2.1692.0/LICENSE.txt", "licenseTextSource": "file", "publisher": "Amazon Web Services", "url": "https://aws.amazon.com/" @@ -587,42 +599,42 @@ "email": "maochenyan@gmail.com", "url": "https://github.com/stevemao" }, - "eslint-plugin-jsdoc@50.8.0": { + "eslint-plugin-jsdoc@50.6.14": { "name": "eslint-plugin-jsdoc", - "version": "50.8.0", + "version": "50.6.14", "range": "^50.6.1", "licenses": "BSD-3-Clause", "repoUrl": "https://github.com/gajus/eslint-plugin-jsdoc", - "versionedRepoUrl": "https://github.com/gajus/eslint-plugin-jsdoc/tree/v50.8.0", + "versionedRepoUrl": "https://github.com/gajus/eslint-plugin-jsdoc/tree/v50.6.14", "licenseFile": "node_modules/eslint-plugin-jsdoc/LICENSE", - "licenseUrl": "https://github.com/gajus/eslint-plugin-jsdoc/blob/v50.8.0/LICENSE", + "licenseUrl": "https://github.com/gajus/eslint-plugin-jsdoc/blob/v50.6.14/LICENSE", "licenseTextSource": "file", "publisher": "Gajus Kuizinas", "email": "gajus@gajus.com", "url": "http://gajus.com" }, - "eslint@9.39.2": { + "eslint@9.26.0": { "name": "eslint", - "version": "9.39.2", + "version": "9.26.0", "range": "^9.17.0", "licenses": "MIT", "repoUrl": "https://github.com/eslint/eslint", - "versionedRepoUrl": "https://github.com/eslint/eslint/tree/v9.39.2", + "versionedRepoUrl": "https://github.com/eslint/eslint/tree/v9.26.0", "licenseFile": "node_modules/eslint/LICENSE", - "licenseUrl": "https://github.com/eslint/eslint/blob/v9.39.2/LICENSE", + "licenseUrl": "https://github.com/eslint/eslint/blob/v9.26.0/LICENSE", "licenseTextSource": "file", "publisher": "Nicholas C. Zakas", "email": "nicholas+npm@nczconsulting.com" }, - "express@5.2.1": { + "express@5.1.0": { "name": "express", - "version": "5.2.1", + "version": "5.1.0", "range": "*", "licenses": "MIT", "repoUrl": "https://github.com/expressjs/express", - "versionedRepoUrl": "https://github.com/expressjs/express/tree/v5.2.1", + "versionedRepoUrl": "https://github.com/expressjs/express/tree/v5.1.0", "licenseFile": "node_modules/express/LICENSE", - "licenseUrl": "https://github.com/expressjs/express/blob/v5.2.1/LICENSE", + "licenseUrl": "https://github.com/expressjs/express/blob/v5.1.0/LICENSE", "licenseTextSource": "file", "publisher": "TJ Holowaychuk", "email": "tj@vision-media.ca" @@ -679,15 +691,15 @@ "publisher": "Typicode", "email": "typicode@gmail.com" }, - "jsdoc@4.0.5": { + "jsdoc@4.0.4": { "name": "jsdoc", - "version": "4.0.5", + "version": "4.0.4", "range": "^4.0.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/jsdoc/jsdoc", - "versionedRepoUrl": "https://github.com/jsdoc/jsdoc/tree/v4.0.5", + "versionedRepoUrl": "https://github.com/jsdoc/jsdoc/tree/v4.0.4", "licenseFile": "node_modules/jsdoc/LICENSE.md", - "licenseUrl": "https://github.com/jsdoc/jsdoc/blob/v4.0.5/LICENSE.md", + "licenseUrl": "https://github.com/jsdoc/jsdoc/blob/v4.0.4/LICENSE.md", "licenseTextSource": "file", "publisher": "Michael Mathews", "email": "micmath@gmail.com" @@ -732,15 +744,15 @@ "publisher": "Pedro Teixeira", "email": "pedro.teixeira@gmail.com" }, - "protobufjs@7.5.4": { + "protobufjs@7.5.3": { "name": "protobufjs", - "version": "7.5.4", + "version": "7.5.3", "range": "^7.5.3", "licenses": "BSD-3-Clause", "repoUrl": "https://github.com/protobufjs/protobuf.js", - "versionedRepoUrl": "https://github.com/protobufjs/protobuf.js/tree/v7.5.4", + "versionedRepoUrl": "https://github.com/protobufjs/protobuf.js/tree/v7.5.3", "licenseFile": "node_modules/protobufjs/LICENSE", - "licenseUrl": "https://github.com/protobufjs/protobuf.js/blob/v7.5.4/LICENSE", + "licenseUrl": "https://github.com/protobufjs/protobuf.js/blob/v7.5.3/LICENSE", "licenseTextSource": "file", "publisher": "Daniel Wirtz", "email": "dcode+protobufjs@dcode.io" From 90ecde784a9ed60f3a12388c1359f3d8b6050b30 Mon Sep 17 00:00:00 2001 From: Svetlana Brennan <50715937+svetlanabrennan@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:01:42 -0600 Subject: [PATCH 2/9] refactor some methods and load profiling aggregator --- lib/agent.js | 12 ++++++++++ lib/aggregators/profiling-aggregator.js | 6 ++--- lib/profiling/index.js | 9 ++++---- lib/profiling/profilers/heap.js | 29 ++++++++++--------------- lib/profiling/profilers/index.js | 10 +++++++++ 5 files changed, 40 insertions(+), 26 deletions(-) create mode 100644 lib/profiling/profilers/index.js diff --git a/lib/agent.js b/lib/agent.js index d8a3e30a85..54fc2d56d5 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -41,6 +41,7 @@ const Harvester = require('./harvester') const { createFeatureUsageMetrics } = require('./util/application-logging') const HealthReporter = require('./health-reporter') const Samplers = require('./samplers') +const ProfilingAggregator = require('./aggregators/profiling-aggregator') // Map of valid states to whether or not data collection is valid const STATES = { @@ -302,6 +303,17 @@ function Agent(config) { this.harvester ) + this.profilingData = new ProfilingAggregator( + { + config, + periodMs: config.profiling.sample_interval_ms, + method: 'pprof_data', + isAsync: !config.serverless_mode.enabled, + enabled: (config) => config.profiling.enabled + }, + this + ) + // Set up all the configuration events the agent needs to listen for. this._listenForConfigChanges() diff --git a/lib/aggregators/profiling-aggregator.js b/lib/aggregators/profiling-aggregator.js index 5de3551c0a..d87fc7c935 100644 --- a/lib/aggregators/profiling-aggregator.js +++ b/lib/aggregators/profiling-aggregator.js @@ -45,6 +45,7 @@ class ProfilingAggregator extends BaseAggregator { start() { logger.trace(`${this.method} aggregator started.`) this.profilingManager.register() + this.profilingManager.start() if (!this.sendTimer) { this.sendTimer = setInterval(this.collectData.bind(this), this.periodMs) @@ -63,11 +64,10 @@ class ProfilingAggregator extends BaseAggregator { * calls send which takes care of sending the data to the collector */ collectData() { - const self = this for (const pprofData of this.profilingManager.collect()) { if (pprofData) { - self.pprofData = pprofData - self.send() + this.pprofData = pprofData + this.send() } } } diff --git a/lib/profiling/index.js b/lib/profiling/index.js index 21497535f9..10a9af6134 100644 --- a/lib/profiling/index.js +++ b/lib/profiling/index.js @@ -5,20 +5,19 @@ 'use strict' const defaultLogger = require('#agentlib/logger.js').child({ component: 'profiling-manager' }) -const HeapProfiler = require('./profilers/heap') class ProfilingManager { constructor(agent, { logger = defaultLogger } = {}) { this.logger = logger - this.config = agent.config + this.config = agent.config.profiling this.profilers = [] } // current no-op until we built out the profilers register() { - if (this.config.profiling.includes('heap')) { - const heapProfiler = new HeapProfiler() - this.profilers.push(heapProfiler) + if (this.config.includes('heap')) { + const { HeapProfiler } = require('./profilers') + this.profilers.push(new HeapProfiler()) } } diff --git a/lib/profiling/profilers/heap.js b/lib/profiling/profilers/heap.js index 7d1d78b82f..bb13347a13 100644 --- a/lib/profiling/profilers/heap.js +++ b/lib/profiling/profilers/heap.js @@ -8,34 +8,27 @@ const BaseProfiler = require('../profilers/base') class HeapProfiler extends BaseProfiler { + #pprof constructor() { super() - this.name = 'heap' - // tmp hard code these values for now - this.heapSampleIntervalBytes = 524288 - this.heapSampleStackDepth = 64 - this.pprofData = null - } - - get pprof() { - if (!this._pprof) { - this._pprof = require('@datadog/pprof') - } - return this._pprof + this.name = 'HeapProfiler' + this.#pprof = require('@datadog/pprof') } start() { - this.pprof.heap.start(this.heapSampleIntervalBytes, this.heapSampleStackDepth) + this.#pprof.heap.start({ + intervalBytes: 52488, + stackDepth: 64 + }) } stop() { - this.pprof.heap.stop() + this.#pprof.heap.stop() } - async collect() { - const profile = await this.pprof.heap.profile() - const buf = await this.pprof.encode(profile) - this.pprofData = buf + collect() { + const profile = this.#pprof.heap.profile() + return this.#pprof.encodeSync(profile) } } diff --git a/lib/profiling/profilers/index.js b/lib/profiling/profilers/index.js new file mode 100644 index 0000000000..9695d1f221 --- /dev/null +++ b/lib/profiling/profilers/index.js @@ -0,0 +1,10 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +const HeapProfiler = require('./heap') + +module.exports = { + HeapProfiler +} From 979921ea8a0c8db1e23453f4d17d4971c0e81b65 Mon Sep 17 00:00:00 2001 From: Svetlana Brennan <50715937+svetlanabrennan@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:03:08 -0600 Subject: [PATCH 3/9] fix config name --- lib/agent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/agent.js b/lib/agent.js index 54fc2d56d5..ed44d51370 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -306,7 +306,7 @@ function Agent(config) { this.profilingData = new ProfilingAggregator( { config, - periodMs: config.profiling.sample_interval_ms, + periodMs: config.profiling.sample_interval, method: 'pprof_data', isAsync: !config.serverless_mode.enabled, enabled: (config) => config.profiling.enabled From fe04b5bb0dc0b893475f623734465b08e0d49ce7 Mon Sep 17 00:00:00 2001 From: Svetlana Brennan <50715937+svetlanabrennan@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:47:50 -0600 Subject: [PATCH 4/9] fix typo --- lib/profiling/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/profiling/index.js b/lib/profiling/index.js index 10a9af6134..08f6dc3876 100644 --- a/lib/profiling/index.js +++ b/lib/profiling/index.js @@ -15,7 +15,7 @@ class ProfilingManager { // current no-op until we built out the profilers register() { - if (this.config.includes('heap')) { + if (this.config.include.includes('heap')) { const { HeapProfiler } = require('./profilers') this.profilers.push(new HeapProfiler()) } From 7f25fc00e157885a76842113bb2144b0a332593e Mon Sep 17 00:00:00 2001 From: Bob Evans Date: Thu, 12 Feb 2026 16:00:13 -0500 Subject: [PATCH 5/9] feat: cpu profiler with writing pprof to file(temp) (#3747) --- THIRD_PARTY_NOTICES.md | 215 +++++++++++++++++- lib/agent.js | 12 + lib/aggregators/profiling-aggregator.js | 17 +- lib/collector/remote-method.js | 6 +- lib/profiling/index.js | 35 ++- lib/profiling/profilers/base.js | 6 +- lib/profiling/profilers/cpu.js | 39 ++++ lib/profiling/profilers/index.js | 10 + package.json | 1 + test/unit/agent/agent.test.js | 3 +- .../aggregators/profiling-aggregator.test.js | 62 +++-- test/unit/collector/api.test.js | 15 +- test/unit/lib/profiling/index.test.js | 40 +--- .../unit/lib/profiling/profilers/base.test.js | 11 +- third_party_manifest.json | 30 ++- 15 files changed, 419 insertions(+), 83 deletions(-) create mode 100644 lib/profiling/profilers/cpu.js create mode 100644 lib/profiling/profilers/index.js diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index de39321793..fa4f11d615 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -81,6 +81,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic **[optionalDependencies](#optionalDependencies)** +* [@datadog/pprof](#datadogpprof) * [@newrelic/fn-inspect](#newrelicfn-inspect) * [@newrelic/native-metrics](#newrelicnative-metrics) * [@prisma/prisma-fmt-wasm](#prismaprisma-fmt-wasm) @@ -2960,7 +2961,7 @@ SOFTWARE. ### @aws-sdk/client-s3 -This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.982.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.982.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.982.0/LICENSE): +This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.986.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.986.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.986.0/LICENSE): ``` Apache License @@ -3169,7 +3170,7 @@ This product includes source derived from [@aws-sdk/client-s3](https://github.co ### @aws-sdk/s3-request-presigner -This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.982.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.982.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.982.0/LICENSE): +This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.986.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.986.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.986.0/LICENSE): ``` Apache License @@ -5749,6 +5750,216 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## optionalDependencies +### @datadog/pprof + +This product includes source derived from [@datadog/pprof](https://github.com/DataDog/pprof-nodejs) ([v5.13.3](https://github.com/DataDog/pprof-nodejs/tree/v5.13.3)), distributed under the [Apache-2.0 License](https://github.com/DataDog/pprof-nodejs/blob/v5.13.3/LICENSE): + +``` + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +``` + ### @newrelic/fn-inspect This product includes source derived from [@newrelic/fn-inspect](https://github.com/newrelic-forks/node-fn-inspect) ([v4.4.0](https://github.com/newrelic-forks/node-fn-inspect/tree/v4.4.0)), distributed under the [MIT License](https://github.com/newrelic-forks/node-fn-inspect/blob/v4.4.0/LICENSE): diff --git a/lib/agent.js b/lib/agent.js index d8a3e30a85..54fc2d56d5 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -41,6 +41,7 @@ const Harvester = require('./harvester') const { createFeatureUsageMetrics } = require('./util/application-logging') const HealthReporter = require('./health-reporter') const Samplers = require('./samplers') +const ProfilingAggregator = require('./aggregators/profiling-aggregator') // Map of valid states to whether or not data collection is valid const STATES = { @@ -302,6 +303,17 @@ function Agent(config) { this.harvester ) + this.profilingData = new ProfilingAggregator( + { + config, + periodMs: config.profiling.sample_interval_ms, + method: 'pprof_data', + isAsync: !config.serverless_mode.enabled, + enabled: (config) => config.profiling.enabled + }, + this + ) + // Set up all the configuration events the agent needs to listen for. this._listenForConfigChanges() diff --git a/lib/aggregators/profiling-aggregator.js b/lib/aggregators/profiling-aggregator.js index 5de3551c0a..4ca14fe4f7 100644 --- a/lib/aggregators/profiling-aggregator.js +++ b/lib/aggregators/profiling-aggregator.js @@ -45,6 +45,7 @@ class ProfilingAggregator extends BaseAggregator { start() { logger.trace(`${this.method} aggregator started.`) this.profilingManager.register() + this.profilingManager.start() if (!this.sendTimer) { this.sendTimer = setInterval(this.collectData.bind(this), this.periodMs) @@ -62,12 +63,18 @@ class ProfilingAggregator extends BaseAggregator { * and collects data for the given time period. Then asynchronously * calls send which takes care of sending the data to the collector */ - collectData() { - const self = this - for (const pprofData of this.profilingManager.collect()) { + async collectData() { + let profilingData = [] + try { + profilingData = await this.profilingManager.collect() + } catch (err) { + logger.error(err, 'Failed to collect profiilng data') + } + + for (const pprofData of profilingData) { if (pprofData) { - self.pprofData = pprofData - self.send() + this.pprofData = pprofData + this.send() } } } diff --git a/lib/collector/remote-method.js b/lib/collector/remote-method.js index afc17f0095..2d9e1f018d 100644 --- a/lib/collector/remote-method.js +++ b/lib/collector/remote-method.js @@ -62,7 +62,11 @@ RemoteMethod.prototype.updateEndpoint = function updateEndpoint(endpoint) { RemoteMethod.prototype.serialize = function serialize(payload, callback) { let res try { - res = stringify(payload, (key, value) => (typeof value === 'bigint' ? value.toString() : value)) + if (this._contentType === 'application/octet-stream') { + res = payload + } else { + res = stringify(payload, (key, value) => (typeof value === 'bigint' ? value.toString() : value)) + } } catch (error) { logger.error(error, 'Unable to serialize payload for method %s.', this.name) return process.nextTick(function onNextTick() { diff --git a/lib/profiling/index.js b/lib/profiling/index.js index 16307b0508..8856c1b77b 100644 --- a/lib/profiling/index.js +++ b/lib/profiling/index.js @@ -5,16 +5,22 @@ 'use strict' const defaultLogger = require('#agentlib/logger.js').child({ component: 'profiling-manager' }) +const { mkdir, writeFile } = require('node:fs/promises') +const { randomUUID } = require('node:crypto') class ProfilingManager { constructor(agent, { logger = defaultLogger } = {}) { this.logger = logger - this.config = agent.config + this.config = agent.config.profiling this.profilers = [] + this.outputDir = process.cwd() + '/profiler-data' } - // current no-op until we built out the profilers register() { + if (this.config.include.includes('cpu')) { + const { CpuProfiler } = require('./profilers') + this.profilers.push(new CpuProfiler({ logger: this.logger })) + } } start() { @@ -41,17 +47,34 @@ class ProfilingManager { } } - collect() { + async writeFile({ pprofData, name }) { + if (this.config.debug) { + const fileName = `${this.outputDir}/${name}-${randomUUID()}.gz` + try { + this.logger.trace(`Writing ${name} pprof data to ${fileName}`) + await mkdir(this.outputDir, { recursive: true }) + writeFile(fileName, pprofData) + } catch (err) { + this.logger.error(`Failed to write pprof data to ${fileName}: ${err.message}`) + } + } + } + + async collect() { const results = [] if (this.profilers.length === 0) { this.logger.warn('No profilers have been included in `config.profiling.include`, not collecting any profiling data.') return results } - return this.profilers.map((profiler) => { + for (const profiler of this.profilers) { this.logger.debug(`Collecting profiling data for ${profiler.name}`) - return profiler.collect() - }) + const pprofData = await profiler.collect() + this.writeFile({ pprofData, name: profiler.name }) + results.push(pprofData) + } + + return results } } diff --git a/lib/profiling/profilers/base.js b/lib/profiling/profilers/base.js index 9198682cf0..429b33ea78 100644 --- a/lib/profiling/profilers/base.js +++ b/lib/profiling/profilers/base.js @@ -6,6 +6,10 @@ 'use strict' class BaseProfiler { + constructor({ logger }) { + this.logger = logger + } + set name(name) { this._name = name } @@ -22,7 +26,7 @@ class BaseProfiler { throw new Error('stop is not implemented') } - collect() { + async collect() { throw new Error('collect is not implemented') } } diff --git a/lib/profiling/profilers/cpu.js b/lib/profiling/profilers/cpu.js new file mode 100644 index 0000000000..5ccf90f6a1 --- /dev/null +++ b/lib/profiling/profilers/cpu.js @@ -0,0 +1,39 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const BaseProfiler = require('./base') + +class CpuProfiler extends BaseProfiler { + #pprof + constructor({ logger }) { + super({ logger }) + this.name = 'CpuProfiler' + this.#pprof = require('@datadog/pprof') + } + + start() { + if (this.#pprof.time.isStarted()) { + this.logger.trace('CpuProfiler is already started, not calling start again.') + return + } + + this.#pprof.time.start({ + durationMillis: 60 * 1e3, // 1 min + intervalMicros: (1e3 / 99) * 1000 + }) + } + + stop() { + this.#pprof.time.stop(false) + } + + async collect() { + const profile = this.#pprof.time.stop(true) + return this.#pprof.encode(profile) + } +} + +module.exports = CpuProfiler diff --git a/lib/profiling/profilers/index.js b/lib/profiling/profilers/index.js new file mode 100644 index 0000000000..1ff79352b4 --- /dev/null +++ b/lib/profiling/profilers/index.js @@ -0,0 +1,10 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +const CpuProfiler = require('./cpu') + +module.exports = { + CpuProfiler +} diff --git a/package.json b/package.json index ade8208816..0b90c429d6 100644 --- a/package.json +++ b/package.json @@ -227,6 +227,7 @@ "winston-transport": "^4.5.0" }, "optionalDependencies": { + "@datadog/pprof": "^5.13.3", "@newrelic/fn-inspect": "^4.4.0", "@newrelic/native-metrics": "^12.0.0", "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085" diff --git a/test/unit/agent/agent.test.js b/test/unit/agent/agent.test.js index e73164068e..5bfb18019b 100644 --- a/test/unit/agent/agent.test.js +++ b/test/unit/agent/agent.test.js @@ -296,7 +296,7 @@ test('#harvesters.stop should stop all aggregators', (t) => { }) test('#onConnect should reconfigure all the aggregators', (t, end) => { - const EXPECTED_AGG_COUNT = 9 + const EXPECTED_AGG_COUNT = 10 const agent = helper.loadMockedAgent(null, false) agent.config.application_logging.forwarding.enabled = true // Mock out the base reconfigure method: @@ -314,6 +314,7 @@ test('#onConnect should reconfigure all the aggregators', (t, end) => { span_event_data: 1 } } + agent.onConnect(false, () => { assert.equal(proto.reconfigure.callCount, EXPECTED_AGG_COUNT) end() diff --git a/test/unit/aggregators/profiling-aggregator.test.js b/test/unit/aggregators/profiling-aggregator.test.js index e4baf1580e..b86f63b3fe 100644 --- a/test/unit/aggregators/profiling-aggregator.test.js +++ b/test/unit/aggregators/profiling-aggregator.test.js @@ -14,11 +14,18 @@ const RUN_ID = 1337 test.beforeEach((ctx) => { const sandbox = sinon.createSandbox() - const agent = helper.loadMockedAgent() + // setting the `profiling.include` to empty array + // as we are creating mock profilers + const agent = helper.loadMockedAgent({ + profiling: { + include: [] + } + }) const cpuProfiler = { name: 'CpuProfiler', + start: sandbox.stub(), stop: sandbox.stub(), - collect() { + async collect() { return 'cpu profile data' } } @@ -26,8 +33,9 @@ test.beforeEach((ctx) => { const clock = sinon.useFakeTimers() const heapProfiler = { name: 'HeapProfiler', + start: sandbox.stub(), stop: sandbox.stub(), - collect() { + async collect() { return 'heap profile data' } } @@ -35,10 +43,11 @@ test.beforeEach((ctx) => { const profilingAggregator = new ProfilingAggregator({ runId: RUN_ID, periodMs: 100 }, agent) const profilingManager = profilingAggregator.profilingManager sandbox.spy(profilingManager, 'register') - profilingAggregator.profilingManager.profilers = [cpuProfiler, heapProfiler] ctx.nr = { agent, clock, + cpuProfiler, + heapProfiler, profilingAggregator, profilingManager, sandbox @@ -63,41 +72,54 @@ test('should initialize pprofData and profilingManager', (t) => { assert.equal(profilingAggregator.pprofData, null) }) -test('should send 2 messages per interval', (t) => { - const { profilingAggregator, profilingManager, clock, agent } = t.nr - assert.equal(profilingManager.register.callCount, 0) +test('should send 2 messages per interval', async (t) => { + const { profilingAggregator, clock, agent, cpuProfiler, heapProfiler } = t.nr + assert.equal(profilingAggregator.profilingManager.register.callCount, 0) + profilingAggregator.profilingManager.profilers = [cpuProfiler, heapProfiler] profilingAggregator.start() - assert.equal(profilingManager.register.callCount, 1) + assert.equal(profilingAggregator.profilingManager.register.callCount, 1) assert.equal(agent.collector.send.callCount, 0) clock.tick(100) - assert.equal(agent.collector.send.callCount, 2) - const [cpuCall, heapCall] = agent.collector.send.args - assert.equal(cpuCall[0], 'pprof_data') - assert.equal(cpuCall[1], 'cpu profile data') - assert.equal(heapCall[0], 'pprof_data') - assert.equal(heapCall[1], 'heap profile data') - assert.equal(profilingAggregator.pprofData, null) + // need to run in next tick to ensure the promise chain around `profilingManager.collectData` resolves + await new Promise((resolve) => { + process.nextTick(() => { + assert.equal(agent.collector.send.callCount, 2) + const [cpuCall, heapCall] = agent.collector.send.args + assert.equal(cpuCall[0], 'pprof_data') + assert.equal(cpuCall[1], 'cpu profile data') + assert.equal(heapCall[0], 'pprof_data') + assert.equal(heapCall[1], 'heap profile data') + assert.equal(profilingAggregator.pprofData, null) + resolve() + }) + }) }) -test('should not send any data if there are no profilers registered', (t) => { +test('should not send any data if there are no profilers registered', async (t) => { const { profilingAggregator, clock, agent } = t.nr profilingAggregator.profilingManager.profilers = [] profilingAggregator.start() assert.equal(agent.collector.send.callCount, 0) clock.tick(100) - assert.equal(agent.collector.send.callCount, 0) + await new Promise((resolve) => { + process.nextTick(() => { + assert.equal(agent.collector.send.callCount, 0) + resolve() + }) + }) }) test('should stop ProfilingManager when aggregator is stopped', (t) => { - const { profilingAggregator, profilingManager } = t.nr + const { profilingAggregator, cpuProfiler, heapProfiler } = t.nr + profilingAggregator.profilingManager.profilers = [cpuProfiler, heapProfiler] profilingAggregator.start() assert.ok(profilingAggregator.sendTimer) - for (const profiler of profilingManager.profilers) { + for (const profiler of profilingAggregator.profilingManager.profilers) { assert.equal(profiler.stop.callCount, 0) } profilingAggregator.stop() assert.equal(profilingAggregator.sendTimer, null) - for (const profiler of profilingManager.profilers) { + for (const profiler of profilingAggregator.profilingManager.profilers) { assert.equal(profiler.stop.callCount, 1) } }) diff --git a/test/unit/collector/api.test.js b/test/unit/collector/api.test.js index f1f31dd055..68d628fdda 100644 --- a/test/unit/collector/api.test.js +++ b/test/unit/collector/api.test.js @@ -379,11 +379,16 @@ test('api methods', async (t) => { const { collector, collectorApi } = t.nr collector.addHandler(helper.generateCollectorPath(method.key, RUN_ID), async (req, res) => { const body = await req.body() - const found = JSON.parse(body) - - let expected = method.data - if (method.data.toJSON) { - expected = method.data.toJSON() + let found, expected + // pprof_data will be gzipped binary data + // we need to keep the response and call `toString` + // on the expected, as for this test we just response with a buffer + if (method.key === 'pprof_data') { + found = body + expected = method.data.toString() + } else { + found = JSON.parse(body) + expected = method.data.toJSON ? method.data.toJSON() : method.data } assert.deepStrictEqual(found, expected) diff --git a/test/unit/lib/profiling/index.test.js b/test/unit/lib/profiling/index.test.js index 6e8829b18a..59935aef34 100644 --- a/test/unit/lib/profiling/index.test.js +++ b/test/unit/lib/profiling/index.test.js @@ -53,7 +53,7 @@ describe('constructor', () => { const profilingManager = new ProfilingManager(agent) assert.ok(profilingManager.config, 'should have config') - assert.deepStrictEqual(profilingManager.config, t.nr.agent.config, 'should store agent config') + assert.deepStrictEqual(profilingManager.config, t.nr.agent.config.profiling, 'should store agent config') }) test('should initialize empty profilers array', (t) => { @@ -138,11 +138,11 @@ describe('stop', () => { }) describe('collect', (t) => { - test('should warn and return empty array when no profilers are registered', (t) => { + test('should warn and return empty array when no profilers are registered', async (t) => { const { agent, logger } = t.nr const profilingManager = new ProfilingManager(agent, { logger }) - const results = profilingManager.collect() + const results = await profilingManager.collect() assert.equal(results.length, 0, 'should return empty array') assert.equal(logger.warn.callCount, 1) assert.ok( @@ -152,16 +152,16 @@ describe('collect', (t) => { ) }) - test('should collect data from all registered profilers', (t) => { + test('should collect data from all registered profilers', async (t) => { const { agent, cpuProfiler, heapProfiler, logger } = t.nr const profilingManager = new ProfilingManager(agent, { logger }) const expectedCpuData = { type: 'cpu', data: Buffer.from('cpu-profile-data') } const expectedHeapData = { type: 'heap', data: Buffer.from('heap-profile-data') } - cpuProfiler.collect.returns(expectedCpuData) - heapProfiler.collect.returns(expectedHeapData) + cpuProfiler.collect.resolves(expectedCpuData) + heapProfiler.collect.resolves(expectedHeapData) profilingManager.profilers = [cpuProfiler, heapProfiler] - const results = profilingManager.collect() + const results = await profilingManager.collect() assert.equal(results.length, 2, 'should return array with two items') const [cpuData, heapData] = results assert.equal(cpuProfiler.collect.callCount, 1) @@ -172,16 +172,16 @@ describe('collect', (t) => { assert.ok(logger.debug.calledWith('Collecting profiling data for heap')) }) - test('should handle profilers returning undefined or null', (t) => { + test('should handle profilers returning undefined or null', async (t) => { const { agent, cpuProfiler, heapProfiler, logger } = t.nr const profilingManager = new ProfilingManager(agent, { logger }) const expectedCpuData = undefined const expectedHeapData = null - cpuProfiler.collect.returns(expectedCpuData) - heapProfiler.collect.returns(expectedHeapData) + cpuProfiler.collect.resolves(expectedCpuData) + heapProfiler.collect.resolves(expectedHeapData) profilingManager.profilers = [cpuProfiler, heapProfiler] - const results = profilingManager.collect() + const results = await profilingManager.collect() assert.equal(results.length, 2, 'should return array with two items') const [cpuData, heapData] = results assert.equal(cpuProfiler.collect.callCount, 1) @@ -191,22 +191,4 @@ describe('collect', (t) => { assert.ok(logger.debug.calledWith('Collecting profiling data for cpu')) assert.ok(logger.debug.calledWith('Collecting profiling data for heap')) }) - - test('should handle profilers returning null', (t) => { - const profilingManager = new ProfilingManager(t.nr.agent) - - const mockProfiler = { - name: 'test-profiler', - start: sinon.stub(), - stop: sinon.stub(), - collect: sinon.stub().returns(null) - } - - profilingManager.profilers.push(mockProfiler) - const results = profilingManager.collect() - - assert.ok(Array.isArray(results), 'should return an array') - assert.strictEqual(results.length, 1, 'should include the null result') - assert.strictEqual(results[0], null, 'should include null value') - }) }) diff --git a/test/unit/lib/profiling/profilers/base.test.js b/test/unit/lib/profiling/profilers/base.test.js index 4b20c4ba8e..bc0002282b 100644 --- a/test/unit/lib/profiling/profilers/base.test.js +++ b/test/unit/lib/profiling/profilers/base.test.js @@ -10,10 +10,15 @@ const BaseProfiler = require('#agentlib/profiling/profilers/base.js') test.beforeEach((ctx) => { ctx.nr = { - profiler: new BaseProfiler() + profiler: new BaseProfiler({ logger: 'logger' }) } }) +test('should assign logger property', (t) => { + const { profiler } = t.nr + assert.equal(profiler.logger, 'logger') +}) + test('should set name', (t) => { const { profiler } = t.nr profiler.name = 'TestProfiler' @@ -36,7 +41,5 @@ test('should throw error when stop is called', (t) => { test('should throw error when collect is called', (t) => { const { profiler } = t.nr - assert.throws(() => { - profiler.collect() - }) + assert.rejects(() => profiler.collect()) }) diff --git a/third_party_manifest.json b/third_party_manifest.json index 99b659531f..678e2e214a 100644 --- a/third_party_manifest.json +++ b/third_party_manifest.json @@ -1,9 +1,21 @@ { - "lastUpdated": "Wed Feb 11 2026 09:42:58 GMT-0500 (Eastern Standard Time)", + "lastUpdated": "Wed Feb 11 2026 15:50:47 GMT-0500 (Eastern Standard Time)", "projectName": "New Relic Node Agent", "projectUrl": "https://github.com/newrelic/node-newrelic", "includeOptDeps": true, "optionalDependencies": { + "@datadog/pprof@5.13.3": { + "name": "@datadog/pprof", + "version": "5.13.3", + "range": "^5.13.3", + "licenses": "Apache-2.0", + "repoUrl": "https://github.com/DataDog/pprof-nodejs", + "versionedRepoUrl": "https://github.com/DataDog/pprof-nodejs/tree/v5.13.3", + "licenseFile": "node_modules/@datadog/pprof/LICENSE", + "licenseUrl": "https://github.com/DataDog/pprof-nodejs/blob/v5.13.3/LICENSE", + "licenseTextSource": "file", + "publisher": "Google Inc." + }, "@newrelic/fn-inspect@4.4.0": { "name": "@newrelic/fn-inspect", "version": "4.4.0", @@ -333,28 +345,28 @@ } }, "devDependencies": { - "@aws-sdk/client-s3@3.982.0": { + "@aws-sdk/client-s3@3.986.0": { "name": "@aws-sdk/client-s3", - "version": "3.982.0", + "version": "3.986.0", "range": "^3.556.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js-v3", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.982.0", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.986.0", "licenseFile": "node_modules/@aws-sdk/client-s3/LICENSE", - "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.982.0/LICENSE", + "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.986.0/LICENSE", "licenseTextSource": "file", "publisher": "AWS SDK for JavaScript Team", "url": "https://aws.amazon.com/javascript/" }, - "@aws-sdk/s3-request-presigner@3.982.0": { + "@aws-sdk/s3-request-presigner@3.986.0": { "name": "@aws-sdk/s3-request-presigner", - "version": "3.982.0", + "version": "3.986.0", "range": "^3.556.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js-v3", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.982.0", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.986.0", "licenseFile": "node_modules/@aws-sdk/s3-request-presigner/LICENSE", - "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.982.0/LICENSE", + "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.986.0/LICENSE", "licenseTextSource": "file", "publisher": "AWS SDK for JavaScript Team", "url": "https://aws.amazon.com/javascript/" From bffb841a8fb520f88655a82f70aef9b4bfceb3c8 Mon Sep 17 00:00:00 2001 From: Svetlana Brennan <50715937+svetlanabrennan@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:01:04 -0600 Subject: [PATCH 6/9] fix heap start --- lib/profiling/profilers/heap.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/profiling/profilers/heap.js b/lib/profiling/profilers/heap.js index bb13347a13..eaebf091ed 100644 --- a/lib/profiling/profilers/heap.js +++ b/lib/profiling/profilers/heap.js @@ -16,10 +16,10 @@ class HeapProfiler extends BaseProfiler { } start() { - this.#pprof.heap.start({ - intervalBytes: 52488, - stackDepth: 64 - }) + const intervalBytes = 524288 + const stackDepth = 64 + + this.#pprof.heap.start(intervalBytes, stackDepth) } stop() { From 296f5568e5c6a291026d5c98840f94372cc9ede5 Mon Sep 17 00:00:00 2001 From: Svetlana Brennan <50715937+svetlanabrennan@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:11:26 -0600 Subject: [PATCH 7/9] Update package.json --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 217ce1c005..0b90c429d6 100644 --- a/package.json +++ b/package.json @@ -230,8 +230,7 @@ "@datadog/pprof": "^5.13.3", "@newrelic/fn-inspect": "^4.4.0", "@newrelic/native-metrics": "^12.0.0", - "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085", - "@datadog/pprof": "^5.13.3" + "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085" }, "devDependencies": { "@aws-sdk/client-s3": "^3.556.0", From 5e83642d954d467e5ffcb28dfd846f5a2d433f49 Mon Sep 17 00:00:00 2001 From: Bob Evans Date: Thu, 12 Feb 2026 16:19:50 -0500 Subject: [PATCH 8/9] chore: Fix a few issues with sync vs async --- lib/profiling/index.js | 2 +- lib/profiling/profilers/heap.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/profiling/index.js b/lib/profiling/index.js index 34dfdc5088..191ea2d2c6 100644 --- a/lib/profiling/index.js +++ b/lib/profiling/index.js @@ -19,7 +19,7 @@ class ProfilingManager { register() { if (this.config.include.includes('heap')) { const { HeapProfiler } = require('./profilers') - this.profilers.push(new HeapProfiler()) + this.profilers.push(new HeapProfiler({ logger: this.logger })) } if (this.config.include.includes('cpu')) { diff --git a/lib/profiling/profilers/heap.js b/lib/profiling/profilers/heap.js index eaebf091ed..0adc50b873 100644 --- a/lib/profiling/profilers/heap.js +++ b/lib/profiling/profilers/heap.js @@ -26,9 +26,9 @@ class HeapProfiler extends BaseProfiler { this.#pprof.heap.stop() } - collect() { + async collect() { const profile = this.#pprof.heap.profile() - return this.#pprof.encodeSync(profile) + return this.#pprof.encode(profile) } } From 5091220cde76482b6a377ea9e94acf8feb4cff53 Mon Sep 17 00:00:00 2001 From: Bob Evans Date: Thu, 12 Feb 2026 16:30:48 -0500 Subject: [PATCH 9/9] chore: fixed heap constructor --- lib/profiling/profilers/heap.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/profiling/profilers/heap.js b/lib/profiling/profilers/heap.js index 0adc50b873..abb163efd6 100644 --- a/lib/profiling/profilers/heap.js +++ b/lib/profiling/profilers/heap.js @@ -9,8 +9,8 @@ const BaseProfiler = require('../profilers/base') class HeapProfiler extends BaseProfiler { #pprof - constructor() { - super() + constructor({ logger }) { + super({ logger }) this.name = 'HeapProfiler' this.#pprof = require('@datadog/pprof') }