Skip to content

Commit 11d8a6b

Browse files
IlyasShabiBridgeAR
authored andcommitted
feat(profiling): add allocations profiling support (#8764)
1 parent 2623df7 commit 11d8a6b

13 files changed

Lines changed: 240 additions & 31 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
const childProcess = require('node:child_process')
5+
const { fork } = childProcess
6+
const path = require('node:path')
7+
8+
const satisfies = require('semifies')
9+
10+
const { Profile } = require('../../vendor/dist/pprof-format')
11+
const {
12+
FakeAgent,
13+
sandboxCwd,
14+
stopProc,
15+
useSandbox,
16+
} = require('../helpers')
17+
const { processExitPromise } = require('./helpers')
18+
19+
const TIMEOUT = 30000
20+
const isAtLeast26 = satisfies(process.versions.node, '>=26.0.0')
21+
22+
function getString (strings, value) {
23+
const index = typeof value?.toNumber === 'function' ? value.toNumber() : Number(value)
24+
return strings[index]
25+
}
26+
27+
function getSampleTypeNames (profile) {
28+
const strings = profile.stringTable.strings
29+
return profile.sampleType.map(sampleType => getString(strings, sampleType.type))
30+
}
31+
32+
function findFile (files, originalname) {
33+
const file = files.find(file => file.originalname === originalname)
34+
assert.ok(file, `Expected ${originalname} attachment`)
35+
return file
36+
}
37+
38+
function expectProfileUpload (agent) {
39+
let upload
40+
41+
const messagePromise = agent.assertMessageReceived(({ files }) => {
42+
assert.ok(files, 'Expected profiling upload')
43+
44+
upload = {
45+
event: JSON.parse(findFile(files, 'event.json').buffer.toString()),
46+
spaceProfile: Profile.decode(findFile(files, 'space.pprof').buffer),
47+
}
48+
}, TIMEOUT)
49+
50+
return messagePromise.then(() => upload)
51+
}
52+
53+
describe('allocation profiler', () => {
54+
let agent
55+
let cwd
56+
let proc
57+
let profilerTestFile
58+
59+
useSandbox()
60+
61+
before(() => {
62+
cwd = sandboxCwd()
63+
profilerTestFile = path.join(cwd, 'profiler/allocation.js')
64+
})
65+
66+
beforeEach(async () => {
67+
agent = await new FakeAgent().start()
68+
})
69+
70+
afterEach(async () => {
71+
await stopProc(proc)
72+
await agent.stop()
73+
})
74+
75+
it('sends heap profiles with the expected sample types on Node.js 26+', async function () {
76+
if (!isAtLeast26) {
77+
this.skip()
78+
return
79+
}
80+
81+
const cases = [
82+
{
83+
allocationProfilingEnabled: false,
84+
sampleTypes: ['objects', 'space'],
85+
},
86+
{
87+
allocationProfilingEnabled: true,
88+
sampleTypes: ['inuse_objects', 'alloc_objects', 'inuse_space', 'alloc_space'],
89+
},
90+
]
91+
92+
for (const { allocationProfilingEnabled, sampleTypes } of cases) {
93+
proc = fork(profilerTestFile, {
94+
cwd,
95+
env: {
96+
DD_TRACE_AGENT_PORT: agent.port,
97+
DD_PROFILING_ALLOCATION_ENABLED: allocationProfilingEnabled ? '1' : '0',
98+
DD_PROFILING_DEBUG_UPLOAD_COMPRESSION: 'off',
99+
DD_PROFILING_EXPORTERS: 'agent',
100+
DD_PROFILING_PROFILERS: 'space',
101+
DD_PROFILING_SOURCE_MAP: '0',
102+
DD_PROFILING_UPLOAD_PERIOD: '1',
103+
TEST_DURATION_MS: '5000',
104+
},
105+
})
106+
107+
const [
108+
{ event, spaceProfile },
109+
] = await Promise.all([
110+
expectProfileUpload(agent),
111+
processExitPromise(proc, TIMEOUT),
112+
])
113+
114+
assert.deepStrictEqual(event.attachments, ['space.pprof'])
115+
assert.strictEqual(event.info.profiler.settings.allocationProfilingEnabled, allocationProfilingEnabled)
116+
assert.deepStrictEqual(getSampleTypeNames(spaceProfile), sampleTypes)
117+
118+
await stopProc(proc)
119+
proc = undefined
120+
}
121+
})
122+
123+
it('does not crash when allocation profiling is requested on unsupported Node.js versions', async function () {
124+
if (isAtLeast26) {
125+
this.skip()
126+
return
127+
}
128+
129+
proc = fork(profilerTestFile, {
130+
cwd,
131+
env: {
132+
DD_PROFILING_ALLOCATION_ENABLED: '1',
133+
DD_PROFILING_DEBUG_UPLOAD_COMPRESSION: 'off',
134+
DD_PROFILING_EXPORTERS: 'file',
135+
DD_PROFILING_PROFILERS: 'space',
136+
DD_PROFILING_SOURCE_MAP: '0',
137+
DD_PROFILING_UPLOAD_PERIOD: '1',
138+
TEST_DURATION_MS: '5000',
139+
},
140+
})
141+
142+
await processExitPromise(proc, TIMEOUT)
143+
})
144+
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict'
2+
3+
require('dd-trace').init({ profiling: true })
4+
5+
const durationMs = Number.parseInt(process.env.TEST_DURATION_MS ?? '5000')
6+
7+
function runAllocations (ms) {
8+
return new Promise(resolve => {
9+
const allocations = []
10+
const end = Date.now() + ms
11+
12+
function work () {
13+
if (Date.now() >= end) {
14+
resolve()
15+
return
16+
}
17+
18+
for (let i = 0; i < 1000; i++) {
19+
allocations.push({ index: i, values: [i, i + 1, i + 2] })
20+
}
21+
22+
if (allocations.length > 10000) {
23+
allocations.splice(0, 5000)
24+
}
25+
26+
setImmediate(work)
27+
}
28+
29+
work()
30+
})
31+
}
32+
33+
runAllocations(durationMs).catch(err => {
34+
process.exitCode = 1
35+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict'
2+
3+
function processExitPromise (proc, timeout, expectBadExit = false) {
4+
return new Promise((resolve, reject) => {
5+
const timeoutObj = setTimeout(() => {
6+
reject(new Error('Process timed out'))
7+
}, timeout)
8+
9+
function checkExitCode (code) {
10+
clearTimeout(timeoutObj)
11+
12+
if ((code !== 0) !== expectBadExit) {
13+
reject(new Error(`Process exited with unexpected status code ${code}.`))
14+
} else {
15+
resolve()
16+
}
17+
}
18+
19+
proc
20+
.on('error', reject)
21+
.on('exit', checkExitCode)
22+
})
23+
}
24+
25+
module.exports = {
26+
processExitPromise,
27+
}

integration-tests/profiler/profiler.spec.js

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
assertObjectContains,
2020
stopProc,
2121
} = require('../helpers')
22+
const { processExitPromise } = require('./helpers')
2223

2324
const DEFAULT_PROFILE_TYPES = ['wall', 'space']
2425
if (process.platform !== 'win32') {
@@ -77,28 +78,6 @@ function expectProfileMessagePromise (agent, timeout,
7778
}, timeout, 1, true)
7879
}
7980

80-
function processExitPromise (proc, timeout, expectBadExit = false) {
81-
return new Promise((resolve, reject) => {
82-
const timeoutObj = setTimeout(() => {
83-
reject(new Error('Process timed out'))
84-
}, timeout)
85-
86-
function checkExitCode (code) {
87-
clearTimeout(timeoutObj)
88-
89-
if ((code !== 0) !== expectBadExit) {
90-
reject(new Error(`Process exited with unexpected status code ${code}.`))
91-
} else {
92-
resolve()
93-
}
94-
}
95-
96-
proc
97-
.on('error', reject)
98-
.on('exit', checkExitCode)
99-
})
100-
}
101-
10281
async function getLatestProfile (cwd, pattern) {
10382
const pprofCompressed = await readLatestFile(cwd, pattern)
10483
const pprofUncompressed = zlib[isAtLeast24 ? 'zstdDecompressSync' : 'gunzipSync'](pprofCompressed)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@
169169
"@datadog/native-iast-taint-tracking": "4.2.0",
170170
"@datadog/native-metrics": "3.1.2",
171171
"@datadog/openfeature-node-server": "2.0.0",
172-
"@datadog/pprof": "5.14.4",
172+
"@datadog/pprof": "5.15.0",
173173
"@datadog/wasm-js-rewriter": "5.0.1",
174174
"@opentelemetry/api": ">=1.0.0 <1.10.0",
175175
"@opentelemetry/api-logs": "<1.0.0",

packages/dd-trace/src/config/generated-config-types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export interface GeneratedConfig {
130130
DD_MINI_AGENT_PATH: string | undefined;
131131
DD_PIPELINE_EXECUTION_ID: string | undefined;
132132
DD_PLAYWRIGHT_WORKER: string | undefined;
133+
DD_PROFILING_ALLOCATION_ENABLED: boolean;
133134
DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED: boolean;
134135
DD_PROFILING_CODEHOTSPOTS_ENABLED: boolean;
135136
DD_PROFILING_CPU_ENABLED: boolean;

packages/dd-trace/src/config/supported-configurations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,13 @@
13081308
"default": null
13091309
}
13101310
],
1311+
"DD_PROFILING_ALLOCATION_ENABLED": [
1312+
{
1313+
"implementation": "A",
1314+
"type": "boolean",
1315+
"default": "false"
1316+
}
1317+
],
13111318
"DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED": [
13121319
{
13131320
"implementation": "A",

packages/dd-trace/src/profiling/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class Config {
8686

8787
this.timelineEnabled = options.DD_PROFILING_TIMELINE_ENABLED
8888
this.timelineSamplingEnabled = options.DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED
89+
this.allocationProfilingEnabled = options.DD_PROFILING_ALLOCATION_ENABLED
8990
this.codeHotspotsEnabled = options.DD_PROFILING_CODEHOTSPOTS_ENABLED
9091
this.cpuProfilingEnabled = options.DD_PROFILING_CPU_ENABLED
9192
this.heapSamplingInterval = options.DD_PROFILING_HEAP_SAMPLING_INTERVAL
@@ -139,6 +140,7 @@ class Config {
139140

140141
get systemInfoReport () {
141142
const report = {
143+
allocationProfilingEnabled: this.allocationProfilingEnabled,
142144
asyncContextFrameEnabled: this.asyncContextFrameEnabled,
143145
codeHotspotsEnabled: this.codeHotspotsEnabled,
144146
cpuProfilingEnabled: this.cpuProfilingEnabled,

packages/dd-trace/src/profiling/profilers/space.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ class NativeSpaceProfiler {
1313
#mapper
1414
#oomMonitoring
1515
#pprof
16+
#allocationProfilingEnabled = false
1617
#samplingInterval = 512 * 1024
1718
#started = false
1819

1920
constructor (options = {}) {
2021
// TODO: Remove default value. It is only used in testing.
2122
this.#samplingInterval = options.heapSamplingInterval || 512 * 1024
23+
this.#allocationProfilingEnabled = options.allocationProfilingEnabled
2224
this.#oomMonitoring = options.oomMonitoring || {}
2325
}
2426

@@ -31,7 +33,7 @@ class NativeSpaceProfiler {
3133

3234
this.#mapper = mapper
3335
this.#pprof = require('@datadog/pprof')
34-
this.#pprof.heap.start(this.#samplingInterval, STACK_DEPTH)
36+
this.#pprof.heap.start(this.#samplingInterval, STACK_DEPTH, this.#allocationProfilingEnabled)
3537
if (this.#oomMonitoring.enabled) {
3638
const strategies = this.#oomMonitoring.exportStrategies
3739
this.#pprof.heap.monitorOutOfMemory(

packages/dd-trace/test/config/index.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,7 @@ describe('Config', () => {
10161016
{ name: 'plugins', value: true, origin: 'default' },
10171017
{ name: 'DD_TRACE_AGENT_PORT', value: 8126, origin: 'default' },
10181018
{ name: 'DD_PROFILING_ENABLED', value: 'false', origin: 'default' },
1019+
{ name: 'DD_PROFILING_ALLOCATION_ENABLED', value: false, origin: 'default' },
10191020
{ name: 'DD_PROFILING_EXPORTERS', value: 'agent', origin: 'default' },
10201021
{ name: 'DD_PROFILING_SOURCE_MAP', value: true, origin: 'default' },
10211022
{ name: 'DD_TRACE_AGENT_PROTOCOL_VERSION', value: '0.4', origin: 'default' },

0 commit comments

Comments
 (0)