From 0a5eb02a0ee07cf7fed524cd586ebce30df5da8a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 18 Sep 2018 15:59:54 +0200 Subject: [PATCH] Revert "Merge pull request #165 from BridgeAR/filter-init" This reverts commit be8e703b03ed86d0252d3a32399fef17a415a485, reversing changes made to 2bbc814beeee89693f2a55764a5c41ce22d54a61. --- .gitignore | 3 +- lib/ticks-to-tree.js | 61 +++++++++++++++++++++++++++-------- platform/linux.js | 1 - platform/sun.js | 12 +++---- platform/v8.js | 8 ++--- readme.md | 77 ++++++++++++++++++++++---------------------- usage.txt | 48 +++++++++++++-------------- visualizer/index.js | 7 ++-- 8 files changed, 125 insertions(+), 92 deletions(-) diff --git a/.gitignore b/.gitignore index d89ab0c..df8d4d3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ node_modules win package-lock.json todo -.vscode -*.0x \ No newline at end of file +.vscode \ No newline at end of file diff --git a/lib/ticks-to-tree.js b/lib/ticks-to-tree.js index 05d61ea..caaa292 100644 --- a/lib/ticks-to-tree.js +++ b/lib/ticks-to-tree.js @@ -23,7 +23,6 @@ function ticksToTree (ticks, mapFrames, inlined) { stack = removeInstrumentationFrames(stack) if (typeof mapFrames === 'function') stack = mapFrames(stack) if (!stack) return - stack = stack.map(({name, kind, type}, ix) => { name = name.replace(/ (:[0-9]+:[0-9]+)/, (_, loc) => ` [eval]${loc}`) // 0 no info @@ -65,15 +64,14 @@ function ticksToTree (ticks, mapFrames, inlined) { } } if (kind === 'Unopt') S += 1 - else if (kind === 'Opt') S += 2 + if (kind === 'Opt') S += 2 } if (type && type !== 'JS') name += ' [' + type + (kind ? ':' + kind : '') + ']' - return {S, name, value: 0, top: 0} }) - labelInitFrames(stack) + stack = labelInitFrames(stack) addToMergedTree(stack.map(({S, name, value, top}) => ({S, name, value, top}))) // mutate original (save another loop over stack + extra objects) @@ -118,9 +116,9 @@ function ticksToTree (ticks, mapFrames, inlined) { if (child === undefined) { frame.fn = frame.name if (frame.S === 1) frame.name = '~' + frame.name - else if (frame.S === 2) frame.name = '*' + frame.name - else if (frame.S === 3) frame.name = '~' + frame.name + ' [INLINABLE]' - else if (frame.S === 4) frame.name = '*' + frame.name + ' [INLINABLE]' + if (frame.S === 2) frame.name = '*' + frame.name + if (frame.S === 3) frame.name = '~' + frame.name + ' [INLINABLE]' + if (frame.S === 4) frame.name = '*' + frame.name + ' [INLINABLE]' children.push(frame) } else frame = child @@ -135,14 +133,51 @@ function ticksToTree (ticks, mapFrames, inlined) { return { merged, unmerged } } + function labelInitFrames (frames) { - frames.findIndex((frame) => { - if (/^.?\(anonymous\) \/.+\.m?js:[0-9]+:[0-9]+/.test(frame.name)) { - return true - } - frame.name += ' [INIT]' - frame.isInit = true + const startupBootstrapNodeIndex = frames.findIndex(({name}, ix) => { + if (frames[ix + 1] && /Module.runMain module\.js/.test(frames[ix + 1].name)) return false + return /startup bootstrap_node\.js/.test(name) }) + + if (startupBootstrapNodeIndex !== -1) { + frames.slice(startupBootstrapNodeIndex + 1).forEach((frame) => { + if (frame.isInit) return + frame.name += ' [INIT]' + frame.isInit = true + }) + } + + const moduleRunMainIndex = frames.findIndex(({name}, ix) => { + return /Module.runMain module\.js/.test(name) + }) + + if (moduleRunMainIndex !== -1) { + frames.slice(moduleRunMainIndex + 1).forEach((frame) => { + if (frame.isInit) return + if (/.+ (internal\/)?module\.js/.test(frame.name)) frame.name += ' [INIT]' + frame.isInit = true + }) + } + + // if there's so many modules to load, the module requiring may + // actually go into another tick, so far that's been observed where Module.load + // is the first function, but there could be variation... + + const partOfModuleLoadingCycle = frames.findIndex(({name}, ix) => { + return /(Module\.load|Module\._load|tryModuleLoad|Module\._extensions.+|Module\._compile|Module.require|require internal.+) module\.js/.test(name) + }) + + if (partOfModuleLoadingCycle === 0) { + frames.forEach((frame) => { + if (frame.isInit) return + if (/.+ (internal\/)?module\.js/.test(frame.name)) frame.name += ' [INIT]' + frame.isInit = true + }) + } + + + return frames } function removeInstrumentationFrames (frames) { diff --git a/platform/linux.js b/platform/linux.js index e4dd7db..afffc99 100644 --- a/platform/linux.js +++ b/platform/linux.js @@ -45,7 +45,6 @@ function linux (args, sudo, binary, cb) { '--', node, '--perf-basic-prof', - '-r', path.join(__dirname, '..', 'lib', 'preload', 'no-cluster'), '-r', path.join(__dirname, '..', 'lib', 'preload', 'soft-exit'), ...(onPort ? ['-r', path.join(__dirname, '..', 'lib', 'preload', 'detect-port.js')] : []) ].filter(Boolean).concat(args.argv), { diff --git a/platform/sun.js b/platform/sun.js index d09620e..91a7b94 100644 --- a/platform/sun.js +++ b/platform/sun.js @@ -33,7 +33,6 @@ function sun (args, sudo, binary, cb) { args = Object.assign([ '--perf-basic-prof', - '-r', path.join(__dirname, '..', 'lib', 'preload', 'no-cluster'), '-r', path.join(__dirname, '..', 'lib', 'preload', 'soft-exit'), ...(onPort ? ['-r', path.join(__dirname, '..', 'lib', 'preload', 'detect-port.js')] : []) ].concat(args.argv), args) @@ -52,6 +51,7 @@ function sun (args, sudo, binary, cb) { }) var folder var prof + var profExited = false function start () { prof = spawn('sudo', [profile, '-p', proc.pid]) @@ -80,7 +80,7 @@ function sun (args, sudo, binary, cb) { else status('Profiling') start() - + if (onPort) when(proc.stdio[5], 'data').then((port) => { const whenPort = spawnOnPort(onPort, port) whenPort.then(() => proc.kill('SIGINT')) @@ -122,11 +122,11 @@ function sun (args, sudo, binary, cb) { try { process.kill(prof.pid, 'SIGKILL') } catch (e) {} } - try { - translate = sym({silent: true, pid: proc.pid}) + try { + translate = sym({silent: true, pid: proc.pid}) capture(attempts, translate) } catch (e) { - setTimeout(capture, 300, attempts - 1) + setTimeout(capture, 300, attempts - 1) } } else { status('Unable to find map file!\n') @@ -137,7 +137,7 @@ function sun (args, sudo, binary, cb) { } return } - + translate = translate || sym({silent: true, pid: proc.pid}) if (!translate) { diff --git a/platform/v8.js b/platform/v8.js index 91014c5..b2393f0 100644 --- a/platform/v8.js +++ b/platform/v8.js @@ -150,12 +150,12 @@ function collectInliningInfo (sp) { const [ match, inlinedFn ] = /INLINE \((.*)\)/.exec(s) || [ false ] // shouldn't not match though.. if (match === false) return cb() - - if (lastOptimizedFrame === null) return cb() + + if (lastOptimizedFrame === null) return cb() const { fn, file } = lastOptimizedFrame // could be a big problem if the fn doesn't match if (fn !== inlinedFn) return cb() - + const key = `${fn} ${file}` inlined[key] = inlined[key] || [] inlined[key].push(lastOptimizedFrame) @@ -177,7 +177,7 @@ function collectInliningInfo (sp) { if (ix === '-1') root = {file, fn, id, ix, pos, key: `${fn} ${file}`} else { lastOptimizedFrame = {file, fn, id, ix, pos, caller: root} - } + } } else process.stdout.write(s) } diff --git a/readme.md b/readme.md index ef708c5..43bc298 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,7 @@ on any platform which Node runs on (macOs, Linux, Windows, Android...). * Node v8.5.0 and above * Default usage supports any Operating System that Node runs on! * Chrome - * Other browsers may open flamegraphs in a degraded, but functional form + * Other browsers may open flamegraphs in a degraded, but functional form ## Legacy @@ -82,12 +82,12 @@ generates a profile folder (`.0x`), containing `flamegraph.html`. ## The UI -The `flamegraph.html` file contains the 0x UI, which is explained in +The `flamegraph.html` file contains the 0x UI, which is explained in [docs/ui.md](docs/ui.md). ## Production Servers -A lightweight, production server friendly, approach to generating a +A lightweight, production server friendly, approach to generating a flamegraph is described in [docs/production-servers.md](docs/production-servers.md). ## The Profile Folder @@ -125,30 +125,30 @@ Print usage info. Open the flamegraph in the browser using `open` or `xdg-open` (see https://www.npmjs.com/package/open for details). -### --on-port | -P +### --on-port | -P -Run a given command and then generate the flamegraph. -The command as specified has access to a `$PORT` variable. -The `$PORT` variable is set according to the first port that -profiled process opens. +Run a given command and then generate the flamegraph. +The command as specified has access to a `$PORT` variable. +The `$PORT` variable is set according to the first port that +profiled process opens. -For instance, here's an example of using [autocannon](http://npm.im/autocannon) +For instance, here's an example of using [autocannon](http://npm.im/autocannon) to load-test the process: ```sh 0x -P 'autocannon localhost:$PORT' app.js ``` -When the load-test completes, the profiled processed will be -sent a SIGINT and the flamegraph will be automatically generated. +When the load-test completes, the profiled processed will be +sent a SIGINT and the flamegraph will be automatically generated. Remember to use single quotes to avoid bash interpolation, or else escape variable (e.g. `0x -P "autocannon localhost:$PORT" app.js` won't work wheras `0x -P "autocannon localhost:\$PORT" app.js` will). Note: On Windows interpolation usually occurs with `%PORT%`, however -in this case the dollar-prefix `$PORT` is the correct syntax -(because the interpolation is not shell based). +in this case the dollar-prefix `$PORT` is the correct syntax +(because the interpolation is not shell based). Default: '' @@ -156,24 +156,24 @@ Default: '' The name of the HTML file, without the .html extension Can be set to - to write HTML to STDOUT (note -due to the nature of CLI argument parsing, this must be set using `=`, +due to the nature of CLI argument parsing, this must be set using `=`, e.g. `--name=-`). -If either this flag or `--output-html-file` is set to `-` +If either this flag or `--output-html-file` is set to `-` then the HTML will go to STDOUT. Default: flamegraph -### ---title +### ---title Set the title to display in the flamegraph UI. -Default: the command that 0x ran to start the process +Default: the command that 0x ran to start the process ### --output-dir | -D Specify artifact output directory. This can be specified in template -form with possible variables being `{pid}`, `{timestamp}`, `{name}` +form with possible variables being `{pid}`, `{timestamp}`, `{name}` (based on the `--name` flag) and `{outputDir}`(variables must be specified without whitespace, e.g. `{ pid }` is not supported). @@ -181,34 +181,34 @@ Default: `{pid}.0x` ### --output-html | -F -Specify destination of the generated flamegraph HTML file. -This can be specified in template form with possible variables -being `{pid}`, `{timestamp}`, `{name}` (based on the `--name` flag) and -`{outputDir}` (variables must be specified without whitespace, -e.g. `{ pid }` is not supported). It can also be set to `-` to +Specify destination of the generated flamegraph HTML file. +This can be specified in template form with possible variables +being `{pid}`, `{timestamp}`, `{name}` (based on the `--name` flag) and +`{outputDir}` (variables must be specified without whitespace, +e.g. `{ pid }` is not supported). It can also be set to `-` to send the HTML output to STDOUT (note -due to the nature of CLI argument parsing, this must be set using `=`, +due to the nature of CLI argument parsing, this must be set using `=`, e.g. `--output-html=-`). -If either this flag or `--name` is set to `-` +If either this flag or `--name` is set to `-` then the HTML will go to STDOUT. Default: `{outputDir}/{name}.html` ### --kernel-tracing -Use an OS kernel tracing tool (perf on Linux or -dtrace on macOS and SmartOS). This will capture -native stack frames (C++ modules and Libuv I/O), +Use an OS kernel tracing tool (perf on Linux or +dtrace on macOS and SmartOS). This will capture +native stack frames (C++ modules and Libuv I/O), but may result in missing stacks on Node 8. See [docs/kernel-tracing.md](docs/kernel-tracing.md) for more information. -Default: false +Default: false -### --quiet | -q +### --quiet | -q -Limit output, the only output will be fatal errors or +Limit output, the only output will be fatal errors or the path to the `flamegraph.html` upon successful generation. Default: false @@ -226,9 +226,9 @@ with relevant outputs. Default: false -### --visualize-only +### --visualize-only -Supply a path to a profile folder to build or rebuild visualization +Supply a path to a profile folder to build or rebuild visualization from original stacks. Default: undefined @@ -246,7 +246,7 @@ file. Default: false -## Programmatic API +## Programmatic API 0x can also be required as a Node module and scripted: @@ -268,6 +268,7 @@ async function capture () { } capture() + ``` The Programmatic API is detailed in [docs/api.md](docs/api.md). @@ -276,9 +277,9 @@ The Programmatic API is detailed in [docs/api.md](docs/api.md). ### Memory Issues -Very complex applications with lots of stacks may hit memory issues. +Very complex applications with lots of stacks may hit memory issues. -The `--stack-size` flag can be used to set the memory to the maximum 8GB +The `--stack-size` flag can be used to set the memory to the maximum 8GB in order to work around this when profiling: ``` @@ -287,7 +288,7 @@ node --stack-size=8024 $(which 0x) my-app.js There may still be a problem opening the flamegraph in Chrome. The same work around can be used by opening Chrome from the command line (platform dependent) -and nesting the `--stack-size` flag within the `--js-flags` flag: +and nesting the `--stack-size` flag within the `--js-flags` flag: `--js-flags="--stack-size 8024"`. ## Debugging @@ -305,7 +306,7 @@ and nesting the `--stack-size` flag within the `--js-flags` flag: Sponsored by [nearForm](http://nearform.com) This tool is inspired from various info and code sources -and would have taken much longer without the following people and +and would have taken much longer without the following people and their Open Source/Info Sharing efforts: * Thorsten Lorenz () diff --git a/usage.txt b/usage.txt index 5b96736..135df22 100644 --- a/usage.txt +++ b/usage.txt @@ -1,15 +1,15 @@ --open | -o Automatically open after finishing - + Default: false --on-port | -P Run a given command and then generate the flamegraph. The command as specified - has access to a $PORT variable. + has access to a $PORT variable. The $PORT variable is set according - to the first port that profiled process - opens. + to the first port that profiled process + opens. - Example: + Example: 0x -P 'autocannon localhost:$PORT' app.js Note: Remember to use single quotes or else @@ -19,49 +19,49 @@ -q | --quiet Only output flamegraph URI, and fatal errors. - + Default: false -s | --silent Complete silence, 0x will not output anything, other than fatal errors. - + Default: false - --kernel-tracing Use an OS kernel tracing tool (perf on Linux or - dtrace on macOs and Solaris). This will capture - native stack frames (C++ modules and Libuv I/O), + --kernel-tracing Use an OS kernel tracing tool (perf on Linux or + dtrace on macOs and Solaris). This will capture + native stack frames (C++ modules and Libuv I/O), but may result in missing stacks on Node 8. - + Default: false --output-dir | -D Specify artifact output directory. Template variables {outputDir}, {pid}, {timestamp}, {cwd} - (current working directory) and {name} + (current working directory) and {name} (based on the --name flag) are supported. Default: '{pid}.0x' --output-html | -F Specify destination path for the flamegraph HTML file. Template variables {outputDir}, {pid}, {timestamp}, {cwd} - (current working directory) and {name} - (based on the --name flag) are supported. + (current working directory) and {name} + (based on the --name flag) are supported. May also be set to - to send HTML file to STDOUT (note - due to the nature of CLI argument parsing, this must be + due to the nature of CLI argument parsing, this must be set using =, e.g. --output-html=-). - If either this flag or --name is set to - then the HTML - will go to STDOUT. - + If either this flag or --name is set to - then the HTML + will go to STDOUT. +                        Default: '{outputDir}/{name}.html' - --kernel-tracing-debug Show output from dtrace or perf tools. - + --kernel-tracing-debug Show output from dtrace or perf tools. + Default: false --tree-debug Output a JSON file of stacks as {outputDir}/stacks.{pid}.json - + Default: false --collect-only Do not process captured stacks into a flamegraph. @@ -71,16 +71,16 @@ --name The name of the HTML file, without the .html extension Can be set to - to write HTML to STDOUT (note - due to the nature of CLI argument parsing, this must + due to the nature of CLI argument parsing, this must be set using =, e.g. --name=-) - If either this flag or --output-html is set to - + If either this flag or --output-html is set to - then the HTML will go to STDOUT. Default: flamegraph --title Set the title to display in the flamegraph UI - + Default: node [nodeFlags] script.js -v | --version Output the 0x version diff --git a/visualizer/index.js b/visualizer/index.js index 0e551d4..fbf69cf 100644 --- a/visualizer/index.js +++ b/visualizer/index.js @@ -15,12 +15,11 @@ module.exports = function (trees, opts) { const chart = graph() const tree = trees.unmerged // default view - const categorizer = !kernelTracing && graph.v8cats const flamegraph = fg({ - categorizer, - tree, - exclude: Array.from(exclude), + categorizer, + tree, + exclude: Array.from(exclude), element: chart, topOffset: 55 })