Skip to content
Merged
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
12 changes: 7 additions & 5 deletions integration-tests/profiler/profiler.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -749,11 +749,13 @@ describe('profiler', () => {

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)
assert.equal(pp.namespace, 'profilers');
['live', 'used'].forEach(metricName => {
const sampleContexts = pp.series.find(s => s.metric === `wall.async_contexts_${metricName}`)
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])
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"@datadog/native-iast-taint-tracking": "4.0.0",
"@datadog/native-metrics": "3.1.1",
"@datadog/openfeature-node-server": "0.1.0-preview.10",
"@datadog/pprof": "5.10.0",
"@datadog/pprof": "5.11.1",
"@datadog/sketches-js": "2.1.1",
"@datadog/wasm-js-rewriter": "4.0.1",
"@isaacs/ttlcache": "^1.4.1",
Expand Down
22 changes: 21 additions & 1 deletion packages/dd-trace/src/profiling/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Config {
DD_AGENT_HOST,
DD_ENV,
DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing
DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED,
DD_PROFILING_CODEHOTSPOTS_ENABLED,
DD_PROFILING_CPU_ENABLED,
DD_PROFILING_DEBUG_SOURCE_MAPS,
Expand All @@ -42,7 +43,6 @@ class Config {
DD_PROFILING_TIMELINE_ENABLED,
DD_PROFILING_UPLOAD_PERIOD,
DD_PROFILING_UPLOAD_TIMEOUT,
DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED,
DD_PROFILING_V8_PROFILER_BUG_WORKAROUND,
DD_PROFILING_WALLTIME_ENABLED,
DD_SERVICE,
Expand Down Expand Up @@ -242,6 +242,26 @@ class Config {

this.profilers = ensureProfilers(profilers, this)
}

get systemInfoReport () {
const report = {
asyncContextFrameEnabled: this.asyncContextFrameEnabled,
codeHotspotsEnabled: this.codeHotspotsEnabled,
cpuProfilingEnabled: this.cpuProfilingEnabled,
debugSourceMaps: this.debugSourceMaps,
endpointCollectionEnabled: this.endpointCollectionEnabled,
heapSamplingInterval: this.heapSamplingInterval,
oomMonitoring: { ...this.oomMonitoring },
profilerTypes: this.profilers.map(p => p.type),
sourceMap: this.sourceMap,
timelineEnabled: this.timelineEnabled,
timelineSamplingEnabled: this.timelineSamplingEnabled,
uploadCompression: { ...this.uploadCompression },
v8ProfilerBugWorkaroundEnabled: this.v8ProfilerBugWorkaroundEnabled
}
delete report.oomMonitoring.exportCommand
return report
}
}

module.exports = { Config }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class EventSerializer {
return `${type}.pprof`
}

getEventJSON ({ profiles, start, end, tags = {}, endpointCounts }) {
getEventJSON ({ profiles, infos, start, end, tags = {}, endpointCounts }) {
return JSON.stringify({
attachments: Object.keys(profiles).map(t => this.typeToFile(t)),
start: start.toISOString(),
Expand Down Expand Up @@ -58,7 +58,8 @@ class EventSerializer {
ssi: {
mechanism: this._libraryInjected ? 'injected_agent' : 'none'
},
version
version,
...infos
},
runtime: {
available_processors: availableParallelism(),
Expand Down
66 changes: 44 additions & 22 deletions packages/dd-trace/src/profiling/profiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,16 @@ function findWebSpan (startedSpans, spanId) {
return false
}

function processInfo (infos, info, type) {
if (Object.keys(info).length > 0) {
infos[type] = info
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No functional grouping here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought not, the rest are typically metrics, so they'll end up as
wall.totalAsyncContextCount. Of course, we could embed this into another level, e.g. metrics so then it'd become wall.metrics.totalAsyncContextCount and we could then transpose that to metrics.wall.totalAsyncContextCount. FWIW, we don't even need to do the whole transposition, it just adds complexity; I originally wanted a top-level settings key because that's how e.g. Ruby does it, so I wanted to keep it a bit consistent with it:
Screenshot 2025-10-09 at 09 40 23

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree better be consistent with other langages

Copy link
Contributor Author

@szegedi szegedi Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually now reworked it so that settings are constructed by config.js and embedded in system info by top-level profiler inprofiler.js and very little is left in individual profiler facets, just computed settings and metrics, and for those I don't bother with transposition anymore. I think it's much better this way, but I needed you asking to nudge me into rethinking it :-)

}
}

class Profiler extends EventEmitter {
#compressionFn
#compressionOptions
#config
#enabled = false
#endpointCounts = new Map()
#lastStart
Expand All @@ -54,10 +61,13 @@ class Profiler extends EventEmitter {

constructor () {
super()
this._config = undefined
this._timeoutInterval = undefined
}

get flushInterval () {
return this.#config?.flushInterval
}

start (options) {
return this._start(options).catch((err) => {
logError(options.logger, 'Error starting profiler. For troubleshooting tips, see ' +
Expand All @@ -77,7 +87,7 @@ class Profiler extends EventEmitter {
async _start (options) {
if (this.enabled) return true

const config = this._config = new Config(options)
const config = this.#config = new Config(options)

this.#logger = config.logger
this.#enabled = true
Expand Down Expand Up @@ -158,16 +168,18 @@ class Profiler extends EventEmitter {
}
}

#nearOOMExport (profileType, encodedProfile) {
#nearOOMExport (profileType, encodedProfile, info) {
const start = this.#lastStart
const end = new Date()
const infos = this.#createInitialInfos()
processInfo(infos, info, profileType)
this.#submit({
[profileType]: encodedProfile
}, start, end, snapshotKinds.ON_OUT_OF_MEMORY)
}, infos, start, end, snapshotKinds.ON_OUT_OF_MEMORY)
}

_setInterval () {
this._timeoutInterval = this._config.flushInterval
this._timeoutInterval = this.#config.flushInterval
}

stop () {
Expand All @@ -189,7 +201,7 @@ class Profiler extends EventEmitter {
this.#spanFinishListener = undefined
}

for (const profiler of this._config.profilers) {
for (const profiler of this.#config.profilers) {
profiler.stop()
this.#logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
}
Expand Down Expand Up @@ -229,28 +241,34 @@ class Profiler extends EventEmitter {
}
}

#createInitialInfos () {
return {
settings: this.#config.systemInfoReport
}
}

async _collect (snapshotKind, restart = true) {
if (!this.enabled) return

const startDate = this.#lastStart
const endDate = new Date()
const profiles = []
const encodedProfiles = {}

try {
if (this._config.profilers.length === 0) {
if (this.#config.profilers.length === 0) {
throw new Error('No profile types configured.')
}

const startDate = this.#lastStart
const endDate = new Date()
const profiles = []

crashtracker.withProfilerSerializing(() => {
// collect profiles synchronously so that profilers can be safely stopped asynchronously
for (const profiler of this._config.profilers) {
for (const profiler of this.#config.profilers) {
const info = profiler.getInfo()
const profile = profiler.profile(restart, startDate, endDate)
if (!restart) {
this.#logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`)
}
if (!profile) continue
profiles.push({ profiler, profile })
profiles.push({ profiler, profile, info })
}
})

Expand All @@ -260,16 +278,20 @@ class Profiler extends EventEmitter {

let hasEncoded = false

const encodedProfiles = {}
const infos = this.#createInitialInfos()

// encode and export asynchronously
await Promise.all(profiles.map(async ({ profiler, profile }) => {
await Promise.all(profiles.map(async ({ profiler, profile, info }) => {
try {
const encoded = await profiler.encode(profile)
const compressed = encoded instanceof Buffer && this.#compressionFn !== undefined
? await this.#compressionFn(encoded, this.#compressionOptions)
: encoded
encodedProfiles[profiler.type] = compressed
processInfo(infos, info, profiler.type)
this.#logger.debug(() => {
const profileJson = JSON.stringify(profile, (key, value) => {
const profileJson = JSON.stringify(profile, (_, value) => {
return typeof value === 'bigint' ? value.toString() : value
})
return `Collected ${profiler.type} profile: ` + profileJson
Expand All @@ -283,7 +305,7 @@ class Profiler extends EventEmitter {
}))

if (hasEncoded) {
await this.#submit(encodedProfiles, startDate, endDate, snapshotKind)
await this.#submit(encodedProfiles, infos, startDate, endDate, snapshotKind)
profileSubmittedChannel.publish()
this.#logger.debug('Submitted profiles')
}
Expand All @@ -293,8 +315,8 @@ class Profiler extends EventEmitter {
}
}

#submit (profiles, start, end, snapshotKind) {
const { tags } = this._config
#submit (profiles, infos, start, end, snapshotKind) {
const { tags } = this.#config

// Flatten endpoint counts
const endpointCounts = {}
Expand All @@ -305,8 +327,8 @@ class Profiler extends EventEmitter {

tags.snapshot = snapshotKind
tags.profile_seq = this.#profileSeq++
const exportSpec = { profiles, start, end, tags, endpointCounts }
const tasks = this._config.exporters.map(exporter =>
const exportSpec = { profiles, infos, start, end, tags, endpointCounts }
const tasks = this.#config.exporters.map(exporter =>
exporter.export(exportSpec).catch(err => {
if (this.#logger) {
this.#logger.warn(err)
Expand Down Expand Up @@ -336,7 +358,7 @@ class ServerlessProfiler extends Profiler {

_setInterval () {
this._timeoutInterval = this.#interval * 1000
this.#flushAfterIntervals = this._config.flushInterval / 1000
this.#flushAfterIntervals = this.flushInterval / 1000
}

async _collect (snapshotKind, restart = true) {
Expand Down
15 changes: 12 additions & 3 deletions packages/dd-trace/src/profiling/profilers/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,17 +383,20 @@ function createPoissonProcessSamplingFilter (samplingIntervalMillis) {
* source with a sampling event filter and an event serializer.
*/
class EventsProfiler {
type = 'events'
#maxSamples
#maxSamples = 0
#timelineSamplingEnabled = false
#eventSerializer
#eventSources

get type () { return 'events' }

constructor (options = {}) {
this.#maxSamples = getMaxSamples(options)
this.#timelineSamplingEnabled = !!options.timelineSamplingEnabled
this.#eventSerializer = new EventSerializer(this.#maxSamples)

const eventHandler = event => this.#eventSerializer.addEvent(event)
const eventFilter = options.timelineSamplingEnabled
const eventFilter = this.#timelineSamplingEnabled
? createPoissonProcessSamplingFilter(options.samplingInterval)
: () => true
const filteringEventHandler = event => {
Expand Down Expand Up @@ -432,6 +435,12 @@ class EventsProfiler {
return () => thatEventSerializer.createProfile(startDate, endDate)
}

getInfo () {
return {
maxSamples: this.#maxSamples
}
}

encode (profile) {
return encodeProfileAsync(profile())
}
Expand Down
59 changes: 35 additions & 24 deletions packages/dd-trace/src/profiling/profilers/space.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,70 @@ function strategiesToCallbackMode (strategies, callbackMode) {
return strategies.includes(oomExportStrategies.ASYNC_CALLBACK) ? callbackMode.Async : 0
}

const STACK_DEPTH = 64

class NativeSpaceProfiler {
type = 'space'
_pprof
_started = false
#mapper
#oomMonitoring
#pprof
#samplingInterval = 512 * 1024
#started = false

constructor (options = {}) {
// TODO: Remove default value. It is only used in testing.
this._samplingInterval = options.heapSamplingInterval || 512 * 1024
this._stackDepth = options.stackDepth || 64
this._oomMonitoring = options.oomMonitoring || {}
this.#samplingInterval = options.heapSamplingInterval || 512 * 1024
this.#oomMonitoring = options.oomMonitoring || {}
}

get type () {
return 'space'
}

start ({ mapper, nearOOMCallback } = {}) {
if (this._started) return
if (this.#started) return

this._mapper = mapper
this._pprof = require('@datadog/pprof')
this._pprof.heap.start(this._samplingInterval, this._stackDepth)
if (this._oomMonitoring.enabled) {
const strategies = this._oomMonitoring.exportStrategies
this._pprof.heap.monitorOutOfMemory(
this._oomMonitoring.heapLimitExtensionSize,
this._oomMonitoring.maxHeapExtensionCount,
this.#mapper = mapper
this.#pprof = require('@datadog/pprof')
this.#pprof.heap.start(this.#samplingInterval, STACK_DEPTH)
if (this.#oomMonitoring.enabled) {
const strategies = this.#oomMonitoring.exportStrategies
this.#pprof.heap.monitorOutOfMemory(
this.#oomMonitoring.heapLimitExtensionSize,
this.#oomMonitoring.maxHeapExtensionCount,
strategies.includes(oomExportStrategies.LOGS),
strategies.includes(oomExportStrategies.PROCESS) ? this._oomMonitoring.exportCommand : [],
(profile) => nearOOMCallback(this.type, this._pprof.encodeSync(profile)),
strategiesToCallbackMode(strategies, this._pprof.heap.CallbackMode)
strategies.includes(oomExportStrategies.PROCESS) ? this.#oomMonitoring.exportCommand : [],
(profile) => nearOOMCallback(this.type, this.#pprof.encodeSync(profile), this.getInfo()),
strategiesToCallbackMode(strategies, this.#pprof.heap.CallbackMode)
)
}

this._started = true
this.#started = true
}

profile (restart) {
const profile = this._pprof.heap.profile(undefined, this._mapper, getThreadLabels)
const profile = this.#pprof.heap.profile(undefined, this.#mapper, getThreadLabels)
if (!restart) {
this.stop()
}
return profile
}

getInfo () {
return {}
}

encode (profile) {
return encodeProfileAsync(profile)
}

stop () {
if (!this._started) return
this._pprof.heap.stop()
this._started = false
if (!this.#started) return
this.#pprof.heap.stop()
this.#started = false
}

isStarted () {
return this._started
return this.#started
}
}

Expand Down
Loading