Skip to content

Start profilers synchronously within tracer initialization #5906

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 3 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
143 changes: 84 additions & 59 deletions packages/dd-trace/src/profiling/profiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,10 @@ const dc = require('dc-polyfill')
const crashtracker = require('../crashtracking')

const { promisify } = require('util')
const zlib = require('zlib')

const profileSubmittedChannel = dc.channel('datadog:profiling:profile-submitted')
const spanFinishedChannel = dc.channel('dd-trace:span:finish')

function maybeSourceMap (sourceMap, SourceMapper, debug) {
if (!sourceMap) return
return SourceMapper.create([
process.cwd()
], debug)
}

function logError (logger, ...args) {
if (logger) {
logger.error(...args)
Expand Down Expand Up @@ -50,22 +42,19 @@ class Profiler extends EventEmitter {
this._timer = undefined
this._lastStart = undefined
this._timeoutInterval = undefined
this._sourceMapsLoaded = undefined
this.endpointCounts = new Map()
}

start (options) {
return this._start(options).catch((err) => {
logError(options.logger, 'Error starting profiler. For troubleshooting tips, see ' +
'<https://dtdg.co/nodejs-profiler-troubleshooting>', err)
return false
})
}

_logError (err) {
logError(this._logger, err)
}

async _start (options) {
sourceMapsLoaded () {
return this._sourceMapsLoaded
}

start (options) {
if (this._enabled) return true

const config = this._config = new Config(options)
Expand All @@ -77,48 +66,41 @@ class Profiler extends EventEmitter {
// Log errors if the source map finder fails, but don't prevent the rest
// of the profiler from running without source maps.
let mapper
try {
const { setLogger, SourceMapper } = require('@datadog/pprof')
setLogger(config.logger)

mapper = await maybeSourceMap(config.sourceMap, SourceMapper, config.debugSourceMaps)
if (config.sourceMap && config.debugSourceMaps) {
this._logger.debug(() => {
return mapper.infoMap.size === 0
? 'Found no source maps'
: `Found source maps for following files: [${[...mapper.infoMap.keys()].join(', ')}]`
})
}

const clevel = config.uploadCompression.level
switch (config.uploadCompression.method) {
case 'gzip':
this._compressionFn = promisify(zlib.gzip)
if (clevel !== undefined) {
this._compressionOptions = {
level: clevel
}
if (config.sourceMap) {
try {
const { setLogger, SourceMapper } = require('@datadog/pprof')
setLogger(config.logger)

const maybeMapperLoadingPromise = SourceMapper.create([process.cwd()], config.debugSourceMaps)
if (maybeMapperLoadingPromise instanceof Promise) {
mapper = {
hasMappingInfo: () => false,
mappingInfo: (l) => l
}
break
case 'zstd':
if (typeof zlib.zstdCompress === 'function') {
this._compressionFn = promisify(zlib.zstdCompress)
if (clevel !== undefined) {
this._compressionOptions = {
params: {
[zlib.constants.ZSTD_c_compressionLevel]: clevel
}
}
this._sourceMapsLoaded = maybeMapperLoadingPromise.then((sourceMap) => {
if (config.debugSourceMaps) {
this._logger.debug(() => {
return sourceMap.infoMap.size === 0
? 'Found no source maps'
: `Found source maps for following files: [${[...mapper.infoMap.keys()].join(', ')}]`
})
}
} else {
const zstdCompress = require('@datadog/libdatadog').load('datadog-js-zstd').zstd_compress
const level = clevel ?? 0 // 0 is zstd default compression level
this._compressionFn = (buffer) => Promise.resolve(Buffer.from(zstdCompress(buffer, level)))
}
break
mapper.hasMappingInfo = sourceMap.hasMappingInfo.bind(sourceMap)
mapper.mappingInfo = sourceMap.mappingInfo.bind(sourceMap)
}).catch((err) => {
this._logError(err)
})
} else {
// If the result of SourceMapper.create is not a promise, it is already loaded
mapper = maybeMapperLoadingPromise
}
} catch (err) {
this._logError(err)
}
} catch (err) {
this._logError(err)
}

if (this._sourceMapsLoaded === undefined) {
this._sourceMapsLoaded = Promise.resolve()
}

try {
Expand Down Expand Up @@ -217,6 +199,45 @@ class Profiler extends EventEmitter {
}
}

_getCompressionFn () {
if (this._compressionFn === undefined) {
try {
const method = this._config.uploadCompression.method
if (method === 'off') {
return
}
const zlib = require('zlib')
const clevel = this._config.uploadCompression.level
if (method === 'gzip') {
this._compressionFn = promisify(zlib.gzip)
if (clevel !== undefined) {
this._compressionOptions = {
level: clevel
}
}
} else if (method === 'zstd') {
if (typeof zlib.zstdCompress === 'function') {
this._compressionFn = promisify(zlib.zstdCompress)
if (clevel !== undefined) {
this._compressionOptions = {
params: {
[zlib.constants.ZSTD_c_compressionLevel]: clevel
}
}
}
} else {
const zstdCompress = require('@datadog/libdatadog').load('datadog-js-zstd').zstd_compress
const level = clevel ?? 0 // 0 is zstd default compression level
this._compressionFn = (buffer) => Promise.resolve(Buffer.from(zstdCompress(buffer, level)))
}
}
} catch (err) {
this._logError(err)
}
}
return this._compressionFn
}

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

Expand Down Expand Up @@ -252,9 +273,13 @@ class Profiler extends EventEmitter {
await Promise.all(profiles.map(async ({ profiler, profile }) => {
try {
const encoded = await profiler.encode(profile)
const compressed = encoded instanceof Buffer && this._compressionFn !== undefined
? await this._compressionFn(encoded, this._compressionOptions)
: encoded
let compressed = encoded
if (encoded instanceof Buffer) {
const compressionFn = this._getCompressionFn()
if (compressionFn !== undefined) {
compressed = await compressionFn(encoded, this._compressionOptions)
}
}
encodedProfiles[profiler.type] = compressed
this._logger.debug(() => {
const profileJson = JSON.stringify(profile, (key, value) => {
Expand Down
4 changes: 1 addition & 3 deletions packages/dd-trace/src/profiling/profilers/wall.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ class NativeWallProfiler {
// cpu profiling is enabled.
this._withContexts = this._captureSpanData || this._timelineEnabled || this._cpuProfilingEnabled
this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
this._mapper = undefined
this._pprof = undefined

// Bind these to this so they can be used as callbacks
Expand All @@ -111,7 +110,6 @@ class NativeWallProfiler {
start ({ mapper } = {}) {
if (this._started) return

this._mapper = mapper
this._pprof = require('@datadog/pprof')
kSampleCount = this._pprof.time.constants.kSampleCount

Expand All @@ -126,7 +124,7 @@ class NativeWallProfiler {
this._pprof.time.start({
intervalMicros: this._samplingIntervalMicros,
durationMillis: this._flushIntervalMillis,
sourceMapper: this._mapper,
sourceMapper: mapper,
withContexts: this._withContexts,
lineNumbers: false,
workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled,
Expand Down
3 changes: 2 additions & 1 deletion packages/dd-trace/src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ class Tracer extends NoopProxy {
ssiHeuristics.start()
let mockProfiler = null
if (config.profiling.enabled === 'true') {
this._profilerStarted = this._startProfiler(config)
this._startProfiler(config)
this._profilerStarted = Promise.resolve(true)
} else if (ssiHeuristics.emitsTelemetry) {
// Start a mock profiler that emits mock profile-submitted events for the telemetry.
// It will be stopped if the real profiler is started by the heuristics.
Expand Down
Loading
Loading