Skip to content

Commit f236bca

Browse files
committed
Allow the CPU profiler to store sample context in the async context frame
1 parent efefba5 commit f236bca

File tree

4 files changed

+129
-40
lines changed

4 files changed

+129
-40
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const { tagger } = require('./tagger')
1616
const { isFalse, isTrue } = require('../util')
1717
const { getAzureTagsFromMetadata, getAzureAppMetadata } = require('../azure_metadata')
1818
const { getEnvironmentVariables } = require('../config-helper')
19+
const satisfies = require('semifies')
1920

2021
class Config {
2122
constructor (options = {}) {
@@ -41,6 +42,7 @@ class Config {
4142
DD_PROFILING_TIMELINE_ENABLED,
4243
DD_PROFILING_UPLOAD_PERIOD,
4344
DD_PROFILING_UPLOAD_TIMEOUT,
45+
DD_PROFILING_USE_ASYNC_CONTEXT_FRAME,
4446
DD_PROFILING_V8_PROFILER_BUG_WORKAROUND,
4547
DD_PROFILING_WALLTIME_ENABLED,
4648
DD_SERVICE,
@@ -207,6 +209,29 @@ class Config {
207209

208210
this.uploadCompression = { method: uploadCompression, level }
209211

212+
const that = this
213+
function turnOffAsyncContextFrame (msg) {
214+
that.logger.warn(
215+
`DD_PROFILING_USE_ASYNC_CONTEXT_FRAME was set ${msg}, it will have no effect.`)
216+
that.useAsyncContextFrame = false
217+
}
218+
219+
this.useAsyncContextFrame = isTrue(coalesce(options.useAsyncContextFrame,
220+
DD_PROFILING_USE_ASYNC_CONTEXT_FRAME, false))
221+
if (this.useAsyncContextFrame) {
222+
if (satisfies(process.versions.node, '>=24.0.0')) {
223+
if (process.execArgv.includes('--no-async-context-frame')) {
224+
turnOffAsyncContextFrame('with --no-async-context-frame')
225+
}
226+
} else if (satisfies(process.versions.node, '>=23.0.0')) {
227+
if (!process.execArgv.includes('--experimental-async-context-frame')) {
228+
turnOffAsyncContextFrame('without --experimental-async-context-frame')
229+
}
230+
} else {
231+
turnOffAsyncContextFrame('but it requires at least Node 23')
232+
}
233+
}
234+
210235
this.profilers = ensureProfilers(profilers, this)
211236
}
212237
}

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

Lines changed: 97 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const {
1616

1717
const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils')
1818

19-
const beforeCh = dc.channel('dd-trace:storage:before')
19+
let beforeCh
2020
const enterCh = dc.channel('dd-trace:storage:enter')
2121
const spanFinishCh = dc.channel('dd-trace:span:finish')
2222
const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
@@ -30,14 +30,39 @@ function getActiveSpan () {
3030
return store && store.span
3131
}
3232

33+
function updateContext (context) {
34+
// Converting spanIDs to strings is not necessary as generateLabels will do it
35+
// too. When we don't use async context frame, we can convert them when the
36+
// sample is taken though so we amortize the latency of operations. It is an
37+
// optimization.
38+
if (typeof context.spanId === 'object') {
39+
context.spanId = context.spanId.toString(10)
40+
}
41+
if (typeof context.rootSpanId === 'object') {
42+
context.rootSpanId = context.rootSpanId.toString(10)
43+
}
44+
if (context.webTags !== undefined && context.endpoint === undefined) {
45+
// endpoint may not be determined yet, but keep it as fallback
46+
// if tags are not available anymore during serialization
47+
context.endpoint = endpointNameFromTags(context.webTags)
48+
}
49+
}
50+
3351
let channelsActivated = false
34-
function ensureChannelsActivated () {
52+
function ensureChannelsActivated (useAsyncContextFrame) {
3553
if (channelsActivated) return
3654

37-
const { AsyncLocalStorage, createHook } = require('async_hooks')
3855
const shimmer = require('../../../../datadog-shimmer')
3956

40-
createHook({ before: () => beforeCh.publish() }).enable()
57+
// When using the async context frame to store sample context (available with
58+
// Node 24), we do not need to use the async hooks anymore.
59+
if (!useAsyncContextFrame) {
60+
const { createHook } = require('async_hooks')
61+
beforeCh = dc.channel('dd-trace:storage:before')
62+
createHook({ before: () => beforeCh.publish() }).enable()
63+
}
64+
65+
const { AsyncLocalStorage } = require('async_hooks')
4166

4267
let inRun = false
4368
shimmer.wrap(AsyncLocalStorage.prototype, 'enterWith', function (original) {
@@ -48,22 +73,27 @@ function ensureChannelsActivated () {
4873
}
4974
})
5075

51-
shimmer.wrap(AsyncLocalStorage.prototype, 'run', function (original) {
52-
return function (store, callback, ...args) {
53-
const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
54-
inRun = false
55-
enterCh.publish()
56-
const retVal = cb.apply(this, args)
76+
// The AsyncContextFrame-based implementation of AsyncLocalStorage.run()
77+
// delegates to AsyncLocalStorage.enterWith() so it doesn't need to be
78+
// separately instrumented.
79+
if (!useAsyncContextFrame) {
80+
shimmer.wrap(AsyncLocalStorage.prototype, 'run', function (original) {
81+
return function (store, callback, ...args) {
82+
const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
83+
inRun = false
84+
enterCh.publish()
85+
const retVal = cb.apply(this, args)
86+
inRun = true
87+
return retVal
88+
})
5789
inRun = true
90+
const retVal = original.call(this, store, wrappedCb, ...args)
91+
enterCh.publish()
92+
inRun = false
5893
return retVal
59-
})
60-
inRun = true
61-
const retVal = original.call(this, store, wrappedCb, ...args)
62-
enterCh.publish()
63-
inRun = false
64-
return retVal
65-
}
66-
})
94+
}
95+
})
96+
}
6797

6898
channelsActivated = true
6999
}
@@ -77,6 +107,8 @@ class NativeWallProfiler {
77107
this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
78108
this._timelineEnabled = !!options.timelineEnabled
79109
this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
110+
this._useAsyncContextFrame = !!options.useAsyncContextFrame
111+
80112
// We need to capture span data into the sample context for either code hotspots
81113
// or endpoint collection.
82114
this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
@@ -130,19 +162,24 @@ class NativeWallProfiler {
130162
withContexts: this._withContexts,
131163
lineNumbers: false,
132164
workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled,
133-
collectCpuTime: this._cpuProfilingEnabled
165+
collectCpuTime: this._cpuProfilingEnabled,
166+
useCPED: this._useAsyncContextFrame
134167
})
135168

136169
if (this._withContexts) {
137-
this._setNewContext()
170+
if (!this._useAsyncContextFrame) {
171+
this._setNewContext()
172+
}
138173

139174
if (this._captureSpanData) {
140175
this._profilerState = this._pprof.time.getState()
141176
this._lastSampleCount = 0
142177

143-
ensureChannelsActivated()
178+
ensureChannelsActivated(this._useAsyncContextFrame)
144179

145-
beforeCh.subscribe(this._enter)
180+
if (!this._useAsyncContextFrame) {
181+
beforeCh.subscribe(this._enter)
182+
}
146183
enterCh.subscribe(this._enter)
147184
spanFinishCh.subscribe(this._spanFinished)
148185
}
@@ -154,17 +191,37 @@ class NativeWallProfiler {
154191
_enter () {
155192
if (!this._started) return
156193

157-
const sampleCount = this._profilerState[kSampleCount]
158-
if (sampleCount !== this._lastSampleCount) {
159-
this._lastSampleCount = sampleCount
160-
const context = this._currentContext.ref
161-
this._setNewContext()
194+
const span = getActiveSpan()
195+
const sampleContext = span ? this._getProfilingContext(span) : {}
196+
197+
// Note that we store the sample context differently with and without the
198+
// async context frame. With the async context frame, we tell the profiler
199+
// to store the sample context directly in the frame on each enterWith.
200+
// Without the async context frame, we store one holder object as the
201+
// profiler's single sample context, and reassign its "ref" property on
202+
// every async context change. Then when we detect that the profiler took a
203+
// sample (and thus bound the holder as that sample's context), we create a
204+
// new holder object so that we no longer mutate the old one. This is really
205+
// an optimization to avoid going to profiler's native SetContext every
206+
// time. With async context frame however, we can't have that optimization,
207+
// as we can't tell from which async context frame was the sampling context
208+
// taken. For the same reason we can't call updateContext() on the old
209+
// context -- we simply can't tell which one it might've been across all
210+
// possible async context frames.
211+
if (this._useAsyncContextFrame) {
212+
this._pprof.time.setContext(sampleContext)
213+
} else {
214+
const sampleCount = this._profilerState[kSampleCount]
215+
if (sampleCount !== this._lastSampleCount) {
216+
this._lastSampleCount = sampleCount
217+
const context = this._currentContext.ref
218+
this._setNewContext()
162219

163-
this._updateContext(context)
164-
}
220+
updateContext(context)
221+
}
165222

166-
const span = getActiveSpan()
167-
this._currentContext.ref = span ? this._getProfilingContext(span) : {}
223+
this._currentContext.ref = sampleContext
224+
}
168225
}
169226

170227
_getProfilingContext (span) {
@@ -245,7 +302,7 @@ class NativeWallProfiler {
245302
_stop (restart) {
246303
if (!this._started) return
247304

248-
if (this._captureSpanData) {
305+
if (this._captureSpanData && !this._useAsyncContextFrame) {
249306
// update last sample context if needed
250307
this._enter()
251308
this._lastSampleCount = 0
@@ -259,7 +316,9 @@ class NativeWallProfiler {
259316
}
260317
} else {
261318
if (this._captureSpanData) {
262-
beforeCh.unsubscribe(this._enter)
319+
if (!this._useAsyncContextFrame) {
320+
beforeCh.unsubscribe(this._enter)
321+
}
263322
enterCh.unsubscribe(this._enter)
264323
spanFinishCh.unsubscribe(this._spanFinished)
265324
this._profilerState = undefined
@@ -296,19 +355,20 @@ class NativeWallProfiler {
296355
}
297356

298357
// Native profiler doesn't set context.context for some samples, such as idle samples or when
299-
// the context was otherwise unavailable when the sample was taken.
300-
const ref = context.context?.ref
358+
// the context was otherwise unavailable when the sample was taken. Note that with async context
359+
// frame, we don't use the "ref" indirection.
360+
const ref = this._useAsyncContextFrame ? context.context : context.context?.ref
301361
if (typeof ref !== 'object') {
302362
return labels
303363
}
304364

305365
const { spanId, rootSpanId, webTags, endpoint } = ref
306366

307367
if (spanId !== undefined) {
308-
labels[SPAN_ID_LABEL] = spanId
368+
labels[SPAN_ID_LABEL] = typeof spanId === 'object' ? spanId.toString(10) : spanId
309369
}
310370
if (rootSpanId !== undefined) {
311-
labels[LOCAL_ROOT_SPAN_ID_LABEL] = rootSpanId
371+
labels[LOCAL_ROOT_SPAN_ID_LABEL] = typeof rootSpanId === 'object' ? rootSpanId.toString(10) : rootSpanId
312372
}
313373
if (webTags !== undefined && Object.keys(webTags).length !== 0) {
314374
labels['trace endpoint'] = endpointNameFromTags(webTags)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"DD_PROFILING_PROFILERS": ["A"],
127127
"DD_PROFILING_SOURCE_MAP": ["A"],
128128
"DD_PROFILING_UPLOAD_PERIOD": ["A"],
129+
"DD_PROFILING_USE_ASYNC_CONTEXT_FRAME": ["A"],
129130
"DD_PROFILING_V8_PROFILER_BUG_WORKAROUND": ["A"],
130131
"DD_PROFILING_WALLTIME_ENABLED": ["A"],
131132
"DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": ["A"],

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ describe('profilers/native/wall', () => {
6161
withContexts: false,
6262
lineNumbers: false,
6363
workaroundV8Bug: false,
64-
collectCpuTime: false
64+
collectCpuTime: false,
65+
useCPED: false
6566
})
6667
})
6768

@@ -80,7 +81,8 @@ describe('profilers/native/wall', () => {
8081
withContexts: false,
8182
lineNumbers: false,
8283
workaroundV8Bug: false,
83-
collectCpuTime: false
84+
collectCpuTime: false,
85+
useCPED: false
8486
})
8587
})
8688

@@ -176,7 +178,8 @@ describe('profilers/native/wall', () => {
176178
withContexts: false,
177179
lineNumbers: false,
178180
workaroundV8Bug: false,
179-
collectCpuTime: false
181+
collectCpuTime: false,
182+
useCPED: false
180183
})
181184
})
182185

0 commit comments

Comments
 (0)