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
14 changes: 13 additions & 1 deletion bin/flame.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const { values: args, positionals } = parseArgs({
'sourcemap-dirs': {
type: 'string',
short: 's'
},
'node-modules-source-maps': {
type: 'string',
short: 'n'
}
},
allowPositionals: true
Expand All @@ -63,6 +67,7 @@ Options:
-m, --manual Manual profiling mode (require SIGUSR2 to start)
-d, --delay <value> Delay before starting profiler (ms, 'none', or 'until-started', default: 'until-started')
-s, --sourcemap-dirs <dirs> Directories to search for sourcemaps (colon/semicolon-separated)
-n, --node-modules-source-maps <mods> Node modules to load sourcemaps from (comma-separated, e.g., "next,@next/next-server")
--node-options <options> Node.js CLI options to pass to the profiled process
-h, --help Show this help message
-v, --version Show version number
Expand All @@ -74,6 +79,7 @@ Examples:
flame run --delay=none server.js # Start profiling immediately
flame run --delay=until-started server.js # Start profiling after next event loop tick (default)
flame run --sourcemap-dirs=dist:build server.js # Enable sourcemap support
flame run -n next,@next/next-server server.js # Load Next.js sourcemaps from node_modules
flame run --node-options="--require ts-node/register" server.ts # With Node.js options
flame run --node-options="--import ./loader.js --max-old-space-size=4096" server.js
flame generate profile.pb.gz
Expand Down Expand Up @@ -125,11 +131,17 @@ async function main () {
? args['sourcemap-dirs'].split(/[:;]/).filter(d => d.trim())
: undefined

// Parse node modules sourcemap options
const nodeModulesSourceMaps = args['node-modules-source-maps']
? args['node-modules-source-maps'].split(',').map(s => s.trim())
: undefined

const { pid, process: childProcess } = startProfiling(script, scriptArgs, {
autoStart,
nodeOptions,
delay,
sourcemapDirs
sourcemapDirs,
nodeModulesSourceMaps
})

console.log(`🔥 Started profiling process ${pid}`)
Expand Down
9 changes: 9 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const { spawn } = require('child_process')
* @param {string[]} options.nodeOptions - Node.js CLI options to pass (default: [])
* @param {string} options.delay - Delay before starting profiler ('none', 'until-started', or numeric string for ms, default: 'until-started')
* @param {string|string[]} options.sourcemapDirs - Directory or directories to search for sourcemaps (optional)
* @param {string|string[]} options.nodeModulesSourceMaps - Node modules to load sourcemaps from (optional)
* @returns {Promise<object>} Process information
*/
function startProfiling (script, args = [], options = {}) {
Expand All @@ -34,6 +35,14 @@ function startProfiling (script, args = [], options = {}) {
env.FLAME_SOURCEMAP_DIRS = dirs.join(path.delimiter)
}

// Add node_modules sourcemap configuration if provided
if (options.nodeModulesSourceMaps) {
const mods = Array.isArray(options.nodeModulesSourceMaps)
? options.nodeModulesSourceMaps
: [options.nodeModulesSourceMaps]
env.FLAME_NODE_MODULES_SOURCE_MAPS = mods.join(',')
}

// Construct the node command with options
const nodeArgs = [
...(options.nodeOptions || []),
Expand Down
4 changes: 1 addition & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

169 changes: 157 additions & 12 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,174 @@ const autoStart = process.env.FLAME_AUTO_START === 'true'

// Initialize sourcemap support if enabled
const sourcemapDirs = process.env.FLAME_SOURCEMAP_DIRS
const nodeModulesSourceMaps = process.env.FLAME_NODE_MODULES_SOURCE_MAPS
let sourceMapperPromise = null
let nodeModulesMapperPromise = null

if (sourcemapDirs) {
const dirs = sourcemapDirs.split(path.delimiter).filter(d => d.trim())
// Helper: resolve module path from node_modules
function resolveModulePath (appPath, moduleName) {
try {
const resolved = require.resolve(moduleName, { paths: [appPath] })
const nodeModulesIndex = resolved.lastIndexOf('node_modules')
if (nodeModulesIndex === -1) return null

if (dirs.length > 0) {
console.log(`🗺️ Initializing sourcemap support for directories: ${dirs.join(', ')}`)
const afterNodeModules = resolved.substring(nodeModulesIndex + 'node_modules'.length + 1)
const parts = afterNodeModules.split(path.sep)

if (moduleName.startsWith('@')) {
return path.join(resolved.substring(0, nodeModulesIndex), 'node_modules', parts[0], parts[1])
} else {
return path.join(resolved.substring(0, nodeModulesIndex), 'node_modules', parts[0])
}
} catch {
return null
}
}

// Helper: walk directory for .map files
async function * walkForMapFiles (dir) {
const fsPromises = require('fs').promises
async function * walkRecursive (currentDir) {
try {
const dirHandle = await fsPromises.opendir(currentDir)
for await (const entry of dirHandle) {
const entryPath = path.join(currentDir, entry.name)
if (entry.isDirectory() && entry.name !== '.git') {
yield * walkRecursive(entryPath)
} else if (entry.isFile() && /\.[cm]?js\.map$/.test(entry.name)) {
yield entryPath
}
}
} catch {
// Silently ignore permission errors
}
}
yield * walkRecursive(dir)
}

// Helper: process sourcemap file
async function processSourceMapFile (mapPath) {
try {
const fsPromises = require('fs').promises
const sourceMap = require('source-map')

const contents = await fsPromises.readFile(mapPath, 'utf8')
const consumer = await new sourceMap.SourceMapConsumer(contents)

const dir = path.dirname(mapPath)
const generatedPathCandidates = []

if (consumer.file) {
generatedPathCandidates.push(path.resolve(dir, consumer.file))
}
generatedPathCandidates.push(path.resolve(dir, path.basename(mapPath, '.map')))

for (const generatedPath of generatedPathCandidates) {
try {
await fsPromises.access(generatedPath)
return {
generatedPath,
info: { mapFileDir: dir, mapConsumer: consumer }
}
} catch {}
}
return null
} catch {
return null
}
}

const { SourceMapper } = require('@datadog/pprof/out/src/sourcemapper/sourcemapper')
// Load sourcemaps from node_modules packages
async function loadNodeModulesSourceMaps (moduleNames, debug = false) {
const entries = new Map()

sourceMapperPromise = SourceMapper.create(dirs)
.then(mapper => {
sourceMapper = mapper
console.log('🗺️ Sourcemap initialization complete')
return mapper
for (const moduleName of moduleNames) {
const modulePath = resolveModulePath(process.cwd(), moduleName)
if (!modulePath) {
if (debug) {
console.warn(`⚠️ Could not resolve module: ${moduleName}`)
}
continue
}

if (debug) {
console.log(`🗺️ Scanning ${moduleName} for sourcemaps...`)
}

let mapCount = 0
for await (const mapFile of walkForMapFiles(modulePath)) {
const entry = await processSourceMapFile(mapFile)
if (entry) {
entries.set(entry.generatedPath, entry.info)
mapCount++
}
}

if (debug) {
console.log(`🗺️ Loaded ${mapCount} sourcemaps from ${moduleName}`)
}
}

return entries
}

// Start loading node_modules sourcemaps if configured
if (nodeModulesSourceMaps) {
const mods = nodeModulesSourceMaps.split(',').filter(m => m.trim())

if (mods.length > 0) {
console.log(`🗺️ Loading sourcemaps from node_modules: ${mods.join(', ')}`)

nodeModulesMapperPromise = loadNodeModulesSourceMaps(mods, false)
.then(entries => {
console.log(`🗺️ Loaded ${entries.size} sourcemaps from node_modules`)
return entries
})
.catch(error => {
console.error('⚠️ Warning: Failed to initialize sourcemaps:', error.message)
return null
console.error('⚠️ Warning: Failed to load node_modules sourcemaps:', error.message)
return new Map()
})
}
}

// Parse sourcemap directories
const dirs = sourcemapDirs
? sourcemapDirs.split(path.delimiter).filter(d => d.trim())
: []

// Initialize sourcemaps if we have either dirs or node_modules sourcemaps
if (dirs.length > 0 || nodeModulesMapperPromise) {
const { SourceMapper } = require('@datadog/pprof/out/src/sourcemapper/sourcemapper')

if (dirs.length > 0) {
console.log(`🗺️ Initializing sourcemap support for directories: ${dirs.join(', ')}`)
}

sourceMapperPromise = (async () => {
try {
// Create SourceMapper from dirs if provided, otherwise create empty one
const mapper = dirs.length > 0
? await SourceMapper.create(dirs)
: new SourceMapper(false)

// Merge node_modules sourcemaps if available
if (nodeModulesMapperPromise) {
const nodeModulesEntries = await nodeModulesMapperPromise
for (const [generatedPath, info] of nodeModulesEntries) {
mapper.infoMap.set(generatedPath, info)
}
}

sourceMapper = mapper
console.log('🗺️ Sourcemap initialization complete')
return mapper
} catch (error) {
console.error('⚠️ Warning: Failed to initialize sourcemaps:', error.message)
return null
}
})()
}

function generateFlamegraph (pprofPath, outputPath) {
return new Promise((resolve, reject) => {
// Find the flame CLI
Expand Down
Loading
Loading