Skip to content

Commit 5e0362c

Browse files
committed
Allow the CPU profiler to store sample context in the async context frame
1 parent 86c1d35 commit 5e0362c

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
@@ -43,6 +43,7 @@ class Config {
4343
DD_PROFILING_TIMELINE_ENABLED,
4444
DD_PROFILING_UPLOAD_PERIOD,
4545
DD_PROFILING_UPLOAD_TIMEOUT,
46+
DD_PROFILING_USE_ASYNC_CONTEXT_FRAME,
4647
DD_PROFILING_V8_PROFILER_BUG_WORKAROUND,
4748
DD_PROFILING_WALLTIME_ENABLED,
4849
DD_SERVICE,
@@ -227,6 +228,28 @@ class Config {
227228

228229
this.uploadCompression = { method: uploadCompression, level }
229230

231+
function turnOffAsyncContextFrame (that, msg) {
232+
logger.warn(
233+
`DD_PROFILING_USE_ASYNC_CONTEXT_FRAME was set ${msg}, it will have no effect.`)
234+
that.useAsyncContextFrame = false
235+
}
236+
237+
this.useAsyncContextFrame = isTrue(coalesce(options.useAsyncContextFrame,
238+
DD_PROFILING_USE_ASYNC_CONTEXT_FRAME, false))
239+
if (this.useAsyncContextFrame) {
240+
if (satisfies(process.versions.node, '>=24.0.0')) {
241+
if (process.execArgv.includes('--no-async-context-frame')) {
242+
turnOffAsyncContextFrame(this, 'with --no-async-context-frame')
243+
}
244+
} else if (satisfies(process.versions.node, '>=23.0.0')) {
245+
if (!process.execArgv.includes('--experimental-async-context-frame')) {
246+
turnOffAsyncContextFrame(this, 'without --experimental-async-context-frame')
247+
}
248+
} else {
249+
turnOffAsyncContextFrame(this, 'but it requires at least Node 23')
250+
}
251+
}
252+
230253
this.profilers = ensureProfilers(profilers, this)
231254
}
232255
}

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

Lines changed: 128 additions & 44 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,40 +30,94 @@ 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+
let interceptingAsyncContextFrameSet = false
58+
// When using the async context frame to store sample context (available with
59+
// Node 24), we do not need to use the async hooks anymore.
60+
if (!useAsyncContextFrame) {
61+
const { createHook } = require('async_hooks')
62+
beforeCh = dc.channel('dd-trace:storage:before')
63+
createHook({ before: () => beforeCh.publish() }).enable()
64+
} else if (process.execArgv.includes('--expose-internals')) {
65+
const AsyncContextFrame = require('internal/async_context_frame')
66+
shimmer.wrap(AsyncContextFrame, 'set', function (original) {
67+
return function (frame) {
68+
original.call(this, frame)
69+
enterCh.publish()
70+
}
71+
})
72+
interceptingAsyncContextFrameSet = true
73+
}
4174

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

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
78+
let inRun = false
79+
shimmer.wrap(AsyncLocalStorage.prototype, 'enterWith', function (original) {
80+
return function (...args) {
81+
const retVal = original.apply(this, args)
82+
if (!inRun) enterCh.publish()
83+
return retVal
84+
}
85+
})
86+
87+
shimmer.wrap(AsyncLocalStorage.prototype, 'run', function (original) {
88+
return function (store, callback, ...args) {
89+
const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
90+
inRun = false
91+
enterCh.publish()
92+
const retVal = cb.apply(this, args)
93+
inRun = true
94+
return retVal
95+
})
96+
inRun = true
97+
const retVal = original.call(this, store, wrappedCb, ...args)
5598
enterCh.publish()
56-
const retVal = cb.apply(this, args)
99+
inRun = false
100+
return retVal
101+
}
102+
})
103+
104+
shimmer.wrap(AsyncResource.prototype, 'runInAsyncScope', function (original) {
105+
return function (callback, thisArg, ...args) {
106+
const wrappedCb = shimmer.wrapFunction(callback, cb => function (...args) {
107+
inRun = false
108+
enterCh.publish()
109+
const retVal = cb.apply(this, args)
110+
inRun = true
111+
return retVal
112+
})
57113
inRun = true
114+
const retVal = original.call(this, wrappedCb, thisArg, ...args)
115+
enterCh.publish()
116+
inRun = false
58117
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-
})
118+
}
119+
})
120+
}
67121

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

136193
if (this._withContexts) {
137-
this._setNewContext()
194+
if (!this._useAsyncContextFrame) {
195+
this._setNewContext()
196+
}
138197

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

143-
ensureChannelsActivated()
202+
ensureChannelsActivated(this._useAsyncContextFrame)
144203

145-
beforeCh.subscribe(this._enter)
204+
if (!this._useAsyncContextFrame) {
205+
beforeCh.subscribe(this._enter)
206+
}
146207
enterCh.subscribe(this._enter)
147208
spanFinishCh.subscribe(this._spanFinished)
148209
}
@@ -154,17 +215,37 @@ class NativeWallProfiler {
154215
_enter () {
155216
if (!this._started) return
156217

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

163-
this._updateContext(context)
164-
}
244+
updateContext(context)
245+
}
165246

166-
const span = getActiveSpan()
167-
this._currentContext.ref = span ? this._getProfilingContext(span) : {}
247+
this._currentContext.ref = sampleContext
248+
}
168249
}
169250

170251
_getProfilingContext (span) {
@@ -245,7 +326,7 @@ class NativeWallProfiler {
245326
_stop (restart) {
246327
if (!this._started) return
247328

248-
if (this._captureSpanData) {
329+
if (this._captureSpanData && !this._useAsyncContextFrame) {
249330
// update last sample context if needed
250331
this._enter()
251332
this._lastSampleCount = 0
@@ -259,7 +340,9 @@ class NativeWallProfiler {
259340
}
260341
} else {
261342
if (this._captureSpanData) {
262-
beforeCh.unsubscribe(this._enter)
343+
if (!this._useAsyncContextFrame) {
344+
beforeCh.unsubscribe(this._enter)
345+
}
263346
enterCh.unsubscribe(this._enter)
264347
spanFinishCh.unsubscribe(this._spanFinished)
265348
this._profilerState = undefined
@@ -296,19 +379,20 @@ class NativeWallProfiler {
296379
}
297380

298381
// 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
382+
// the context was otherwise unavailable when the sample was taken. Note that with async context
383+
// frame, we don't use the "ref" indirection.
384+
const ref = this._useAsyncContextFrame ? context.context : context.context?.ref
301385
if (typeof ref !== 'object') {
302386
return labels
303387
}
304388

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

307391
if (spanId !== undefined) {
308-
labels[SPAN_ID_LABEL] = spanId
392+
labels[SPAN_ID_LABEL] = typeof spanId === 'object' ? spanId.toString(10) : spanId
309393
}
310394
if (rootSpanId !== undefined) {
311-
labels[LOCAL_ROOT_SPAN_ID_LABEL] = rootSpanId
395+
labels[LOCAL_ROOT_SPAN_ID_LABEL] = typeof rootSpanId === 'object' ? rootSpanId.toString(10) : rootSpanId
312396
}
313397
if (webTags !== undefined && Object.keys(webTags).length !== 0) {
314398
labels['trace endpoint'] = endpointNameFromTags(webTags)

0 commit comments

Comments
 (0)