Skip to content

Commit 105da3b

Browse files
authored
feat: Added ProfilingManager class, used to orchestrate profilers (newrelic#3738)
1 parent 59ff976 commit 105da3b

File tree

6 files changed

+400
-17
lines changed

6 files changed

+400
-17
lines changed

lib/aggregators/profiling-aggregator.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
const logger = require('../logger').child({ component: 'pprof_aggregator' })
99
const BaseAggregator = require('./base-aggregator')
10+
const ProfilingManager = require('#agentlib/profiling/index.js')
1011

1112
/**
1213
* Serves as a means for transmitting pprof data as it is being collected.
@@ -16,7 +17,7 @@ const BaseAggregator = require('./base-aggregator')
1617
* The `ProfilingAggregator` aggregator overrides the base `start` method.
1718
* It sets up the interval based on configuration but instead of calling `send`,
1819
* it calls `collectData` which instructs the Profiler to collect profiling data
19-
* for every register profiler(cpu, heap at the moment if enabled)
20+
* for every registered profiler(cpu, heap at the moment if enabled)
2021
*
2122
* @private
2223
* @class
@@ -27,7 +28,7 @@ class ProfilingAggregator extends BaseAggregator {
2728
opts.method = opts.method || 'pprof_data'
2829
super(opts, collector, harvester)
2930
this.agent = agent
30-
this.profiler = opts.profiler
31+
this.profilingManager = new ProfilingManager(agent)
3132
this.pprofData = null
3233
}
3334

@@ -43,22 +44,31 @@ class ProfilingAggregator extends BaseAggregator {
4344
*/
4445
start() {
4546
logger.trace(`${this.method} aggregator started.`)
47+
this.profilingManager.register()
4648

4749
if (!this.sendTimer) {
4850
this.sendTimer = setInterval(this.collectData.bind(this), this.periodMs)
4951
this.sendTimer.unref()
5052
}
5153
}
5254

55+
stop() {
56+
super.stop()
57+
this.profilingManager.stop()
58+
}
59+
5360
/**
5461
* Called on an interval. Iterates over all registered profilers
5562
* and collects data for the given time period. Then asynchronously
5663
* calls send which takes care of sending the data to the collector
5764
*/
5865
collectData() {
59-
for (const profiler of this.profiler.profilers) {
60-
this.pprofData = profiler.collect()
61-
this.send()
66+
const self = this
67+
for (const pprofData of this.profilingManager.collect()) {
68+
if (pprofData) {
69+
self.pprofData = pprofData
70+
self.send()
71+
}
6272
}
6373
}
6474

lib/profiling/index.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const defaultLogger = require('#agentlib/logger.js').child({ component: 'profiling-manager' })
8+
9+
class ProfilingManager {
10+
constructor(agent, { logger = defaultLogger } = {}) {
11+
this.logger = logger
12+
this.config = agent.config
13+
this.profilers = []
14+
}
15+
16+
// current no-op until we built out the profilers
17+
register() {
18+
}
19+
20+
start() {
21+
if (this.profilers.length === 0) {
22+
this.logger.warn('No profilers have been included in `config.profiling.include`, not starting any profilers.')
23+
return
24+
}
25+
26+
for (const profiler of this.profilers) {
27+
this.logger.debug(`Starting ${profiler.name}`)
28+
profiler.start()
29+
}
30+
}
31+
32+
stop() {
33+
if (this.profilers.length === 0) {
34+
this.logger.warn('No profilers have been included in `config.profiling.include`, not stopping any profilers.')
35+
return
36+
}
37+
38+
for (const profiler of this.profilers) {
39+
this.logger.debug(`Stopping ${profiler.name}`)
40+
profiler.stop()
41+
}
42+
}
43+
44+
collect() {
45+
const results = []
46+
if (this.profilers.length === 0) {
47+
this.logger.warn('No profilers have been included in `config.profiling.include`, not collecting any profiling data.')
48+
return results
49+
}
50+
51+
return this.profilers.map((profiler) => {
52+
this.logger.debug(`Collecting profiling data for ${profiler.name}`)
53+
return profiler.collect()
54+
})
55+
}
56+
}
57+
58+
module.exports = ProfilingManager

lib/profiling/profilers/base.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
class BaseProfiler {
9+
set name(name) {
10+
this._name = name
11+
}
12+
13+
get name() {
14+
return this._name
15+
}
16+
17+
start() {
18+
throw new Error('start is not implemented')
19+
}
20+
21+
stop() {
22+
throw new Error('stop is not implemented')
23+
}
24+
25+
collect() {
26+
throw new Error('collect is not implemented')
27+
}
28+
}
29+
30+
module.exports = BaseProfiler
Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 New Relic Corporation. All rights reserved.
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -13,36 +13,42 @@ const helper = require('#testlib/agent_helper.js')
1313
const RUN_ID = 1337
1414

1515
test.beforeEach((ctx) => {
16+
const sandbox = sinon.createSandbox()
1617
const agent = helper.loadMockedAgent()
1718
const cpuProfiler = {
19+
name: 'CpuProfiler',
20+
stop: sandbox.stub(),
1821
collect() {
1922
return 'cpu profile data'
2023
}
2124
}
2225

2326
const clock = sinon.useFakeTimers()
2427
const heapProfiler = {
28+
name: 'HeapProfiler',
29+
stop: sandbox.stub(),
2530
collect() {
2631
return 'heap profile data'
2732
}
2833
}
29-
const profiler = {
30-
profilers: [cpuProfiler, heapProfiler]
31-
}
32-
sinon.spy(agent.collector, 'send')
33-
const profilingAggregator = new ProfilingAggregator({ runId: RUN_ID, periodMs: 100, profiler }, agent)
34+
sandbox.spy(agent.collector, 'send')
35+
const profilingAggregator = new ProfilingAggregator({ runId: RUN_ID, periodMs: 100 }, agent)
36+
const profilingManager = profilingAggregator.profilingManager
37+
sandbox.spy(profilingManager, 'register')
38+
profilingAggregator.profilingManager.profilers = [cpuProfiler, heapProfiler]
3439
ctx.nr = {
3540
agent,
3641
clock,
3742
profilingAggregator,
38-
profiler
43+
profilingManager,
44+
sandbox
3945
}
4046
})
4147

4248
test.afterEach((ctx) => {
4349
helper.unloadAgent(ctx.nr.agent)
4450
ctx.nr.clock.restore()
45-
ctx.nr.agent.collector.send.restore()
51+
ctx.nr.sandbox.restore()
4652
})
4753

4854
test('should set the correct default method', (t) => {
@@ -51,15 +57,17 @@ test('should set the correct default method', (t) => {
5157
assert.equal(method, 'pprof_data')
5258
})
5359

54-
test('should intialize pprofData and profiler', (t) => {
55-
const { profilingAggregator, profiler } = t.nr
56-
assert.deepEqual(profilingAggregator.profiler, profiler)
60+
test('should initialize pprofData and profilingManager', (t) => {
61+
const { profilingAggregator, profilingManager } = t.nr
62+
assert.deepEqual(profilingAggregator.profilingManager, profilingManager)
5763
assert.equal(profilingAggregator.pprofData, null)
5864
})
5965

6066
test('should send 2 messages per interval', (t) => {
61-
const { profilingAggregator, clock, agent } = t.nr
67+
const { profilingAggregator, profilingManager, clock, agent } = t.nr
68+
assert.equal(profilingManager.register.callCount, 0)
6269
profilingAggregator.start()
70+
assert.equal(profilingManager.register.callCount, 1)
6371
assert.equal(agent.collector.send.callCount, 0)
6472
clock.tick(100)
6573
assert.equal(agent.collector.send.callCount, 2)
@@ -70,3 +78,26 @@ test('should send 2 messages per interval', (t) => {
7078
assert.equal(heapCall[1], 'heap profile data')
7179
assert.equal(profilingAggregator.pprofData, null)
7280
})
81+
82+
test('should not send any data if there are no profilers registered', (t) => {
83+
const { profilingAggregator, clock, agent } = t.nr
84+
profilingAggregator.profilingManager.profilers = []
85+
profilingAggregator.start()
86+
assert.equal(agent.collector.send.callCount, 0)
87+
clock.tick(100)
88+
assert.equal(agent.collector.send.callCount, 0)
89+
})
90+
91+
test('should stop ProfilingManager when aggregator is stopped', (t) => {
92+
const { profilingAggregator, profilingManager } = t.nr
93+
profilingAggregator.start()
94+
assert.ok(profilingAggregator.sendTimer)
95+
for (const profiler of profilingManager.profilers) {
96+
assert.equal(profiler.stop.callCount, 0)
97+
}
98+
profilingAggregator.stop()
99+
assert.equal(profilingAggregator.sendTimer, null)
100+
for (const profiler of profilingManager.profilers) {
101+
assert.equal(profiler.stop.callCount, 1)
102+
}
103+
})

0 commit comments

Comments
 (0)