Skip to content

Use AsyncContextFrame for storing profiler sampling context #5743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/apm-integrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
- uses: ./.github/actions/testagent/start
- uses: ./.github/actions/node
with:
version: ${{ matrix.node-version }}

Check failure on line 169 in .github/workflows/apm-integrations.yml

View workflow job for this annotation

GitHub Actions / actionlint

property "node-opts" is not defined in object type {node-version: string}
- uses: ./.github/actions/install
- run: yarn test:plugins:ci
env:
Expand Down Expand Up @@ -230,8 +230,6 @@
- run: yarn test:plugins:ci
- uses: ./.github/actions/node/latest
- run: yarn test:plugins:ci
env:
OPTIONS_OVERRIDE: 1
- uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3

confluentinc-kafka-javascript:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/debugger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ jobs:
- uses: ./.github/actions/install
- run: yarn test:debugger:ci
- run: yarn test:integration:debugger
env:
OPTIONS_OVERRIDE: 1
- if: always()
uses: ./.github/actions/testagent/logs
with:
Expand Down
19 changes: 17 additions & 2 deletions integration-tests/profiler/codehotspots.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const DDTrace = require('dd-trace')
const tracer = DDTrace.init()
const NativeWallProfiler = require('dd-trace/packages/dd-trace/src/profiling/profilers/wall')

// Busy cycle duration is communicated in nanoseconds through the environment
// variable by the test. On first execution, it'll be 10 * the sampling period
Expand Down Expand Up @@ -29,12 +30,18 @@
let counter = 0

function runBusySpans () {
tracer.trace('x' + counter, { type: 'web', resource: `endpoint-${counter}` }, (_, done) => {
const id1 = `x-${counter}`
tracer.trace(id1, { type: 'web', resource: `endpoint-${counter}` }, (_, done) => {
logData(id1)
setImmediate(() => {
logData(`${id1} timeout`)
for (let i = 0; i < 3; ++i) {
const z = i
tracer.trace('y' + i, (_, done2) => {
const id2 = `y-${counter}-${i}`
tracer.trace(id2, (_, done2) => {
logData(id2)
const busyWork = () => {
logData(`${id2}-timeout`)
busyLoop()
done2()
if (z === 2) {
Expand All @@ -60,4 +67,12 @@
})
}

function logData (codeContext) {
const active = NativeWallProfiler.prototype.getActiveSpan()
const sampleContext = NativeWallProfiler.prototype.getSampleContext()
const indicator = (active.spanId === sampleContext.spanId) ? '✅' : '❌'
const CPEDContextCount = NativeWallProfiler.prototype.getCPEDContextCount()
console.log(indicator, codeContext, 'activeSpan:', active.spanId, ', sampleContext:', sampleContext.spanId, ', CPEDContextCount:', CPEDContextCount)

Check failure on line 75 in integration-tests/profiler/codehotspots.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

Check failure on line 75 in integration-tests/profiler/codehotspots.js

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 150. Maximum allowed is 120
}

tracer.profilerStarted().then(runBusySpans)
71 changes: 54 additions & 17 deletions integration-tests/profiler/profiler.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
const net = require('net')
const zlib = require('zlib')
const { Profile } = require('pprof-format')
const satisfies = require('semifies')

const DEFAULT_PROFILE_TYPES = ['wall', 'space']
if (process.platform !== 'win32') {
Expand Down Expand Up @@ -332,15 +333,24 @@
// with recomputed busyCycleTimeNs, but let's give ourselves more leeway.
this.retries(9)
const procStart = BigInt(Date.now() * 1000000)
const proc = fork(path.join(cwd, 'profiler/codehotspots.js'), {
cwd,
env: {
DD_PROFILING_EXPORTERS: 'file',
DD_PROFILING_ENABLED: 1,
BUSY_CYCLE_TIME: (busyCycleTimeNs | 0).toString(),
DD_TRACE_AGENT_PORT: agent.port
const env = {
DD_PROFILING_EXPORTERS: 'file',
DD_PROFILING_ENABLED: 1,
BUSY_CYCLE_TIME: (busyCycleTimeNs | 0).toString(),
DD_TRACE_AGENT_PORT: agent.port
}
// With Node 23 or later, test the profiler with async context frame use.
const execArgv = []
if (satisfies(process.versions.node, '>=23.0.0')) {
env.DD_PROFILING_USE_ASYNC_CONTEXT_FRAME = 1
if (!satisfies(process.versions.node, '>=24.0.0')) {
// For Node 23, use the experimental command line flag for Node to enable
// async context frame. Node 24 has it enabled by default.
execArgv.push('--experimental-async-context-frame')
}
})
}
console.log({path: path.join(cwd, 'profiler/codehotspots.js'), env, execArgv })

Check failure on line 352 in integration-tests/profiler/profiler.spec.js

View workflow job for this annotation

GitHub Actions / lint

A space is required after '{'

Check failure on line 352 in integration-tests/profiler/profiler.spec.js

View workflow job for this annotation

GitHub Actions / lint

A space is required after '{'

Check failure on line 352 in integration-tests/profiler/profiler.spec.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
const proc = fork(path.join(cwd, 'profiler/codehotspots.js'), { cwd, env, execArgv })

await processExitPromise(proc, timeout)
const procEnd = BigInt(Date.now() * 1000000)
Expand Down Expand Up @@ -641,7 +651,7 @@
})
})

context('Profiler API telemetry', () => {
context('Profiler telemetry', () => {
beforeEach(async () => {
agent = await new FakeAgent().start()
})
Expand Down Expand Up @@ -669,21 +679,20 @@
const pp = payload.payload
assert.equal(pp.namespace, 'profilers')
const series = pp.series
assert.lengthOf(series, 2)
assert.equal(series[0].metric, 'profile_api.requests')
assert.equal(series[0].type, 'count')
const requests = series.find(s => s.metric === 'profile_api.requests')
assert.equal(requests.type, 'count')
// There's a race between metrics and on-shutdown profile, so metric
// value will be between 1 and 3
requestCount = series[0].points[0][1]
requestCount = requests.points[0][1]
assert.isAtLeast(requestCount, 1)
assert.isAtMost(requestCount, 3)

assert.equal(series[1].metric, 'profile_api.responses')
assert.equal(series[1].type, 'count')
assert.include(series[1].tags, 'status_code:200')
const responses = series.find(s => s.metric === 'profile_api.responses')
assert.equal(responses.type, 'count')
assert.include(responses.tags, 'status_code:200')

// Same number of requests and responses
assert.equal(series[1].points[0][1], requestCount)
assert.equal(responses.points[0][1], requestCount)
}, 'generate-metrics', timeout)

const checkDistributions = agent.assertTelemetryReceived(({ _, payload }) => {
Expand All @@ -704,6 +713,34 @@
// Same number of requests and points
assert.equal(requestCount, pointsCount)
})

it('sends wall profiler sample context telemetry', async function () {
if (satisfies(process.versions.node, '<24.0.0')) {
this.skip() // Wall profiler context count telemetry is not supported in Node < 24
}
proc = fork(profilerTestFile, {
cwd,
env: {
DD_TRACE_AGENT_PORT: agent.port,
DD_PROFILING_ENABLED: 1,
DD_PROFILING_UPLOAD_PERIOD: 1,
DD_PROFILING_USE_ASYNC_CONTEXT_FRAME: 1,
DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, // every second
TEST_DURATION_MS: 1500
}
})

const checkMetrics = agent.assertTelemetryReceived(({ _, payload }) => {
const pp = payload.payload
assert.equal(pp.namespace, 'profilers')
const sampleContexts = pp.series.find(s => s.metric === 'wall.sample_contexts')
assert.isDefined(sampleContexts)
assert.equal(sampleContexts.type, 'gauge')
assert.isAtLeast(sampleContexts.points[0][1], 1)
}, 'generate-metrics', timeout)

await Promise.all([checkProfiles(agent, proc, timeout), checkMetrics])
})
})

function forkSsi (args) {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,8 @@
"tiktoken": "^1.0.21",
"yaml": "^2.8.0",
"yarn-deduplicate": "^6.0.2"
},
"volta": {
"node": "24.2.0"
}
}
5 changes: 4 additions & 1 deletion packages/dd-trace/src/profiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module.exports = {
start: config => {
const { service, version, env, url, hostname, port, tags, repositoryUrl, commitSHA, injectionEnabled } = config
const { enabled, sourceMap, exporters } = config.profiling
const { heartbeatInterval } = config.telemetry

const logger = {
debug: (message) => log.debug(message),
info: (message) => log.info(message),
Expand Down Expand Up @@ -39,7 +41,8 @@ module.exports = {
repositoryUrl,
commitSHA,
libraryInjected,
activation
activation,
heartbeatInterval
})
},

Expand Down
27 changes: 27 additions & 0 deletions packages/dd-trace/src/profiling/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const { tagger } = require('./tagger')
const { isFalse, isTrue } = require('../util')
const { getAzureTagsFromMetadata, getAzureAppMetadata } = require('../azure_metadata')
const { getEnvironmentVariables } = require('../config-helper')
const satisfies = require('semifies')

class Config {
constructor (options = {}) {
Expand All @@ -41,6 +42,7 @@ class Config {
DD_PROFILING_TIMELINE_ENABLED,
DD_PROFILING_UPLOAD_PERIOD,
DD_PROFILING_UPLOAD_TIMEOUT,
DD_PROFILING_USE_ASYNC_CONTEXT_FRAME,
DD_PROFILING_V8_PROFILER_BUG_WORKAROUND,
DD_PROFILING_WALLTIME_ENABLED,
DD_SERVICE,
Expand Down Expand Up @@ -207,6 +209,31 @@ class Config {

this.uploadCompression = { method: uploadCompression, level }

const that = this
function turnOffAsyncContextFrame (msg) {
that.logger.warn(
`DD_PROFILING_USE_ASYNC_CONTEXT_FRAME was set ${msg}, it will have no effect.`)
that.useAsyncContextFrame = false
}

this.useAsyncContextFrame = isTrue(coalesce(options.useAsyncContextFrame,
DD_PROFILING_USE_ASYNC_CONTEXT_FRAME, false))
if (this.useAsyncContextFrame) {
if (satisfies(process.versions.node, '>=24.0.0')) {
if (process.execArgv.includes('--no-async-context-frame')) {
turnOffAsyncContextFrame('with --no-async-context-frame')
}
} else if (satisfies(process.versions.node, '>=23.0.0')) {
if (!process.execArgv.includes('--experimental-async-context-frame')) {
turnOffAsyncContextFrame('without --experimental-async-context-frame')
}
} else {
turnOffAsyncContextFrame('but it requires at least Node 23')
}
}

this.heartbeatInterval = options.heartbeatInterval || 60 * 1000 // 1 minute

this.profilers = ensureProfilers(profilers, this)
}
}
Expand Down
Loading
Loading