@@ -16,7 +16,7 @@ const {
16
16
17
17
const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require ( '../webspan-utils' )
18
18
19
- const beforeCh = dc . channel ( 'dd-trace:storage:before' )
19
+ let beforeCh
20
20
const enterCh = dc . channel ( 'dd-trace:storage:enter' )
21
21
const spanFinishCh = dc . channel ( 'dd-trace:span:finish' )
22
22
const profilerTelemetryMetrics = telemetryMetrics . manager . namespace ( 'profilers' )
@@ -30,14 +30,39 @@ function getActiveSpan () {
30
30
return store && store . span
31
31
}
32
32
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
+
33
51
let channelsActivated = false
34
- function ensureChannelsActivated ( ) {
52
+ function ensureChannelsActivated ( useAsyncContextFrame ) {
35
53
if ( channelsActivated ) return
36
54
37
- const { AsyncLocalStorage, createHook } = require ( 'async_hooks' )
38
55
const shimmer = require ( '../../../../datadog-shimmer' )
39
56
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' )
41
66
42
67
let inRun = false
43
68
shimmer . wrap ( AsyncLocalStorage . prototype , 'enterWith' , function ( original ) {
@@ -48,22 +73,27 @@ function ensureChannelsActivated () {
48
73
}
49
74
} )
50
75
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
+ } )
57
89
inRun = true
90
+ const retVal = original . call ( this , store , wrappedCb , ...args )
91
+ enterCh . publish ( )
92
+ inRun = false
58
93
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
+ }
67
97
68
98
channelsActivated = true
69
99
}
@@ -77,6 +107,8 @@ class NativeWallProfiler {
77
107
this . _endpointCollectionEnabled = ! ! options . endpointCollectionEnabled
78
108
this . _timelineEnabled = ! ! options . timelineEnabled
79
109
this . _cpuProfilingEnabled = ! ! options . cpuProfilingEnabled
110
+ this . _useAsyncContextFrame = ! ! options . useAsyncContextFrame
111
+
80
112
// We need to capture span data into the sample context for either code hotspots
81
113
// or endpoint collection.
82
114
this . _captureSpanData = this . _codeHotspotsEnabled || this . _endpointCollectionEnabled
@@ -130,19 +162,24 @@ class NativeWallProfiler {
130
162
withContexts : this . _withContexts ,
131
163
lineNumbers : false ,
132
164
workaroundV8Bug : this . _v8ProfilerBugWorkaroundEnabled ,
133
- collectCpuTime : this . _cpuProfilingEnabled
165
+ collectCpuTime : this . _cpuProfilingEnabled ,
166
+ useCPED : this . _useAsyncContextFrame
134
167
} )
135
168
136
169
if ( this . _withContexts ) {
137
- this . _setNewContext ( )
170
+ if ( ! this . _useAsyncContextFrame ) {
171
+ this . _setNewContext ( )
172
+ }
138
173
139
174
if ( this . _captureSpanData ) {
140
175
this . _profilerState = this . _pprof . time . getState ( )
141
176
this . _lastSampleCount = 0
142
177
143
- ensureChannelsActivated ( )
178
+ ensureChannelsActivated ( this . _useAsyncContextFrame )
144
179
145
- beforeCh . subscribe ( this . _enter )
180
+ if ( ! this . _useAsyncContextFrame ) {
181
+ beforeCh . subscribe ( this . _enter )
182
+ }
146
183
enterCh . subscribe ( this . _enter )
147
184
spanFinishCh . subscribe ( this . _spanFinished )
148
185
}
@@ -154,17 +191,37 @@ class NativeWallProfiler {
154
191
_enter ( ) {
155
192
if ( ! this . _started ) return
156
193
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 ( )
162
219
163
- this . _updateContext ( context )
164
- }
220
+ updateContext ( context )
221
+ }
165
222
166
- const span = getActiveSpan ( )
167
- this . _currentContext . ref = span ? this . _getProfilingContext ( span ) : { }
223
+ this . _currentContext . ref = sampleContext
224
+ }
168
225
}
169
226
170
227
_getProfilingContext ( span ) {
@@ -245,7 +302,7 @@ class NativeWallProfiler {
245
302
_stop ( restart ) {
246
303
if ( ! this . _started ) return
247
304
248
- if ( this . _captureSpanData ) {
305
+ if ( this . _captureSpanData && ! this . _useAsyncContextFrame ) {
249
306
// update last sample context if needed
250
307
this . _enter ( )
251
308
this . _lastSampleCount = 0
@@ -259,7 +316,9 @@ class NativeWallProfiler {
259
316
}
260
317
} else {
261
318
if ( this . _captureSpanData ) {
262
- beforeCh . unsubscribe ( this . _enter )
319
+ if ( ! this . _useAsyncContextFrame ) {
320
+ beforeCh . unsubscribe ( this . _enter )
321
+ }
263
322
enterCh . unsubscribe ( this . _enter )
264
323
spanFinishCh . unsubscribe ( this . _spanFinished )
265
324
this . _profilerState = undefined
@@ -296,19 +355,20 @@ class NativeWallProfiler {
296
355
}
297
356
298
357
// 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
301
361
if ( typeof ref !== 'object' ) {
302
362
return labels
303
363
}
304
364
305
365
const { spanId, rootSpanId, webTags, endpoint } = ref
306
366
307
367
if ( spanId !== undefined ) {
308
- labels [ SPAN_ID_LABEL ] = spanId
368
+ labels [ SPAN_ID_LABEL ] = typeof spanId === 'object' ? spanId . toString ( 10 ) : spanId
309
369
}
310
370
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
312
372
}
313
373
if ( webTags !== undefined && Object . keys ( webTags ) . length !== 0 ) {
314
374
labels [ 'trace endpoint' ] = endpointNameFromTags ( webTags )
0 commit comments