Skip to content

Commit 9ec7ef9

Browse files
authored
Start collecting async IDs in profiles (#5524)
* Upgrade to profiler 5.7.0 for asyncId collection capability * Add a config option to disable async ID collection * Alphabetically sort some long option lists * Add async ID assertions to the integration test * Only enable the feature for Node.js version >=22.10.0
1 parent c9c351d commit 9ec7ef9

File tree

7 files changed

+66
-23
lines changed

7 files changed

+66
-23
lines changed

integration-tests/profiler/codehotspots.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@ const tracer = DDTrace.init()
66

77
function busyLoop () {
88
const start = process.hrtime.bigint()
9+
let x = 0
910
for (;;) {
1011
const now = process.hrtime.bigint()
1112
// Busy cycle for 100ms
1213
if (now - start > 100000000n) {
1314
break
1415
}
16+
// Do something in addition to invoking hrtime
17+
for (let i = 0; i < 1000; i++) {
18+
x += Math.sqrt(Math.random() * 2 - 1)
19+
}
1520
}
21+
return x
1622
}
1723

1824
let counter = 0

integration-tests/profiler/profiler.spec.js

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,10 @@ describe('profiler', () => {
326326
// 'local root span id's
327327
// - samples with spans also must have a 'trace endpoint' label with values 'endpoint-0',
328328
// 'endpoint-1', or 'endpoint-2'
329-
// - every occurrence of a span must have the same root span and endpoint
329+
// - every occurrence of a span must have the same root span, endpoint, and asyncId
330330
const rootSpans = new Set()
331331
const endpoints = new Set()
332+
const asyncIds = new Set()
332333
const spans = new Map()
333334
const strings = profile.stringTable
334335
const tsKey = strings.dedup('end_timestamp_ns')
@@ -338,11 +339,13 @@ describe('profiler', () => {
338339
const threadNameKey = strings.dedup('thread name')
339340
const threadIdKey = strings.dedup('thread id')
340341
const osThreadIdKey = strings.dedup('os thread id')
342+
const asyncIdKey = strings.dedup('async id')
341343
const threadNameValue = strings.dedup('Main Event Loop')
342344
const nonJSThreadNameValue = strings.dedup('Non-JS threads')
343345

346+
const asyncIdWorks = require('semifies')(process.versions.node, '>=22.10.0')
344347
for (const sample of profile.sample) {
345-
let ts, spanId, rootSpanId, endpoint, threadName, threadId, osThreadId
348+
let ts, spanId, rootSpanId, endpoint, threadName, threadId, osThreadId, asyncId
346349
for (const label of sample.label) {
347350
switch (label.key) {
348351
case tsKey: ts = label.num; break
@@ -352,6 +355,12 @@ describe('profiler', () => {
352355
case threadNameKey: threadName = label.str; break
353356
case threadIdKey: threadId = label.str; break
354357
case osThreadIdKey: osThreadId = label.str; break
358+
case asyncIdKey:
359+
asyncId = label.num
360+
if (asyncId === 0) {
361+
asyncId = undefined
362+
}
363+
break
355364
default: assert.fail(`Unexpected label key ${strings.dedup(label.key)} ${encoded}`)
356365
}
357366
}
@@ -385,11 +394,27 @@ describe('profiler', () => {
385394
// 3 of them.
386395
continue
387396
}
388-
const spanData = { rootSpanId, endpoint }
397+
const spanData = { rootSpanId, endpoint, asyncId }
398+
// Record async ID so we can verify we encountered 9 different values.
399+
// Async ID can be sporadically missing if sampling hits an intrinsified
400+
// function.
401+
if (asyncId !== undefined) {
402+
asyncIds.add(asyncId)
403+
}
389404
const existingSpanData = spans.get(spanId)
390405
if (existingSpanData) {
391-
// Span's root span and endpoint must be consistent across samples
392-
assert.deepEqual(spanData, existingSpanData, encoded)
406+
// Span's root span, endpoint, and async ID must be consistent
407+
// across samples.
408+
assert.equal(existingSpanData.rootSpanId, rootSpanId, encoded)
409+
assert.equal(existingSpanData.endpoint, endpoint, encoded)
410+
if (asyncIdWorks) {
411+
// Account for asyncID sporadically missing
412+
if (existingSpanData.asyncId === undefined) {
413+
existingSpanData.asyncId = asyncId
414+
} else if (asyncId !== undefined) {
415+
assert.equal(existingSpanData.asyncId, asyncId, encoded)
416+
}
417+
}
393418
} else {
394419
// New span id, store span data
395420
spans.set(spanId, spanData)
@@ -407,9 +432,10 @@ describe('profiler', () => {
407432
}
408433
}
409434
}
410-
// Need to have a total of 9 different spans, with 3 different root spans
411-
// and 3 different endpoints.
435+
// Need to have a total of 9 different spans, with 9 different async IDs,
436+
// 3 different root spans, and 3 different endpoints.
412437
assert.equal(spans.size, 9, encoded)
438+
assert.equal(asyncIds.size, asyncIdWorks ? 9 : 0, encoded)
413439
assert.equal(rootSpans.size, 3, encoded)
414440
assert.equal(endpoints.size, 3, encoded)
415441
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
"@datadog/wasm-js-rewriter": "3.1.0",
9090
"@datadog/native-iast-taint-tracking": "3.3.0",
9191
"@datadog/native-metrics": "^3.1.0",
92-
"@datadog/pprof": "5.6.0",
92+
"@datadog/pprof": "5.7.0",
9393
"@datadog/sketches-js": "^2.1.0",
9494
"@isaacs/ttlcache": "^1.4.1",
9595
"@opentelemetry/api": ">=1.0.0 <1.9.0",

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
1515
const { tagger } = require('./tagger')
1616
const { isFalse, isTrue } = require('../util')
1717
const { getAzureTagsFromMetadata, getAzureAppMetadata } = require('../azure_metadata')
18+
const satisfies = require('semifies')
1819

1920
class Config {
2021
constructor (options = {}) {
2122
const {
2223
DD_AGENT_HOST,
2324
DD_ENV,
2425
DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing
26+
DD_PROFILING_ASYNC_ID_ENABLED,
2527
DD_PROFILING_CODEHOTSPOTS_ENABLED,
2628
DD_PROFILING_CPU_ENABLED,
2729
DD_PROFILING_DEBUG_SOURCE_MAPS,
@@ -179,6 +181,10 @@ class Config {
179181
this.timelineSamplingEnabled = isTrue(coalesce(options.timelineSamplingEnabled,
180182
DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, true))
181183

184+
// Async ID gathering only works reliably on Node >= 22.10.0
185+
this.asyncIdEnabled = isTrue(coalesce(options.asyncIdEnabled,
186+
DD_PROFILING_ASYNC_ID_ENABLED, this.timelineEnabled && satisfies(process.versions.node, '>=22.10.0')))
187+
182188
this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
183189
DD_PROFILING_CODEHOTSPOTS_ENABLED,
184190
DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, samplingContextsAvailable))

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,14 @@ function ensureChannelsActivated () {
7070
class NativeWallProfiler {
7171
constructor (options = {}) {
7272
this.type = 'wall'
73-
this._samplingIntervalMicros = options.samplingInterval || 1e6 / 99 // 99hz
74-
this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
73+
this._asyncIdEnabled = !!options.asyncIdEnabled
7574
this._codeHotspotsEnabled = !!options.codeHotspotsEnabled
75+
this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
7676
this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
77+
this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
78+
this._samplingIntervalMicros = options.samplingInterval || 1e6 / 99 // 99hz
7779
this._timelineEnabled = !!options.timelineEnabled
78-
this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
80+
this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
7981
// We need to capture span data into the sample context for either code hotspots
8082
// or endpoint collection.
8183
this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
@@ -84,7 +86,6 @@ class NativeWallProfiler {
8486
// timestamps require the sample contexts feature in the pprof wall profiler), or
8587
// cpu profiling is enabled.
8688
this._withContexts = this._captureSpanData || this._timelineEnabled || this._cpuProfilingEnabled
87-
this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
8889
this._mapper = undefined
8990
this._pprof = undefined
9091

@@ -127,13 +128,14 @@ class NativeWallProfiler {
127128
}
128129

129130
this._pprof.time.start({
130-
intervalMicros: this._samplingIntervalMicros,
131+
collectAsyncId: this._asyncIdEnabled,
132+
collectCpuTime: this._cpuProfilingEnabled,
131133
durationMillis: this._flushIntervalMillis,
134+
intervalMicros: this._samplingIntervalMicros,
135+
lineNumbers: false,
132136
sourceMapper: this._mapper,
133137
withContexts: this._withContexts,
134-
lineNumbers: false,
135-
workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled,
136-
collectCpuTime: this._cpuProfilingEnabled
138+
workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled
137139
})
138140

139141
if (this._withContexts) {

packages/dd-trace/test/profiling/profilers/wall.spec.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ describe('profilers/native/wall', () => {
5858
withContexts: false,
5959
lineNumbers: false,
6060
workaroundV8Bug: false,
61-
collectCpuTime: false
61+
collectCpuTime: false,
62+
collectAsyncId: false
6263
})
6364
})
6465

@@ -77,7 +78,8 @@ describe('profilers/native/wall', () => {
7778
withContexts: false,
7879
lineNumbers: false,
7980
workaroundV8Bug: false,
80-
collectCpuTime: false
81+
collectCpuTime: false,
82+
collectAsyncId: false
8183
})
8284
})
8385

@@ -173,7 +175,8 @@ describe('profilers/native/wall', () => {
173175
withContexts: false,
174176
lineNumbers: false,
175177
workaroundV8Bug: false,
176-
collectCpuTime: false
178+
collectCpuTime: false,
179+
collectAsyncId: false
177180
})
178181
})
179182

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,10 @@
383383
node-addon-api "^6.1.0"
384384
node-gyp-build "^3.9.0"
385385

386-
"@datadog/pprof@5.6.0":
387-
version "5.6.0"
388-
resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.6.0.tgz#b6f5c566512ba5e55c6dbf46e9f0f020cfd5c6b5"
389-
integrity sha512-x7yN0s4wMnRqv3PWQ6eXKH5XE5qvCOwWbOsXqpT2Irbsc7Wcl5w5JrJUcbPCdSJGihpIh6kAeIrS6w/ZCcHy2Q==
386+
"@datadog/pprof@5.7.0":
387+
version "5.7.0"
388+
resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.7.0.tgz#3dde6d0cce023091f30effc0aab0c896ad2d7fe3"
389+
integrity sha512-+pTeElxHwNUQrqY11f6MUUJDk9pMTIvu0LZhEpDZPphq8WDNU9OBvqFFfX9YdQI/nLZAe4L8t2CEgeJJwzW5EQ==
390390
dependencies:
391391
delay "^5.0.0"
392392
node-gyp-build "<4.0"

0 commit comments

Comments
 (0)