diff --git a/changelog.md b/changelog.md index 6d5cc4e..031c75a 100644 --- a/changelog.md +++ b/changelog.md @@ -4,23 +4,44 @@ * add `--kernel-tracing` option * removed `--svg` flag * removed `--gen` flag -* removed `--timestamp-profiles` -* removed `--theme` -* removed `--include` -* removed `--exclude | -x` -* removed `--tiers | -t` -* removed `--langs | -l` -* renamed `--trace-info` to `--kernel-tracing-debug` -* removed `--log-output` -* removed `--stacks-only` -* removed `-d | --delay` +* removed `--timestamp-profiles` flag +* removed `--theme` flag +* removed `--include` flag +* removed `--exclude | -x` flag +* removed `--tiers | -t` flag +* removed `--langs | -l` flag +* renamed `--trace-info` to `--kernel-tracing-debug` flag +* removed `--log-output` flag +* removed `--stacks-only` flag +* removed `-d | --delay` flag +* renamed `--json-stacks` to `--tree-debug` flag * profile folders renamed to `{outputDir}/{name}.0x` * ui: removed langs button * ui: removed theme button -* ui: style changes (adopted tachyons css) +* ui: style changes, minor redesign * altered mapFrames API (frames is now an array of objects, not strings) -* ui: rename/reoganize type labels +* ui: rename/reorganize type labels * ui: tier coloring improvements +* ui: search improvements +* categorization improvements +* API: removed `log` option +* API: added `kernelTracing` option +* API: removed `svg` option +* API: removed `gen` option +* API: removed `timestamp-profiles` option +* API: removed `theme` option +* API: removed `include` option +* API: removed `exclude` option +* API: removed `tiers` option +* API: removed `langs` option +* API: renamed `traceInfo` to `kernelTracingDebug` option +* API: removed `logOutput` option +* API: removed `stacksOnly` option +* API: removed `delay` option +* API: renamed `jsonStacks` to `treeDebug` option +* enhanced status console output (can be overridden in API) +* added merging capability (v8 prof only) +* added capturing inline data along side v8 prof ("pre-inlined" functions) # v3.4.1 diff --git a/cmd.js b/cmd.js index b46b01d..e8613af 100755 --- a/cmd.js +++ b/cmd.js @@ -1,13 +1,15 @@ #!/usr/bin/env node const fs = require('fs') -const minimist = require('minimist') const { join } = require('path') +const minimist = require('minimist') const semver = require('semver') -const zeroEks = require('./') -const { version } = require('./package.json') const debug = require('debug')('0x') const sll = require('single-line-log') +const launch = require('opn') +const hasUnicode = require('has-unicode')() +const zeroEks = semver.lt(process.version, '8.5.0') === true ? () => {} : require('./') +const { version } = require('./package.json') const defaultBanner = ` 0x ${version} @@ -18,20 +20,20 @@ const defaultBanner = ` if (module.parent === null) { cmd(process.argv.slice(2)).catch((err) => { - console.error('0x: ', err.message) + console.error(hasUnicode ? `🔥 ${err.message}` : err.message) debug(err) process.exit(err.code || 1) }) } else module.exports = cmd -function cmd (argv, banner = defaultBanner) { +async function cmd (argv, banner = defaultBanner) { var args = minimist(argv, { stopEarly: true, '--': true, number: ['phase'], boolean: [ 'open', 'version', 'help', 'quiet', - 'silent', 'jsonStacks', 'kernelTracingDebug', + 'silent', 'treeDebug', 'kernelTracingDebug', 'kernelTracing', 'collectOnly' ], alias: { @@ -44,12 +46,12 @@ function cmd (argv, banner = defaultBanner) { F: 'outputHtml', version: 'v', help: 'h', - jsonStacks: 'json-stacks', loggingOutput: 'logging-output', visualizeOnly: 'visualize-only', collectOnly: 'collect-only', kernelTracing: 'kernel-tracing', kernelTracingDebug: 'kernel-tracing-debug', + treeDebug: 'tree-debug' }, default: { phase: 2 @@ -57,10 +59,13 @@ function cmd (argv, banner = defaultBanner) { }) if (semver.lt(process.version, '8.5.0') === true) { - console.error('0x v4 supports Node 8.5.0 and above, current Node version is ' + process.version) - console.error('On Linux, macOS or Solaris the --kernel-tracing flag\nmay be able to generate a flamegraph with the current Node version') - console.error('See 0x --help for more info') - process.exit(1) + throw Error( + 'Node version unsupported. Current Node version is ' + process.version + '\n' + + 'Support extends from Node 8.5.0 and above\n\n' + + 'On Linux, macOS or Solaris kernel tracing mode may be used\n' + + 'to generate a flamegraph with the current Node version\n' + + 'See help for more info\n' + ) } if (args.help || argv.length === 0) { @@ -69,13 +74,23 @@ function cmd (argv, banner = defaultBanner) { } if (args.version) return console.log('0x ' + version) + const status = createStatus(args) args.workingDir = process.cwd() - args.log = createLogger(args) - args.status = createStatus(args) + args.status = status const { pathToNodeBinary, subprocessArgv } = parseSubprocessCommand(args) args.argv = subprocessArgv - return zeroEks(args, pathToNodeBinary) + if (args.visualizeOnly) status(`Creating flamegraph from ${args.visualizeOnly}`) + + const assetPath = await zeroEks(args, pathToNodeBinary) + + if (args.collectOnly) status(`Stats collected in folder file://${assetPath}\n`) + else { + status('Flamegraph generated in\n' + assetPath + '\n') + if (args.open) launch(assetPath, {wait: false}) + } + + return assetPath } function parseSubprocessCommand (args) { @@ -95,18 +110,9 @@ function parseSubprocessCommand (args) { return { pathToNodeBinary, subprocessArgv } } -function createLogger ({silent, quiet}) { - const logStream = process.stderr - return function log (msg, force) { - if (silent) return - if (!force && quiet) return - logStream.write(msg) - } -} - function createStatus ({silent, quiet}) { const statusStream = process.stderr - return quiet || silent - ? () => {} - : sll(statusStream) + if (quiet || silent) return () => {} + const status = sll(statusStream) + return hasUnicode ? (s) => status(`🔥 ${s}`) : status } \ No newline at end of file diff --git a/index.js b/index.js index 8630bbc..c6141fe 100644 --- a/index.js +++ b/index.js @@ -3,175 +3,131 @@ const { sun, linux, windows, v8 } = require('./platform') const { execSync } = require('child_process') const debug = require('debug')('0x') -const launch = require('opn') const { join, isAbsolute, relative, dirname } = require('path') const fs = require('fs') const pump = require('pump') -const ajv = require('ajv')() +const validate = require('./lib/validate')(require('./schema.json')) const traceStacksToTicks = require('./lib/trace-stacks-to-ticks') const v8LogToTicks = require('./lib/v8-log-to-ticks') const ticksToTree = require('./lib/ticks-to-tree') const phases = require('./lib/phases') const render = require('./lib/render') -const schema = require('./schema.json') const platform = process.platform -const { - isSudo, - tidy, - noop -} = require('./lib/util') +const { tidy, noop, isSudo } = require('./lib/util') -async function startProcessAndCollectTraceData (args, binary) { - if (!Array.isArray(args.argv)) { - throw Error('0x: argv option is required') - } - args.name = args.name || 'flamegraph' - - switch (args.kernelTracing ? platform : 'v8') { - case 'v8': return v8(args, binary) - case 'linux': return linux(args, await isSudo(), binary) - case 'win32': return windows() - default: return sun(args, await isSudo(), binary) - } -} +module.exports = zeroEks async function zeroEks (args, binary) { args.name = args.name || 'flamegraph' args.log = args.log || noop args.status = args.status || noop validate(args) - if (args.collectOnly && args.visualizeOnly) { - throw Error('--collect-only and --visualize-only cannot be used together') + const { collectOnly, visualizeOnly, treeDebug, mapFrames, open } = args + if (collectOnly && visualizeOnly) { + throw Error('"collect only" and "visualize only" cannot be used together') } - if (args.visualizeOnly) return visualize(args) + if (visualizeOnly) return visualize(args) args.title = args.title || 'node ' + args.argv.join(' ') - - const { collectOnly, jsonStacks } = args - args.mapFrames = args.mapFrames || phases[args.phase] - var { ticks, pid, folder, inlined } = await startProcessAndCollectTraceData(args, binary) - args.inlined = inlined - if (jsonStacks === true) { - const tree = await ticksToTree(ticks, args.mapFrames, inlined) + if (treeDebug === true) { + const tree = await ticksToTree(ticks, args,mapFrames, inlined) fs.writeFileSync(`${folder}/stacks.${pid}.json`, JSON.stringify(tree, 0, 2)) } - fs.writeFileSync(`${folder}/meta.json`, JSON.stringify(args, 0, 2)) + fs.writeFileSync(`${folder}/meta.json`, JSON.stringify({...args, inlined})) + if (collectOnly === true) { - const log = args.log - debug('--collect-only flag, bailing on rendering') - tidy(args) - log(`\n\n stats collected in file://${folder}\n`, true) - log('\n') + debug('collect-only mode bailing on rendering') + tidy() debug('done') - return + return folder } - await generateFlamegraph(args, {ticks, inlined, pid, folder}) + const file = await generateFlamegraph({...args, ticks, inlined, pid, folder}) + return file } -module.exports = zeroEks - -function validate (args) { - const privateProps = { - workingDir: {type: 'string'} - } - const valid = ajv.compile({ - ...schema, - properties: {...schema.properties, ...privateProps} - } - ) - if (valid(args)) return - const [{keyword, dataPath, params, message}] = valid.errors - if (keyword === 'type') { - - const flag = dataPath.substr( - 1, - dataPath[dataPath.length -1] === ']' ? - dataPath.length - 2 : - dataPath.length - 1 - ) - const dashPrefix = flag.length === 1 ? '-' : '--' - throw Error(`The ${dashPrefix}${flag} option ${message}\n`) +async function startProcessAndCollectTraceData (args, binary) { + if (!Array.isArray(args.argv)) { + throw Error('argv option is required') } - if (keyword === 'additionalProperties') { - const flag = params.additionalProperty - const dashPrefix = flag.length === 1 ? '-' : '--' - throw Error(`${dashPrefix}${flag} is not a recognized flag\n`) + args.name = args.name || 'flamegraph' + + switch (args.kernelTracing ? platform : 'v8') { + case 'v8': return v8(args, binary) + case 'linux': return linux(args, await isSudo(), binary) + case 'win32': return windows() + default: return sun(args, await isSudo(), binary) } } -async function generateFlamegraph (args, opts) { +async function generateFlamegraph (opts) { try { - const file = await render(args, opts) - const log = args.log - log('flamegraph generated in\n') - log(file + '\n', true) - tidy(args) - if (args.open) launch(file, {wait: false}) + const file = await render(opts) + tidy() + return file } catch (err) { - tidy(args) + tidy() throw err } } -async function visualize (args) { +async function visualize ({ visualizeOnly, treeDebug, workingDir, title, mapFrames, phase, open, name }) { try { - const { visualizeOnly, jsonStacks } = args const folder = isAbsolute(visualizeOnly) ? - relative(args.workingDir, visualizeOnly) : + relative(workingDir, visualizeOnly) : visualizeOnly const ls = fs.readdirSync(folder) const traceFile = /^stacks\.(.*)\.out$/ const isolateLog = /^isolate-(0x[0-9A-Fa-f]{2,12})-(.*)-v8.log$/ const stacks = ls.find((f) => isolateLog.test(f) || traceFile.test(f)) if (!stacks) { - throw Error('Invalid data path provided to --visualize-only (no stacks or v8 log file found)') + throw Error('Invalid data path provided (no stacks or v8 log file found)') } - const srcType = isolateLog.test(stacks) ? 'v8' : 'kernel-tracing' - const rx = (srcType === 'v8') ? isolateLog : traceFile - const pid = rx.exec(stacks)[srcType === 'v8' ? 2 : 1] - args.src = join(folder, stacks) - - if (!args.title) { - try { - const { title } = JSON.parse(fs.readFileSync(join(folder, 'meta.json'))) - args.title = title - } catch (e) { - debug(e) - } - } - - args.mapFrames = args.mapFrames || phases[args.phase] - const ticks = (srcType === 'v8') ? - await v8LogToTicks(args.src) : - traceStacksToTicks(args.src) - - var inlined + var meta try { - inlined = JSON.parse(fs.readFileSync(join(folder, 'meta.json'))).inlined + meta = JSON.parse(fs.readFileSync(join(folder, 'meta.json'))) } catch (e) { + meta = {} debug(e) } - if (jsonStacks === true) { - const tree = await ticksToTree(ticks, args.mapFrames, inlined) + const srcType = isolateLog.test(stacks) ? 'v8' : 'kernel-tracing' + const rx = (srcType === 'v8') ? isolateLog : traceFile + const pid = rx.exec(stacks)[srcType === 'v8' ? 2 : 1] + const { inlined } = meta + const src = join(folder, stacks) + title = title || meta.title + name = name || meta.name + + mapFrames = mapFrames || phases['phase' in meta ? meta.phase : phase] + const ticks = (srcType === 'v8') ? + await v8LogToTicks(src) : + traceStacksToTicks(src) + + if (treeDebug === true) { + const tree = await ticksToTree(ticks, mapFrames, inlined) fs.writeFileSync(`${folder}/stacks.${pid}.json`, JSON.stringify(tree, 0, 2)) } - await generateFlamegraph(args, {ticks, inlined, pid, folder}) + const file = await generateFlamegraph({ + visualizeOnly, treeDebug, workingDir, title, name, + mapFrames, phase, open, ticks, inlined, pid, folder + }) + + return file } catch (e) { if (e.code === 'ENOENT') { - throw Error('Invalid data path provided to --visualize-only (unable to access/does not exist)') + throw Error('Invalid data path provided (unable to access/does not exist)') } else if (e.code === 'ENOTDIR') { - throw Error('Invalid data path provided to --visualize-only (not a directory)') + throw Error('Invalid data path provided (not a directory)') } else throw e } } \ No newline at end of file diff --git a/lib/prof-log-convert.js b/lib/prof-log-convert.js index c67b44f..5ac53cd 100644 --- a/lib/prof-log-convert.js +++ b/lib/prof-log-convert.js @@ -11,7 +11,7 @@ function profLogConvert ({isolateLogPath, pid, folder, stream}, args) { const { stdout, stderr, status } = spawnSync('node', ['--prof-process', '--preprocess', '-j', isolateLogPath]) if (status !== 0) { - args.log('prof isolateLogPath convert Failed: ', stderr + '', stdout + '') + args.status('prof isolateLogPath convert Failed: ', stderr + '', stdout + '') return } const json = isolateLogPath + '.json' diff --git a/lib/render.js b/lib/render.js index 0bb2dcf..b44a81a 100644 --- a/lib/render.js +++ b/lib/render.js @@ -12,43 +12,45 @@ const html = require('../visualizer/html') module.exports = render -async function render (args, {ticks, inlined, pid, folder}) { - const { name, title, kernelTracing } = args +async function render (opts) { + const { + name, title, kernelTracing, outputHtml, pid, + workingDir, mapFrames, ticks, inlined, folder + } = opts debug('converted stacks to intermediate format') - const trees = ticksToTree(ticks, args.mapFrames, inlined) + const trees = ticksToTree(ticks, mapFrames, inlined) const script = ` ${await createBundle()} visualizer(${JSON.stringify(trees)}, ${JSON.stringify({title, kernelTracing})}) ` + const htmlPath = determineHtmlPath({name, outputHtml, workingDir, pid, outputDir: folder}) - const htmlPath = determineHtmlPath(args, {pid, folder}) - const opts = { + await html({ title, name, script, htmlPath, dir: folder, stdout: name === '-' || htmlPath === '-' - } + }) - await html(opts) debug('done rendering') return htmlPath === '-' ? null : `file://${htmlPath}` } -function determineHtmlPath (args, {pid, folder}) { - if (args.name === '-') return '-' - var htmlPath = (args.outputHtml || ( +function determineHtmlPath ({name, outputHtml, workingDir, pid, outputDir}) { + if (name === '-') return '-' + var htmlPath = (outputHtml || ( `{outputDir}${path.sep}{name}.html` )).replace('{pid}', pid || 'UNKNOWN_PID') - .replace('{timestamp}', Date.now()) - .replace('{outputDir}', folder) - .replace('{cwd}', args.workingDir) - .replace('{name}', args.name) + .replace('{timestamp}', Date.now()) + .replace('{outputDir}', outputDir) + .replace('{cwd}', workingDir) + .replace('{name}', name) if (path.isAbsolute(htmlPath) === false) { - htmlPath = path.join(args.workingDir, htmlPath) + htmlPath = path.join(workingDir, htmlPath) } return htmlPath } diff --git a/lib/util.js b/lib/util.js index 29c0b1b..38412a0 100644 --- a/lib/util.js +++ b/lib/util.js @@ -9,37 +9,32 @@ const debug = require('debug')('0x') const profLogConvert = require('./prof-log-convert') module.exports = { - determineOutputDir: determineOutputDir, - ensureDirExists: ensureDirExists, - tidy: tidy, - pathTo: pathTo, - isSudo: isSudo, - noop: noop + getTargetFolder, tidy, pathTo, isSudo, noop } -function determineOutputDir (args, proc) { - var name = (args.outputDir || '{pid}.0x').replace('{pid}', proc.pid || 'UNKNOWN_PID') - .replace('{timestamp}', Date.now()) - .replace('{cwd}', args.workingDir) - .replace('{name}', args.name) - - return path.resolve(args.workingDir, name) -} +function getTargetFolder ({outputDir, workingDir, name, pid}) { + name = (outputDir || '{pid}.0x').replace('{pid}', pid || 'UNKNOWN_PID') + .replace('{timestamp}', Date.now()) + .replace('{cwd}', workingDir) + .replace('{name}', name) + const folder = path.resolve(workingDir, name) -function ensureDirExists (path) { try { - fs.accessSync(path) + fs.accessSync(folder) } catch (e) { if (e.code === 'ENOENT') { - fs.mkdirSync(path) + fs.mkdirSync(folder) } else { + // function is always used within async/await or promise - fine to throw throw e } } + + return folder } -function tidy (args) { +function tidy () { debug('tidying up') fs.readdirSync('.') diff --git a/lib/validate.js b/lib/validate.js new file mode 100644 index 0000000..8fb1714 --- /dev/null +++ b/lib/validate.js @@ -0,0 +1,37 @@ +'use strict' + +const ajv = require('ajv')() + +module.exports = (schema) => { + + return validate + + function validate (args) { + const privateProps = { + workingDir: {type: 'string'} + } + const valid = ajv.compile({ + ...schema, + properties: {...schema.properties, ...privateProps} + } + ) + if (valid(args)) return + const [{keyword, dataPath, params, message}] = valid.errors + if (keyword === 'type') { + + const flag = dataPath.substr( + 1, + dataPath[dataPath.length -1] === ']' ? + dataPath.length - 2 : + dataPath.length - 1 + ) + const dashPrefix = flag.length === 1 ? '-' : '--' + throw Error(`The ${dashPrefix}${flag} option ${message}\n`) + } + if (keyword === 'additionalProperties') { + const flag = params.additionalProperty + const dashPrefix = flag.length === 1 ? '-' : '--' + throw Error(`${dashPrefix}${flag} is not a recognized flag\n`) + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 623d98d..c29fc48 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "debounce": "^1.1.0", "debug": "^2.2.0", "end-of-stream": "^1.1.0", + "has-unicode": "^2.0.1", "hsl-to-rgb-for-reals": "^1.1.0", "jsonstream2": "^1.1.1", "minimist": "^1.2.0", diff --git a/platform/linux.js b/platform/linux.js index 12356a3..fcb3f6c 100644 --- a/platform/linux.js +++ b/platform/linux.js @@ -10,8 +10,7 @@ const traceStacksToTicks = require('../lib/trace-stacks-to-ticks') const { promisify } = require('util') const { - determineOutputDir, - ensureDirExists, + getTargetFolder, tidy, pathTo } = require('../lib/util') @@ -19,11 +18,11 @@ const { module.exports = promisify(linux) function linux (args, sudo, binary, cb) { - const { status } = args + const { status, outputDir, workingDir, name } = args var perf = pathTo('perf') - if (!perf) return void cb(Error('0x: Unable to locate dtrace - make sure it\'s in your PATH')) + if (!perf) return void cb(Error('Unable to locate dtrace - make sure it\'s in your PATH')) if (!sudo) { - status('0x captures stacks using perf, which requires sudo access\n') + status('Stacks are captured using perf(1), which requires sudo access\n') return spawn('sudo', ['true']) .on('exit', function () { linux(args, true, binary, cb) }) } @@ -61,8 +60,7 @@ function linux (args, sudo, binary, cb) { analyze(true) }) - var folder = determineOutputDir(args, proc) - ensureDirExists(folder) + var folder = getTargetFolder({outputDir, workingDir, name, pid: proc.pid}) status('Profiling') @@ -79,11 +77,11 @@ function linux (args, sudo, binary, cb) { if (!manual) { debug('Caught SIGINT, generating flamegraph') - status('Caught SIGINT, generating flamegraph\n') + status('Caught SIGINT, generating flamegraph') proc.on('exit', generate) } else { debug('Process exited, generating flamegraph') - status('Process exited, generating flamegraph\n') + status('Process exited, generating flamegraph') generate() } diff --git a/platform/sun.js b/platform/sun.js index a626b0d..489aafa 100644 --- a/platform/sun.js +++ b/platform/sun.js @@ -9,8 +9,7 @@ const debug = require('debug')('0x') const traceStacksToTicks = require('../lib/trace-stacks-to-ticks') const { promisify } = require('util') const { - determineOutputDir, - ensureDirExists, + getTargetFolder, tidy, pathTo } = require('../lib/util') @@ -18,12 +17,13 @@ const { module.exports = promisify(sun) function sun (args, sudo, binary, cb) { - const { status } = args + const { status, outputDir, workingDir, name } = args + var dtrace = pathTo('dtrace') var profile = require.resolve('perf-sym/profile_1ms.d') - if (!dtrace) return void cb(Error('0x: Unable to locate dtrace - make sure it\'s in your PATH')) + if (!dtrace) return void cb(Error('Unable to locate dtrace - make sure it\'s in your PATH')) if (!sudo) { - status('0x captures stacks using dtrace, which requires sudo access\n') + status('Stacks are captured using DTrace, which requires sudo access\n') return spawn('sudo', ['true']) .on('exit', function () { sun(args, true, binary, cb) }) } @@ -40,7 +40,7 @@ function sun (args, sudo, binary, cb) { }).on('exit', function (code) { if (code !== 0) { tidy(args) - const err = Error('0x Target subprocess error, code: ' + code) + const err = Error('Target subprocess error, code: ' + code) err.code = code cb(err) return @@ -56,8 +56,7 @@ function sun (args, sudo, binary, cb) { if (kernelTracingDebug) { prof.stderr.pipe(process.stderr) } - folder = determineOutputDir(args, proc) - ensureDirExists(folder) + folder = getTargetFolder({outputDir, workingDir, name, pid: proc.pid}) prof.on('exit', function (code) { profExited = true @@ -73,6 +72,8 @@ function sun (args, sudo, binary, cb) { } debug('dtrace out closed') }) + + setTimeout(status, 100, 'Profiling') @@ -94,13 +95,13 @@ function sun (args, sudo, binary, cb) { debug('Profiling not begun') status('No stacks, profiling had not begun\n') tidy(args) - cb(Error('0x: Profiling not begun')) + cb(Error('Profiling not begun')) return } if (!manual) { debug('Caught SIGINT, generating flamegraph') - status('Caught SIGINT, generating flamegraph\n ') + status('Caught SIGINT, generating flamegraph') } try { process.kill(proc.pid, 'SIGINT') } catch (e) {} diff --git a/platform/v8.js b/platform/v8.js index e40b308..8df024e 100644 --- a/platform/v8.js +++ b/platform/v8.js @@ -11,8 +11,7 @@ const debug = require('debug')('0x') const v8LogToTicks = require('../lib/v8-log-to-ticks') const { - determineOutputDir, - ensureDirExists, + getTargetFolder, tidy, pathTo } = require('../lib/util') @@ -20,7 +19,7 @@ const { module.exports = v8 async function v8 (args, binary) { - const { status } = args + const { status, outputDir, workingDir, name } = args var node = !binary || binary === 'node' ? await pathTo('node') : binary @@ -34,6 +33,7 @@ async function v8 (args, binary) { stdio: ['ignore', 'pipe', 'inherit'] }) + status('Profiling') const inlined = collectInliningInfo(proc) const { code, manual } = await Promise.race([ @@ -48,15 +48,14 @@ async function v8 (args, binary) { throw err } - var folder = determineOutputDir(args, proc) - ensureDirExists(folder) + const folder = getTargetFolder({outputDir, workingDir, name, pid: proc.pid}) if (process.stdin.isPaused()) { process.stdin.resume() process.stdin.write('\u001b[?25l') } - status('Process exited, generating flamegraph\n') + status('Process exited, generating flamegraph') debug('moving isolate file into folder') const isolateLog = fs.readdirSync(args.workingDir).find(function (f) { diff --git a/schema.json b/schema.json index 8dc61a3..6f4f020 100644 --- a/schema.json +++ b/schema.json @@ -38,10 +38,10 @@ "s": { "type": "boolean" }, - "jsonStacks": { + "treeDebug": { "type": "boolean" }, - "json-stacks": { + "tree-debug": { "type": "boolean" }, "logging-output": { diff --git a/usage.txt b/usage.txt index b27408c..b5f5515 100644 --- a/usage.txt +++ b/usage.txt @@ -48,7 +48,7 @@                        Default: '{outputDir}/{name}.html' - --json-stacks Output a JSON file of stacks as {outputDir}/stacks.{pid}.json + --tree-debug Output a JSON file of stacks as {outputDir}/stacks.{pid}.json --collect-only Do not process captured stacks into a flamegraph. diff --git a/visualizer/actions.js b/visualizer/actions.js index 6ab63c8..98f4dd8 100644 --- a/visualizer/actions.js +++ b/visualizer/actions.js @@ -6,7 +6,6 @@ module.exports = createActions function createActions ({flamegraph, state}, emit) { const { colors } = flamegraph - state.typeFilters.bgs = state.typeFilters.unhighlighted return { @@ -20,6 +19,16 @@ function createActions ({flamegraph, state}, emit) { } } + function highlightTypeFilters () { + return Object.assign( + {}, + state.typeFilters.highlighted, + Array.from(state.typeFilters.exclude).reduce((o, k) => { + o[k] = state.typeFilters.unhighlighted[k] + return o + }, {})) + } + function control () { return ({type}) => { switch (type) { @@ -27,7 +36,7 @@ function createActions ({flamegraph, state}, emit) { state.control.tiers = !state.control.tiers flamegraph.tiers(state.control.tiers) state.typeFilters.bgs = state.control.tiers ? - state.typeFilters.highlighted : + highlightTypeFilters() : state.typeFilters.unhighlighted emit(state) return @@ -83,6 +92,7 @@ function createActions ({flamegraph, state}, emit) { flamegraph.typeHide(name) state.typeFilters.exclude.add(name) } + if (state.control.tiers) state.typeFilters.bgs = highlightTypeFilters() emit(state) } } diff --git a/visualizer/cmp/controls.js b/visualizer/cmp/controls.js index dd880d7..99a7305 100644 --- a/visualizer/cmp/controls.js +++ b/visualizer/cmp/controls.js @@ -2,10 +2,10 @@ const button = (render) => ({label, pressed, disabled, width}, action) => render ` ` } -module.exports = (render) => ({bgs, exclude, enablePreInlined}, action) => { +module.exports = (render) => ({bgs, exclude, enablePreInlined, renderPreInlined}, action) => { const hoc = createHoc(render) - const preInlined = hoc({bg: bgs['pre-inlined'], exclude, name: 'pre-inlined', disabled: !enablePreInlined}, action) + const preInlined = renderPreInlined ? hoc({bg: bgs['pre-inlined'], exclude, name: 'pre-inlined', disabled: !enablePreInlined}, action) : '' const app = hoc({bg: bgs.app, exclude, name: 'app'}, action) const deps = hoc({bg: bgs.deps, exclude, name: 'deps'}, action) const core = hoc({bg: bgs.core, exclude, name: 'core'}, action) @@ -24,7 +25,7 @@ module.exports = (render) => ({bgs, exclude, enablePreInlined}, action) => { const v8 = hoc({bg: bgs.v8, exclude, name: 'v8'}, action) const cpp = hoc({bg: bgs.cpp, exclude, name: 'cpp'}, action) return render ` -
+
${app}${deps}${core}${preInlined}${native}${regexp}${v8}${cpp}
` diff --git a/visualizer/html.js b/visualizer/html.js index c010d71..b064434 100644 --- a/visualizer/html.js +++ b/visualizer/html.js @@ -19,7 +19,7 @@ function html (opts) { diff --git a/visualizer/index.js b/visualizer/index.js index 1d92847..8bc9abb 100644 --- a/visualizer/index.js +++ b/visualizer/index.js @@ -19,7 +19,7 @@ module.exports = function (trees, opts) { const flamegraph = fg({categorizer, tree, exclude: Array.from(exclude), element: chart}) const { colors } = flamegraph - const state = createState({colors, trees, exclude}) + const state = createState({colors, trees, exclude, kernelTracing}) const actions = createActions({flamegraph, state}, (state) => { morphdom(iface, ui({state, actions})) diff --git a/visualizer/state.js b/visualizer/state.js index fcf01a8..1bd4d16 100644 --- a/visualizer/state.js +++ b/visualizer/state.js @@ -2,7 +2,7 @@ const { colorHash } = require('d3-fg') -module.exports = ({colors, trees, exclude, merged = false}) => ({ +module.exports = ({colors, trees, exclude, merged = false, kernelTracing}) => ({ trees, key: { colors: [ @@ -17,10 +17,12 @@ module.exports = ({colors, trees, exclude, merged = false}) => ({ tiers: false, optimized: false, unoptimized: false, + renderOptUnopt: !kernelTracing, merged: merged }, typeFilters: { enablePreInlined: !merged, + renderPreInlined: !kernelTracing, unhighlighted: { 'pre-inlined': '#fff', app: '#fff',