@@ -108,6 +108,8 @@ class NativeWallProfiler {
108108 #captureSpanData = false
109109 #codeHotspotsEnabled = false
110110 #cpuProfilingEnabled = false
111+ #customLabelsActive = false
112+ #customLabelKeys
111113 #endpointCollectionEnabled = false
112114 #flushIntervalMillis = 0
113115 #logger
@@ -245,7 +247,24 @@ class NativeWallProfiler {
245247 // context -- we simply can't tell which one it might've been across all
246248 // possible async context frames.
247249 if ( this . #asyncContextFrameEnabled) {
248- this . #pprof. time . setContext ( sampleContext )
250+ if ( this . #customLabelsActive) {
251+ // Custom labels may be active in this async context. The current CPED
252+ // context could be a 2-element array [profilingContext, customLabels].
253+ // Replace the profiling context while preserving the custom labels.
254+ // This flag is monotonic (once set, stays true) because async
255+ // continuations from runWithLabels can fire at any time after the
256+ // synchronous runWithLabels call has returned.
257+ const current = this . #pprof. time . getContext ( )
258+ if ( Array . isArray ( current ) ) {
259+ if ( current [ 0 ] !== sampleContext ) {
260+ this . #pprof. time . setContext ( [ sampleContext , current [ 1 ] ] )
261+ }
262+ } else if ( current !== sampleContext ) {
263+ this . #pprof. time . setContext ( sampleContext )
264+ }
265+ } else {
266+ this . #pprof. time . setContext ( sampleContext )
267+ }
249268 } else {
250269 const sampleCount = this . _profilerState [ kSampleCount ]
251270 if ( sampleCount !== this . _lastSampleCount ) {
@@ -344,6 +363,13 @@ class NativeWallProfiler {
344363 const lowCardinalityLabels = Object . keys ( getThreadLabels ( ) )
345364 lowCardinalityLabels . push ( TRACE_ENDPOINT_LABEL )
346365
366+ // Custom labels are expected to be low-cardinality (e.g. customer tier, region)
367+ if ( this . #customLabelKeys) {
368+ for ( const key of this . #customLabelKeys) {
369+ lowCardinalityLabels . push ( key )
370+ }
371+ }
372+
347373 const profile = this . #pprof. time . stop ( restart , this . #boundGenerateLabels, lowCardinalityLabels )
348374
349375 if ( restart ) {
@@ -383,7 +409,29 @@ class NativeWallProfiler {
383409 return getThreadLabels ( )
384410 }
385411
386- const labels = { ...getThreadLabels ( ) }
412+ // Native profiler doesn't set context.context for some samples, such as idle samples or when
413+ // the context was otherwise unavailable when the sample was taken. Note that with ACF, we don't
414+ // use the "ref" indirection.
415+ let ref
416+ let customLabels
417+ const cctx = context . context
418+ if ( this . #asyncContextFrameEnabled) {
419+ // When custom labels are active with ACF, context.context is a 2-element array:
420+ // [profilingContext, customLabels]. Otherwise it's a plain object.
421+ if ( Array . isArray ( cctx ) ) {
422+ [ ref , customLabels ] = cctx
423+ } else {
424+ ref = cctx
425+ }
426+ } else {
427+ ref = cctx ?. ref
428+ }
429+
430+ // Custom labels are spread first so that internal labels always take
431+ // precedence and overwrite them.
432+ const labels = customLabels === undefined
433+ ? { ...getThreadLabels ( ) }
434+ : { ...customLabels , ...getThreadLabels ( ) }
387435
388436 if ( this . #timelineEnabled) {
389437 // Incoming timestamps are in microseconds, we emit nanos.
@@ -395,10 +443,6 @@ class NativeWallProfiler {
395443 labels [ 'async id' ] = asyncId
396444 }
397445
398- // Native profiler doesn't set context.context for some samples, such as idle samples or when
399- // the context was otherwise unavailable when the sample was taken. Note that with async context
400- // frame, we don't use the "ref" indirection.
401- const ref = this . #asyncContextFrameEnabled ? context . context : context . context ?. ref
402446 if ( typeof ref !== 'object' ) {
403447 return labels
404448 }
@@ -421,6 +465,45 @@ class NativeWallProfiler {
421465 return labels
422466 }
423467
468+ /**
469+ * Sets the custom label keys used for pprof low-cardinality deduplication.
470+ * Called once by the top-level Profiler when keys are declared.
471+ *
472+ * @param {Iterable<string> } keys
473+ */
474+ setCustomLabelKeys ( keys ) {
475+ this . #customLabelKeys = keys
476+ }
477+
478+ /**
479+ * Runs a function with custom profiling labels attached to all wall profiler
480+ * samples taken during its execution. Labels are key-value pairs that appear
481+ * in the pprof output and can be used to filter flame graphs in the Datadog UI.
482+ *
483+ * Requires AsyncContextFrame (ACF) to be enabled. Supports nesting: inner
484+ * calls merge labels with outer calls, with inner values taking precedence.
485+ *
486+ * @param {Record<string, string | number> } labels - Custom labels to attach
487+ * @param {function(): T } fn - Function to execute with the labels
488+ * @returns {T } The return value of fn
489+ * @template T
490+ */
491+ runWithLabels ( labels , fn ) {
492+ if ( ! this . #asyncContextFrameEnabled || ! this . #withContexts) {
493+ return fn ( )
494+ }
495+
496+ // Read current context; merge custom labels if already in a runWithLabels scope
497+ const current = this . #pprof. time . getContext ( )
498+ const isCurrentArray = Array . isArray ( current )
499+ const customLabels = isCurrentArray ? { ...current [ 1 ] , ...labels } : labels
500+
501+ const profilingContext = ( isCurrentArray ? current [ 0 ] : current ) ?? { }
502+
503+ this . #customLabelsActive = true
504+ return this . #pprof. time . runWithContext ( [ profilingContext , customLabels ] , fn )
505+ }
506+
424507 profile ( restart ) {
425508 return this . #stop( restart )
426509 }
0 commit comments