Skip to content

Commit 13c325f

Browse files
committed
Allow the CPU profiler to store sample context in the async context frame
1 parent c47766f commit 13c325f

File tree

2 files changed

+151
-44
lines changed

2 files changed

+151
-44
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ 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 = {}) {
@@ -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,
@@ -191,6 +193,27 @@ class Config {
191193
logExperimentalVarDeprecation('CPU_ENABLED')
192194
checkOptionWithSamplingContextAllowed(this.cpuProfilingEnabled, 'CPU profiling')
193195

196+
function turnOffAsyncContextFrame (that, msg) {
197+
logger.warn(
198+
`DD_PROFILING_USE_ASYNC_CONTEXT_FRAME was set ${msg}, it will have no effect.`)
199+
that.useAsyncContextFrame = false
200+
}
201+
202+
this.useAsyncContextFrame = isTrue(coalesce(options.useAsyncContextFrame,
203+
DD_PROFILING_USE_ASYNC_CONTEXT_FRAME, false))
204+
if (this.useAsyncContextFrame) {
205+
if (satisfies(process.versions.node, '>=24.0.0')) {
206+
if (process.execArgv.includes('--no-async-context-frame')) {
207+
turnOffAsyncContextFrame(this, 'with --no-async-context-frame')
208+
}
209+
} else if (satisfies(process.versions.node, '>=23.0.0')) {
210+
if (!process.execArgv.includes('--experimental-async-context-frame')) {
211+
turnOffAsyncContextFrame(this, 'without --experimental-async-context-frame')
212+
}
213+
} else {
214+
turnOffAsyncContextFrame(this, 'but it requires at least Node 23')
215+
}
216+
}
194217
this.profilers = ensureProfilers(profilers, this)
195218
}
196219
}

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

Lines changed: 128 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const {
1515

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

18-
const beforeCh = dc.channel('dd-trace:storage:before')
18+
let beforeCh
1919
const enterCh = dc.channel('dd-trace:storage:enter')
2020
const spanFinishCh = dc.channel('dd-trace:span:finish')
2121
const profilerTelemetryMetrics = telemetryMetrics.manager.namespace('profilers')
@@ -29,40 +29,94 @@ function getActiveSpan () {
2929
return store && store.span
3030
}
3131

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

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

39-
createHook({ before: () => beforeCh.publish() }).enable()
56+
let interceptingAsyncContextFrameSet = false
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+
} else if (process.execArgv.includes('--expose-internals')) {
64+
const AsyncContextFrame = require('internal/async_context_frame')
65+
shimmer.wrap(AsyncContextFrame, 'set', function (original) {
66+
return function (frame) {
67+
original.call(this, frame)
68+
enterCh.publish()
69+
}
70+
})
71+
interceptingAsyncContextFrameSet = true
72+
}
4073

41-
let inRun = false
42-
shimmer.wrap(AsyncLocalStorage.prototype, 'enterWith', function (original) {
43-
return function (...args) {
44-
const retVal = original.apply(this, args)
45-
if (!inRun) enterCh.publish()
46-
return retVal
47-
}
48-
})
74+
if (!interceptingAsyncContextFrameSet) {
75+
const { AsyncLocalStorage, AsyncResource } = require('async_hooks')
4976

50-
shimmer.wrap(AsyncLocalStorage.prototype, 'run', function (original) {
51-
return function (store, callback, ...args) {
52-
const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
53-
inRun = false
77+
let inRun = false
78+
shimmer.wrap(AsyncLocalStorage.prototype, 'enterWith', function (original) {
79+
return function (...args) {
80+
const retVal = original.apply(this, args)
81+
if (!inRun) enterCh.publish()
82+
return retVal
83+
}
84+
})
85+
86+
shimmer.wrap(AsyncLocalStorage.prototype, 'run', function (original) {
87+
return function (store, callback, ...args) {
88+
const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
89+
inRun = false
90+
enterCh.publish()
91+
const retVal = cb.apply(this, args)
92+
inRun = true
93+
return retVal
94+
})
95+
inRun = true
96+
const retVal = original.call(this, store, wrappedCb, ...args)
5497
enterCh.publish()
55-
const retVal = cb.apply(this, args)
98+
inRun = false
99+
return retVal
100+
}
101+
})
102+
103+
shimmer.wrap(AsyncResource.prototype, 'runInAsyncScope', function (original) {
104+
return function (callback, thisArg, ...args) {
105+
const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
106+
inRun = false
107+
enterCh.publish()
108+
const retVal = cb.apply(this, args)
109+
inRun = true
110+
return retVal
111+
})
56112
inRun = true
113+
const retVal = original.call(this, wrappedCb, thisArg, ...args)
114+
enterCh.publish()
115+
inRun = false
57116
return retVal
58-
})
59-
inRun = true
60-
const retVal = original.call(this, store, wrappedCb, ...args)
61-
enterCh.publish()
62-
inRun = false
63-
return retVal
64-
}
65-
})
117+
}
118+
})
119+
}
66120

67121
channelsActivated = true
68122
}
@@ -76,6 +130,8 @@ class NativeWallProfiler {
76130
this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
77131
this._timelineEnabled = !!options.timelineEnabled
78132
this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
133+
this._useAsyncContextFrame = !!options.useAsyncContextFrame
134+
79135
// We need to capture span data into the sample context for either code hotspots
80136
// or endpoint collection.
81137
this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
@@ -131,19 +187,24 @@ class NativeWallProfiler {
131187
withContexts: this._withContexts,
132188
lineNumbers: false,
133189
workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled,
134-
collectCpuTime: this._cpuProfilingEnabled
190+
collectCpuTime: this._cpuProfilingEnabled,
191+
useCPED: this._useAsyncContextFrame
135192
})
136193

137194
if (this._withContexts) {
138-
this._setNewContext()
195+
if (!this._useAsyncContextFrame) {
196+
this._setNewContext()
197+
}
139198

140199
if (this._captureSpanData) {
141200
this._profilerState = this._pprof.time.getState()
142201
this._lastSampleCount = 0
143202

144-
ensureChannelsActivated()
203+
ensureChannelsActivated(this._useAsyncContextFrame)
145204

146-
beforeCh.subscribe(this._enter)
205+
if (!this._useAsyncContextFrame) {
206+
beforeCh.subscribe(this._enter)
207+
}
147208
enterCh.subscribe(this._enter)
148209
spanFinishCh.subscribe(this._spanFinished)
149210
}
@@ -155,17 +216,37 @@ class NativeWallProfiler {
155216
_enter () {
156217
if (!this._started) return
157218

158-
const sampleCount = this._profilerState[kSampleCount]
159-
if (sampleCount !== this._lastSampleCount) {
160-
this._lastSampleCount = sampleCount
161-
const context = this._currentContext.ref
162-
this._setNewContext()
219+
const span = getActiveSpan()
220+
const sampleContext = span ? this._getProfilingContext(span) : {}
221+
222+
// Note that we store the sample context differently with and without the
223+
// async context frame. With the async context frame, we tell the profiler
224+
// to store the sample context directly in the frame on each enterWith.
225+
// Without the async context frame, we store one holder object as the
226+
// profiler's single sample context, and reassign its "ref" property on
227+
// every async context change. Then when we detect that the profiler took a
228+
// sample (and thus bound the holder as that sample's context), we create a
229+
// new holder object so that we no longer mutate the old one. This is really
230+
// an optimization to avoid going to profiler's native SetContext every
231+
// time. With async context frame however, we can't have that optimization,
232+
// as we can't tell from which async context frame was the sampling context
233+
// taken. For the same reason we can't call updateContext() on the old
234+
// context -- we simply can't tell which one it might've been across all
235+
// possible async context frames.
236+
if (this._useAsyncContextFrame) {
237+
this._pprof.time.setContext(sampleContext)
238+
} else {
239+
const sampleCount = this._profilerState[kSampleCount]
240+
if (sampleCount !== this._lastSampleCount) {
241+
this._lastSampleCount = sampleCount
242+
const context = this._currentContext.ref
243+
this._setNewContext()
163244

164-
this._updateContext(context)
165-
}
245+
updateContext(context)
246+
}
166247

167-
const span = getActiveSpan()
168-
this._currentContext.ref = span ? this._getProfilingContext(span) : {}
248+
this._currentContext.ref = sampleContext
249+
}
169250
}
170251

171252
_getProfilingContext (span) {
@@ -246,7 +327,7 @@ class NativeWallProfiler {
246327
_stop (restart) {
247328
if (!this._started) return
248329

249-
if (this._captureSpanData) {
330+
if (this._captureSpanData && !this._useAsyncContextFrame) {
250331
// update last sample context if needed
251332
this._enter()
252333
this._lastSampleCount = 0
@@ -260,7 +341,9 @@ class NativeWallProfiler {
260341
}
261342
} else {
262343
if (this._captureSpanData) {
263-
beforeCh.unsubscribe(this._enter)
344+
if (!this._useAsyncContextFrame) {
345+
beforeCh.unsubscribe(this._enter)
346+
}
264347
enterCh.unsubscribe(this._enter)
265348
spanFinishCh.unsubscribe(this._spanFinished)
266349
this._profilerState = undefined
@@ -297,19 +380,20 @@ class NativeWallProfiler {
297380
}
298381

299382
// Native profiler doesn't set context.context for some samples, such as idle samples or when
300-
// the context was otherwise unavailable when the sample was taken.
301-
const ref = context.context?.ref
383+
// the context was otherwise unavailable when the sample was taken. Note that with async context
384+
// frame, we don't use the "ref" indirection.
385+
const ref = this._useAsyncContextFrame ? context.context : context.context?.ref
302386
if (typeof ref !== 'object') {
303387
return labels
304388
}
305389

306390
const { spanId, rootSpanId, webTags, endpoint } = ref
307391

308392
if (spanId !== undefined) {
309-
labels[SPAN_ID_LABEL] = spanId
393+
labels[SPAN_ID_LABEL] = typeof spanId === 'object' ? spanId.toString(10) : spanId
310394
}
311395
if (rootSpanId !== undefined) {
312-
labels[LOCAL_ROOT_SPAN_ID_LABEL] = rootSpanId
396+
labels[LOCAL_ROOT_SPAN_ID_LABEL] = typeof rootSpanId === 'object' ? rootSpanId.toString(10) : rootSpanId
313397
}
314398
if (webTags !== undefined && Object.keys(webTags).length !== 0) {
315399
labels['trace endpoint'] = endpointNameFromTags(webTags)

0 commit comments

Comments
 (0)