Skip to content

Commit d608888

Browse files
committed
Extract shared web-tags cache used by wall profiler and OTEP-4947 writer
The wall profiler and the OTEP-4947 thread-context writer were both walking each span's started-spans chain to find the nearest web-server ancestor, each caching the answer under its own Symbol. Move the walk and cache into packages/dd-trace/src/web-tags-cache.js. - getCachedWebTags(span): lazy parent-chain walk, cached on a shared Symbol. - onSpanTagsUpdated(span): call from a tagsUpdate subscriber; if the walk previously came up empty and the span is now a web-server span, promote its tags into the cache. Returns true iff the cache transitioned from undefined to a real value — a signal to consumers that they should snapshot the new value into whatever they built while the answer was undefined. wall.js still snapshots webTags into its per-sample ProfilingContext because label generation reads it from the sample-context ref (not from the span); its tagsUpdate handler now refreshes that snapshot only when the shared cache signals a transition. otel-thread-ctx.js's cached-per-span record drops the local walk plus the webTags / webTagsResolved fields; it now just tracks whether the ThreadContext was built without an endpoint (needsEndpoint) and appends one when the shared cache reports a late transition. No functional change. CODEOWNERS gets the new file scoped to @DataDog/profiling-js (same as its two callers).
1 parent 463f561 commit d608888

5 files changed

Lines changed: 164 additions & 114 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@
320320
/packages/dd-trace/*/profiling/ @DataDog/profiling-js
321321
/packages/dd-trace/src/otel-thread-ctx.js @DataDog/profiling-js
322322
/packages/dd-trace/src/storage-channels.js @DataDog/profiling-js @DataDog/apm-sdk-capabilities-js
323+
/packages/dd-trace/src/web-tags-cache.js @DataDog/profiling-js
323324
/packages/dd-trace/test/exporters/common/form-data.spec.js @DataDog/profiling-js
324325
/packages/dd-trace/test/otel-thread-ctx.spec.js @DataDog/profiling-js
325326

packages/dd-trace/src/otel-thread-ctx.js

Lines changed: 28 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,14 @@ const log = require('./log')
3030
const {
3131
enterCh,
3232
spanFinishCh,
33-
tagsUpdateCh,
3433
getActiveSpan,
3534
ensureChannelsActivated,
3635
} = require('./storage-channels')
3736
const {
38-
isWebServerSpan,
3937
endpointNameFromTags,
4038
getStartedSpans,
4139
} = require('./profiling/webspan-utils')
40+
const webTagsCache = require('./web-tags-cache')
4241

4342
// Positional attribute layout. The local root span ID stays at index 0 by
4443
// convention (mirrors libdatadog's libdd-otel-thread-ctx, where
@@ -75,59 +74,23 @@ const THREAD_ID = String(threadId)
7574
// reference verbatim, and the context's record buffer is mutated in
7675
// place by appendAttributes, so all frames observe the same record.
7776
//
78-
// Fields populated lazily:
79-
// context: ThreadContext from @datadog/pprof.otelThreadCtx — built the first
80-
// time onEnter activates the span.
81-
// webTagsResolved + webTags: true once the parent-chain walk has
82-
// run; webTags is the resolved tag bag (or undefined when no web
83-
// ancestor was found).
77+
// Fields:
78+
// context: ThreadContext from @datadog/pprof.otelThreadCtx —
79+
// built the first time onEnter activates the span.
80+
// needsEndpoint: true when the ThreadContext was built without an
81+
// endpoint attribute (the shared web-tags cache came
82+
// up empty at build time). Cleared when a late
83+
// tagsUpdate lets us append the endpoint.
8484
const CachedSym = Symbol('OtelThreadCtx.cached')
8585

8686
let started = false
8787
let ThreadContext
8888
let getContext
8989
let clearContext
9090

91-
function getOrCreateCache (span) {
92-
let cached = span[CachedSym]
93-
if (cached === undefined) {
94-
cached = {}
95-
span[CachedSym] = cached
96-
}
97-
return cached
98-
}
99-
100-
// Walks up the started-spans stack to find the nearest ancestor whose
101-
// tags identify it as a web-server span. Mirrors the same walk in
102-
// profiling/profilers/wall.js (which keeps its own cache under a
103-
// different Symbol). If the two ever drift we should extract.
104-
function getCachedWebTags (span) {
105-
const cached = getOrCreateCache(span)
106-
if (cached.webTagsResolved) return cached.webTags
107-
const spanContext = span.context()
108-
const tags = spanContext.getTags()
109-
let webTags
110-
if (isWebServerSpan(tags)) {
111-
webTags = tags
112-
} else {
113-
const parentId = spanContext._parentId
114-
const startedSpans = getStartedSpans(spanContext)
115-
for (let i = startedSpans.length; --i >= 0;) {
116-
const ispan = startedSpans[i]
117-
if (ispan.context()._spanId === parentId) {
118-
webTags = getCachedWebTags(ispan)
119-
break
120-
}
121-
}
122-
}
123-
cached.webTags = webTags
124-
cached.webTagsResolved = true
125-
return webTags
126-
}
127-
12891
function getOrBuildContext (span) {
129-
const cached = getOrCreateCache(span)
130-
if (cached.context !== undefined) return cached.context
92+
let cached = span[CachedSym]
93+
if (cached !== undefined && cached.context !== undefined) return cached.context
13194
const spanContext = span.context()
13295
const traceId = Uint8Array.from(Buffer.from(spanContext.toTraceId(true), 'hex'))
13396
const spanId = Uint8Array.from(Buffer.from(spanContext.toSpanId(true), 'hex'))
@@ -136,13 +99,18 @@ function getOrBuildContext (span) {
13699
// per the libdatadog convention.
137100
const startedSpans = getStartedSpans(spanContext)
138101
const rootContext = startedSpans.length ? startedSpans[0].context() : spanContext
139-
const webTags = getCachedWebTags(span)
102+
const webTags = webTagsCache.getCachedWebTags(span)
140103
const attrs = []
141104
attrs[LOCAL_ROOT_SPAN_ID_IDX] = rootContext.toSpanId(true)
142105
if (webTags) attrs[ENDPOINT_IDX] = endpointNameFromTags(webTags)
143106
attrs[THREAD_NAME_IDX] = THREAD_NAME
144107
attrs[THREAD_ID_IDX] = THREAD_ID
108+
if (cached === undefined) {
109+
cached = {}
110+
span[CachedSym] = cached
111+
}
145112
cached.context = new ThreadContext(traceId, spanId, attrs)
113+
cached.needsEndpoint = webTags === undefined
146114
return cached.context
147115
}
148116

@@ -177,24 +145,18 @@ function onSpanFinished (span) {
177145

178146
function onTagsUpdated (span) {
179147
if (!started) return
148+
// Invoked (via webTagsCache.resolvedCh) once per span at the moment the
149+
// shared cache promotes a previously-undefined webTags answer into a
150+
// real value.
180151
const cached = span[CachedSym]
181-
// Skip unless the prior parent-chain walk already ran and came up
182-
// empty. If the walk hasn't happened yet (cached.webTagsResolved
183-
// false), onEnter will resolve it the natural way. If it ran and
184-
// found a web span, we already have the endpoint.
185-
if (cached === undefined || !cached.webTagsResolved || cached.webTags !== undefined) return
186-
const tags = span.context().getTags()
187-
if (!isWebServerSpan(tags)) return
188-
cached.webTags = tags
189-
if (cached.context !== undefined) {
190-
// The context was already built without an endpoint; append it in
191-
// place. The record buffer is shared across every async-context
192-
// frame holding this context, so the endpoint becomes visible
193-
// everywhere at once.
194-
const append = []
195-
append[ENDPOINT_IDX] = endpointNameFromTags(tags)
196-
cached.context.appendAttributes(append)
197-
}
152+
if (cached === undefined || !cached.needsEndpoint || cached.context === undefined) return
153+
// Append the endpoint in place. The record buffer is shared across every
154+
// async-context frame holding this context, so the endpoint becomes
155+
// visible everywhere at once.
156+
const append = []
157+
append[ENDPOINT_IDX] = endpointNameFromTags(webTagsCache.getCachedWebTags(span))
158+
cached.context.appendAttributes(append)
159+
cached.needsEndpoint = false
198160
}
199161

200162
function start () {
@@ -232,7 +194,7 @@ function start () {
232194
ensureChannelsActivated(isACFActive)
233195
enterCh.subscribe(onEnter)
234196
spanFinishCh.subscribe(onSpanFinished)
235-
tagsUpdateCh.subscribe(onTagsUpdated)
197+
webTagsCache.resolvedCh.subscribe(onTagsUpdated)
236198

237199
started = true
238200
log.info('OTEP-4947 thread context writer started')

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

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
const log = require('../../log')
44
const runtimeMetrics = require('../../runtime_metrics')
55
const telemetryMetrics = require('../../telemetry/metrics')
6-
const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils')
6+
const { endpointNameFromTags, getStartedSpans } = require('../webspan-utils')
77
const { SAMPLING_INTERVAL } = require('../constants')
88
const {
99
enterCh,
1010
beforeCh,
1111
spanFinishCh,
12-
tagsUpdateCh,
1312
getActiveSpan,
1413
ensureChannelsActivated,
1514
} = require('../../storage-channels')
15+
const webTagsCache = require('../../web-tags-cache')
1616

1717
const {
1818
END_TIMESTAMP_LABEL,
@@ -162,7 +162,10 @@ class NativeWallProfiler {
162162
enterCh.subscribe(this.#boundEnter)
163163
spanFinishCh.subscribe(this.#boundSpanFinished)
164164
if (this.#endpointCollectionEnabled) {
165-
tagsUpdateCh.subscribe(this.#boundSpanTagsUpdated)
165+
// Web-tags cache publishes once per span at the moment its
166+
// walk-result transitions from undefined to a real value —
167+
// exactly when we need to refresh the ProfilingContext snapshot.
168+
webTagsCache.resolvedCh.subscribe(this.#boundSpanTagsUpdated)
166169
}
167170
}
168171
}
@@ -245,32 +248,20 @@ class NativeWallProfiler {
245248
let profilingContext = span[ProfilingContext]
246249
if (profilingContext === undefined) {
247250
const context = span.context()
248-
const startedSpans = getStartedSpans(context)
249251

250252
let spanId
251253
let rootSpanId
252254
if (this.#codeHotspotsEnabled) {
255+
const startedSpans = getStartedSpans(context)
253256
spanId = context._spanId
254257
rootSpanId = startedSpans.length ? startedSpans[0].context()._spanId : context._spanId
255258
}
256259

257-
let webTags
258-
if (this.#endpointCollectionEnabled) {
259-
const tags = context.getTags()
260-
if (isWebServerSpan(tags)) {
261-
webTags = tags
262-
} else {
263-
// Get parent's context's web tags
264-
const parentId = context._parentId
265-
for (let i = startedSpans.length; --i >= 0;) {
266-
const ispan = startedSpans[i]
267-
if (ispan.context()._spanId === parentId) {
268-
webTags = this.#getProfilingContext(ispan).webTags
269-
break
270-
}
271-
}
272-
}
273-
}
260+
// webTags is snapshotted into the sample context at getProfilingContext
261+
// time; if the answer turns out to be undefined and the span later gets
262+
// web-server tags, #spanTagsUpdated refreshes this field via the shared
263+
// cache (see web-tags-cache.js).
264+
const webTags = this.#endpointCollectionEnabled ? webTagsCache.getCachedWebTags(span) : undefined
274265

275266
profilingContext = { spanId, rootSpanId, webTags }
276267
span[ProfilingContext] = profilingContext
@@ -291,14 +282,15 @@ class NativeWallProfiler {
291282
}
292283
}
293284

285+
// Invoked (via webTagsCache.resolvedCh) once per span at the moment the
286+
// shared cache promotes a previously-undefined webTags answer into a
287+
// real value. Refresh the ProfilingContext snapshot so future samples
288+
// pick it up.
294289
#spanTagsUpdated (span) {
295290
if (!this.#started) return
296291
const profilingContext = span[ProfilingContext]
297-
if (profilingContext === undefined || profilingContext.webTags !== undefined) return
298-
const tags = span.context().getTags()
299-
if (isWebServerSpan(tags)) {
300-
profilingContext.webTags = tags
301-
}
292+
if (profilingContext === undefined) return
293+
profilingContext.webTags = webTagsCache.getCachedWebTags(span)
302294
}
303295

304296
#reportV8bug (maybeBug) {
@@ -347,7 +339,7 @@ class NativeWallProfiler {
347339
enterCh.unsubscribe(this.#boundEnter)
348340
spanFinishCh.unsubscribe(this.#boundSpanFinished)
349341
if (this.#endpointCollectionEnabled) {
350-
tagsUpdateCh.unsubscribe(this.#boundSpanTagsUpdated)
342+
webTagsCache.resolvedCh.unsubscribe(this.#boundSpanTagsUpdated)
351343
}
352344
this.#profilerState = undefined
353345
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict'
2+
3+
// Per-span cache of "which tag bag from the started-spans chain identifies
4+
// this span (or its nearest web-server ancestor) as a web-server span?"
5+
// Populated lazily on first `getCachedWebTags(span)`, refreshed
6+
// automatically when a `dd-trace:span:tags:update` event promotes a
7+
// previously-empty answer for that span into a real value.
8+
//
9+
// Used by the wall profiler (endpoint-collection label on samples) and by
10+
// the OTEP-4947 thread-context writer (endpoint attribute in the record);
11+
// having a single cache means the parent-chain walk happens once per span
12+
// no matter how many consumers ask.
13+
//
14+
// Consumers that want to react to late web-server-span discovery
15+
// subscribe to `resolvedCh` — a diagnostics channel we publish on once
16+
// per span at the moment its cached webTags transitions from undefined
17+
// to a real value. Doing it via a channel (rather than exposing a
18+
// stateful "did the transition happen?" query) means each consumer sees
19+
// every transition exactly once, regardless of subscription order or
20+
// how many other consumers are attached.
21+
22+
const dc = require('dc-polyfill')
23+
const { isWebServerSpan, getStartedSpans } = require('./profiling/webspan-utils')
24+
25+
// Fields on the cache entry:
26+
// resolved: true once the parent-chain walk has run.
27+
// webTags: the resolved tag bag, or undefined when the walk came up
28+
// empty (no web-server span found in the started-spans chain).
29+
const CachedSym = Symbol('WebTagsCache')
30+
31+
const tagsUpdateCh = dc.channel('dd-trace:span:tags:update')
32+
const resolvedCh = dc.channel('dd-trace:web-tags:resolved')
33+
34+
function getCache (span) {
35+
let cached = span[CachedSym]
36+
if (cached === undefined) {
37+
cached = {}
38+
span[CachedSym] = cached
39+
}
40+
return cached
41+
}
42+
43+
// Returns the web-server tag bag for this span or its nearest web-server
44+
// ancestor in the started-spans chain, or undefined if none is a
45+
// web-server span. Lazy: walks the parent chain on the first call, caches
46+
// the result on the span.
47+
function getCachedWebTags (span) {
48+
const cached = getCache(span)
49+
if (cached.resolved) return cached.webTags
50+
const spanContext = span.context()
51+
const tags = spanContext.getTags()
52+
let webTags
53+
if (isWebServerSpan(tags)) {
54+
webTags = tags
55+
} else {
56+
const parentId = spanContext._parentId
57+
const startedSpans = getStartedSpans(spanContext)
58+
for (let i = startedSpans.length; --i >= 0;) {
59+
const ispan = startedSpans[i]
60+
if (ispan.context()._spanId === parentId) {
61+
webTags = getCachedWebTags(ispan)
62+
break
63+
}
64+
}
65+
}
66+
cached.webTags = webTags
67+
cached.resolved = true
68+
return webTags
69+
}
70+
71+
// Own the tagsUpdate → transition promotion here. Subscribed at module
72+
// load; inert (an O(1) Symbol check) for any span nobody has queried yet.
73+
tagsUpdateCh.subscribe((span) => {
74+
const cached = span[CachedSym]
75+
if (cached === undefined || !cached.resolved || cached.webTags !== undefined) return
76+
const tags = span.context().getTags()
77+
if (!isWebServerSpan(tags)) return
78+
cached.webTags = tags
79+
resolvedCh.publish(span)
80+
})
81+
82+
module.exports = { getCachedWebTags, resolvedCh }

0 commit comments

Comments
 (0)