Skip to content
12,715 changes: 6,463 additions & 6,252 deletions THIRD_PARTY_NOTICES.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -302,6 +303,17 @@ function Agent(config) {
this.harvester
)

this.profilingData = new ProfilingAggregator(
{
config,
periodMs: config.profiling.sample_interval,
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()

Expand Down
17 changes: 12 additions & 5 deletions lib/aggregators/profiling-aggregator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion lib/collector/remote-method.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
40 changes: 34 additions & 6 deletions lib/profiling/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@

'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('heap')) {
const { HeapProfiler } = require('./profilers')
this.profilers.push(new HeapProfiler({ logger: this.logger }))
}

if (this.config.include.includes('cpu')) {
const { CpuProfiler } = require('./profilers')
this.profilers.push(new CpuProfiler({ logger: this.logger }))
}
}

start() {
Expand All @@ -41,17 +52,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
}
}

Expand Down
6 changes: 5 additions & 1 deletion lib/profiling/profilers/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
'use strict'

class BaseProfiler {
constructor({ logger }) {
this.logger = logger
}

set name(name) {
this._name = name
}
Expand All @@ -22,7 +26,7 @@ class BaseProfiler {
throw new Error('stop is not implemented')
}

collect() {
async collect() {
throw new Error('collect is not implemented')
}
}
Expand Down
39 changes: 39 additions & 0 deletions lib/profiling/profilers/cpu.js
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions lib/profiling/profilers/heap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 {
#pprof
constructor({ logger }) {
super({ logger })
this.name = 'HeapProfiler'
this.#pprof = require('@datadog/pprof')
}

start() {
const intervalBytes = 524288
const stackDepth = 64

this.#pprof.heap.start(intervalBytes, stackDepth)
}

stop() {
this.#pprof.heap.stop()
}

async collect() {
const profile = this.#pprof.heap.profile()
return this.#pprof.encode(profile)
}
}

module.exports = HeapProfiler
12 changes: 12 additions & 0 deletions lib/profiling/profilers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2026 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

const CpuProfiler = require('./cpu')
const HeapProfiler = require('./heap')

module.exports = {
CpuProfiler,
HeapProfiler
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion test/unit/agent/agent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
Expand Down
62 changes: 42 additions & 20 deletions test/unit/aggregators/profiling-aggregator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,40 @@ 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'
}
}

const clock = sinon.useFakeTimers()
const heapProfiler = {
name: 'HeapProfiler',
start: sandbox.stub(),
stop: sandbox.stub(),
collect() {
async collect() {
return 'heap profile data'
}
}
sandbox.spy(agent.collector, 'send')
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
Expand All @@ -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)
}
})
Loading
Loading