Skip to content

Commit 8503d25

Browse files
authored
feat: Added ability to delay the start of the Profiler and run for n milliseconds before shutting down. (#3758)
1 parent f1e3a0e commit 8503d25

File tree

8 files changed

+118
-43
lines changed

8 files changed

+118
-43
lines changed

lib/aggregators/base-aggregator.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ class Aggregator extends EventEmitter {
160160

161161
this.defaultPeriod = this.periodMs = opts.periodMs
162162
this.defaultLimit = this.limit = opts.limit
163+
this.delay = opts.delay ?? 0
164+
this.duration = opts.duration ?? 0
163165
this.runId = opts.runId
164166
this.isAsync = opts.isAsync || false
165167
// function to pass in to determine if we can start a given aggregator

lib/aggregators/profiling-aggregator.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,13 @@ class ProfilingAggregator extends BaseAggregator {
4343
* and send the gzipped binary encoded data for each profiler
4444
*/
4545
start() {
46-
logger.trace(`${this.method} aggregator started.`)
4746
this.profilingManager.register()
47+
const started = this.profilingManager.start()
48+
if (!started) {
49+
return
50+
}
51+
52+
logger.trace(`${this.method} aggregator started.`)
4853

4954
if (!this.sendTimer) {
5055
this.sendTimer = setInterval(this.collectData.bind(this), this.periodMs)

lib/harvester.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,41 @@
22
* Copyright 2024 New Relic Corporation. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
65
'use strict'
6+
const defaultLogger = require('./logger').child({ component: 'harvester' })
77

88
/**
99
* @class
1010
* @classdesc Used to keep track of all registered aggregators.
1111
*/
1212
module.exports = class Harvester {
13-
constructor() {
13+
constructor({ logger = defaultLogger } = {}) {
1414
this.aggregators = []
15+
this.logger = logger
1516
}
1617

1718
/**
1819
* Calls start on every registered aggregator that is enabled.
1920
*/
2021
start() {
2122
for (const aggregator of this.aggregators) {
22-
if (aggregator.enabled) {
23+
if (aggregator.enabled && aggregator.delay > 0) {
24+
this.logger.debug(`Delay start of ${aggregator.method} by ${aggregator.delay} milliseconds`)
25+
const timeout = setTimeout(() => {
26+
aggregator.start()
27+
}, aggregator.delay)
28+
timeout.unref()
29+
} else if (aggregator.enabled) {
2330
aggregator.start()
2431
}
32+
33+
if (aggregator.enabled && aggregator.duration > 0) {
34+
this.logger.debug(`Running ${aggregator.method} for ${aggregator.duration} milliseconds`)
35+
const durationTimeout = setTimeout(() => {
36+
aggregator.stop()
37+
}, aggregator.delay + aggregator.duration)
38+
durationTimeout.unref()
39+
}
2540
}
2641
}
2742

lib/profiling/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ class ProfilingManager {
2020
start() {
2121
if (this.profilers.length === 0) {
2222
this.logger.warn('No profilers have been included in `config.profiling.include`, not starting any profilers.')
23-
return
23+
return false
2424
}
2525

2626
for (const profiler of this.profilers) {
2727
this.logger.debug(`Starting ${profiler.name}`)
2828
profiler.start()
2929
}
30+
return true
3031
}
3132

3233
stop() {

test/unit/aggregators/profiling-aggregator.test.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,14 @@ const sinon = require('sinon')
1111
const ProfilingAggregator = require('#agentlib/aggregators/profiling-aggregator.js')
1212
const helper = require('#testlib/agent_helper.js')
1313
const RUN_ID = 1337
14+
const createProfiler = require('../mocks/profiler')
1415

1516
test.beforeEach((ctx) => {
1617
const sandbox = sinon.createSandbox()
1718
const agent = helper.loadMockedAgent()
18-
const cpuProfiler = {
19-
name: 'CpuProfiler',
20-
stop: sandbox.stub(),
21-
collect() {
22-
return 'cpu profile data'
23-
}
24-
}
25-
19+
const cpuProfiler = createProfiler({ sandbox, name: 'CpuProfiler', data: 'cpu profile data' })
20+
const heapProfiler = createProfiler({ sandbox, name: 'HeapProfiler', data: 'heap profile data' })
2621
const clock = sinon.useFakeTimers()
27-
const heapProfiler = {
28-
name: 'HeapProfiler',
29-
stop: sandbox.stub(),
30-
collect() {
31-
return 'heap profile data'
32-
}
33-
}
3422
sandbox.spy(agent.collector, 'send')
3523
const profilingAggregator = new ProfilingAggregator({ runId: RUN_ID, periodMs: 100 }, agent)
3624
const profilingManager = profilingAggregator.profilingManager

test/unit/harvester.test.js

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class FakeAggregator extends EventEmitter {
1818
super()
1919
this.enabled = opts.enabled
2020
this.method = opts.method
21+
this.delay = opts.delay ?? 0
22+
this.duration = opts.duration ?? 0
2123
}
2224

2325
start() {}
@@ -40,19 +42,21 @@ function createAggregator(sandbox, opts) {
4042
}
4143

4244
test.beforeEach((ctx) => {
43-
ctx.nr = {}
44-
4545
const sandbox = sinon.createSandbox()
4646
const aggregators = [
4747
createAggregator(sandbox, { enabled: true, method: 'agg1' }),
4848
createAggregator(sandbox, { enabled: false, method: 'agg2' })
4949
]
50-
const harvester = new Harvester()
50+
const logger = require('./mocks/logger')(sandbox)
51+
const harvester = new Harvester({ logger })
5152
aggregators.forEach((a) => harvester.add(a))
5253

53-
ctx.nr.sandbox = sandbox
54-
ctx.nr.aggregators = aggregators
55-
ctx.nr.harvester = harvester
54+
ctx.nr = {
55+
sandbox,
56+
aggregators,
57+
harvester,
58+
logger
59+
}
5660
})
5761

5862
test.afterEach((ctx) => {
@@ -71,10 +75,11 @@ test('should add aggregator to this.aggregators', (t) => {
7175
})
7276

7377
test('should start all aggregators that are enabled', (t) => {
74-
const { harvester, aggregators } = t.nr
78+
const { harvester, aggregators, logger } = t.nr
7579
harvester.start()
7680
assert.equal(aggregators[0].start.callCount, 1, 'should start enabled aggregator')
7781
assert.equal(aggregators[1].start.callCount, 0, 'should not start disabled aggregator')
82+
assert.equal(logger.debug.callCount, 0)
7883
})
7984

8085
test('should stop all aggregators', (t) => {
@@ -103,3 +108,52 @@ test('resolve when all data is sent', async (t) => {
103108
})
104109
await promise
105110
})
111+
112+
test('should delay starting of aggregator when it has a delay property', (t) => {
113+
const { sandbox, logger } = t.nr
114+
const clock = sandbox.useFakeTimers()
115+
const delayAggregator = createAggregator(sandbox, { enabled: true, method: 'test-method', delay: 200 })
116+
const harvester = new Harvester({ logger })
117+
harvester.add(delayAggregator)
118+
harvester.start()
119+
assert.equal(logger.debug.callCount, 1)
120+
assert.equal(logger.debug.args[0][0], 'Delay start of test-method by 200 milliseconds')
121+
const { aggregators } = harvester
122+
assert.equal(aggregators[0].start.callCount, 0, 'should not start delayed aggregator yet')
123+
clock.tick(201)
124+
assert.equal(aggregators[0].start.callCount, 1, 'should start delayed aggregator after delay has elapsed')
125+
})
126+
127+
test('should stop aggregator dynamically when it has a duration property', (t) => {
128+
const { sandbox, logger } = t.nr
129+
const clock = sandbox.useFakeTimers()
130+
const delayAggregator = createAggregator(sandbox, { enabled: true, method: 'test-method', duration: 200 })
131+
const harvester = new Harvester({ logger })
132+
harvester.add(delayAggregator)
133+
harvester.start()
134+
assert.equal(logger.debug.callCount, 1)
135+
assert.equal(logger.debug.args[0][0], 'Running test-method for 200 milliseconds')
136+
const { aggregators } = harvester
137+
assert.equal(aggregators[0].start.callCount, 1, 'should start aggregator')
138+
assert.equal(aggregators[0].stop.callCount, 0, 'should not stop aggregator yet')
139+
clock.tick(201)
140+
assert.equal(aggregators[0].stop.callCount, 1, 'should stop aggregator after duration has elapsed')
141+
})
142+
143+
test('should delay start and stop aggregator after duration', (t) => {
144+
const { sandbox, logger } = t.nr
145+
const clock = sandbox.useFakeTimers()
146+
const delayAggregator = createAggregator(sandbox, { enabled: true, method: 'test-method', delay: 100, duration: 200 })
147+
const harvester = new Harvester({ logger })
148+
harvester.add(delayAggregator)
149+
harvester.start()
150+
assert.equal(logger.debug.callCount, 2)
151+
const { aggregators } = harvester
152+
assert.equal(aggregators[0].start.callCount, 0, 'should not start delayed aggregator yet')
153+
assert.equal(aggregators[0].stop.callCount, 0, 'should not stop aggregator yet')
154+
clock.tick(101)
155+
assert.equal(aggregators[0].start.callCount, 1, 'should start delayed aggregator after delay has elapsed')
156+
assert.equal(aggregators[0].stop.callCount, 0, 'should not stop aggregator yet')
157+
clock.tick(200)
158+
assert.equal(aggregators[0].stop.callCount, 1, 'should stop aggregator after duration has elapsed')
159+
})

test/unit/lib/profiling/index.test.js

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { describe, test } = require('node:test')
99
const assert = require('node:assert')
1010
const sinon = require('sinon')
1111
const ProfilingManager = require('#agentlib/profiling/index.js')
12+
const createProfiler = require('../../mocks/profiler')
1213

1314
test.beforeEach((ctx) => {
1415
const sandbox = sinon.createSandbox()
@@ -21,19 +22,8 @@ test.beforeEach((ctx) => {
2122
}
2223
}
2324
}
24-
const cpuProfiler = {
25-
name: 'cpu',
26-
start: sandbox.stub(),
27-
stop: sandbox.stub(),
28-
collect: sandbox.stub()
29-
}
30-
31-
const heapProfiler = {
32-
name: 'heap',
33-
start: sandbox.stub(),
34-
stop: sandbox.stub(),
35-
collect: sandbox.stub()
36-
}
25+
const cpuProfiler = createProfiler({ sandbox, name: 'cpu' })
26+
const heapProfiler = createProfiler({ sandbox, name: 'heap' })
3727
ctx.nr = {
3828
agent,
3929
cpuProfiler,
@@ -81,7 +71,8 @@ describe('start', () => {
8171
const { agent, logger } = t.nr
8272
const profilingManager = new ProfilingManager(agent, { logger })
8373

84-
profilingManager.start()
74+
const started = profilingManager.start()
75+
assert.equal(started, false)
8576

8677
assert.equal(logger.warn.callCount, 1)
8778
assert.ok(
@@ -95,8 +86,8 @@ describe('start', () => {
9586
const { agent, cpuProfiler, heapProfiler, logger } = t.nr
9687
const profilingManager = new ProfilingManager(agent, { logger })
9788
profilingManager.profilers = [cpuProfiler, heapProfiler]
98-
profilingManager.start()
99-
89+
const started = profilingManager.start()
90+
assert.equal(started, true)
10091
assert.equal(cpuProfiler.start.callCount, 1)
10192
assert.equal(heapProfiler.start.callCount, 1)
10293
assert.ok(

test/unit/mocks/profiler.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const sinon = require('sinon')
8+
const DEFAULT_DATA = Buffer.from('test-data')
9+
10+
function createProfiler({ sandbox = sinon, name = 'TestProfiler', data = DEFAULT_DATA } = {}) {
11+
return {
12+
name,
13+
start: sandbox.stub(),
14+
stop: sandbox.stub(),
15+
collect: sandbox.stub().returns(data)
16+
}
17+
}
18+
19+
module.exports = createProfiler

0 commit comments

Comments
 (0)