├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── Makefile.targ ├── README.md ├── cmd ├── flamegraph ├── stackcollapse ├── stackcollapse-perf ├── stackcollapse-stap └── stackvis ├── lib ├── color.js ├── demo.js ├── input-collapsed.js ├── input-dtrace.js ├── input-perf.js ├── input-stap.js ├── output-collapsed.js ├── output-flamegraph-d3.js ├── output-flamegraph-svg.js ├── stackvis.js └── xml.js ├── package.json ├── share ├── d3.v2.js ├── icicle.css ├── icicle.htm └── icicle.js └── tools ├── catest ├── jsl.node.conf └── jsl.web.conf /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TritonDataCenter/node-stackvis/79fba1c7c459b2663d6cc95e1a531c4345528866/.gitmodules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Joyent, Inc. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 3 | # 4 | # Makefile: top-level Makefile 5 | # 6 | # This Makefile contains only repo-specific logic and uses included makefiles 7 | # to supply common targets (javascriptlint, jsstyle, restdown, etc.), which are 8 | # used by other repos as well. 9 | # 10 | 11 | # 12 | # Tools 13 | # 14 | NPM = npm 15 | CATEST = tools/catest 16 | JSL = jsl 17 | JSSTYLE = jsstyle 18 | 19 | # 20 | # Files 21 | # 22 | JS_FILES := $(shell find cmd lib -name '*.js' -not -path 'lib/www/*') 23 | JS_FILES += cmd/flamegraph cmd/stackvis cmd/stackcollapse 24 | 25 | JSL_CONF_NODE = tools/jsl.node.conf 26 | JSL_CONF_WEB = tools/jsl.web.conf 27 | JSL_FILES_NODE = $(JS_FILES) 28 | JSL_FILES_WEB := $(shell find share -name '*.js' -not -name 'd3.*.js') 29 | 30 | JSSTYLE_FLAGS = -oleading-right-paren-ok=1 31 | JSSTYLE_FILES = $(JSL_FILES_NODE) $(JSL_FILES_WEB) 32 | 33 | # 34 | # Repo-specific targets 35 | # 36 | .PHONY: all 37 | all: 38 | $(NPM) install 39 | 40 | .PHONY: test 41 | test: 42 | @echo no tests defined 43 | 44 | include ./Makefile.targ 45 | -------------------------------------------------------------------------------- /Makefile.targ: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.targ: common targets. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This Makefile defines several useful targets and rules. You can use it by 13 | # including it from a Makefile that specifies some of the variables below. 14 | # 15 | # Targets defined in this Makefile: 16 | # 17 | # check Checks JavaScript files for lint and style 18 | # Checks bash scripts for syntax 19 | # Checks SMF manifests for validity against the SMF DTD 20 | # 21 | # clean Removes built files 22 | # 23 | # docs Builds restdown documentation in docs/ 24 | # 25 | # prepush Depends on "check" and "test" 26 | # 27 | # test Does nothing (you should override this) 28 | # 29 | # xref Generates cscope (source cross-reference index) 30 | # 31 | # For details on what these targets are supposed to do, see the Joyent 32 | # Engineering Guide. 33 | # 34 | # To make use of these targets, you'll need to set some of these variables. Any 35 | # variables left unset will simply not be used. 36 | # 37 | # BASH_FILES Bash scripts to check for syntax 38 | # (paths relative to top-level Makefile) 39 | # 40 | # CLEAN_FILES Files to remove as part of the "clean" target. Note 41 | # that files generated by targets in this Makefile are 42 | # automatically included in CLEAN_FILES. These include 43 | # restdown-generated HTML and JSON files. 44 | # 45 | # DOC_FILES Restdown (documentation source) files. These are 46 | # assumed to be contained in "docs/", and must NOT 47 | # contain the "docs/" prefix. 48 | # 49 | # JSL_CONF_NODE Specify JavaScriptLint configuration files 50 | # JSL_CONF_WEB (paths relative to top-level Makefile) 51 | # 52 | # Node.js and Web configuration files are separate 53 | # because you'll usually want different global variable 54 | # configurations. If no file is specified, none is given 55 | # to jsl, which causes it to use a default configuration, 56 | # which probably isn't what you want. 57 | # 58 | # JSL_FILES_NODE JavaScript files to check with Node config file. 59 | # JSL_FILES_WEB JavaScript files to check with Web config file. 60 | # 61 | # You can also override these variables: 62 | # 63 | # BASH Path to bash (default: bash) 64 | # 65 | # CSCOPE_DIRS Directories to search for source files for the cscope 66 | # index. (default: ".") 67 | # 68 | # JSL Path to JavaScriptLint (default: "jsl") 69 | # 70 | # JSL_FLAGS_NODE Additional flags to pass through to JSL 71 | # JSL_FLAGS_WEB 72 | # JSL_FLAGS 73 | # 74 | # JSSTYLE Path to jsstyle (default: jsstyle) 75 | # 76 | # JSSTYLE_FLAGS Additional flags to pass through to jsstyle 77 | # 78 | 79 | # 80 | # Defaults for the various tools we use. 81 | # 82 | BASH ?= bash 83 | BASHSTYLE ?= tools/bashstyle 84 | CP ?= cp 85 | CSCOPE ?= cscope 86 | CSCOPE_DIRS ?= . 87 | JSL ?= jsl 88 | JSSTYLE ?= jsstyle 89 | MKDIR ?= mkdir -p 90 | MV ?= mv 91 | RESTDOWN_FLAGS ?= 92 | RMTREE ?= rm -rf 93 | JSL_FLAGS ?= --nologo --nosummary 94 | 95 | ifeq ($(shell uname -s),SunOS) 96 | TAR ?= gtar 97 | else 98 | TAR ?= tar 99 | endif 100 | 101 | 102 | # 103 | # Defaults for other fixed values. 104 | # 105 | BUILD = build 106 | DISTCLEAN_FILES += $(BUILD) 107 | DOC_BUILD = $(BUILD)/docs/public 108 | 109 | # 110 | # Configure JSL_FLAGS_{NODE,WEB} based on JSL_CONF_{NODE,WEB}. 111 | # 112 | ifneq ($(origin JSL_CONF_NODE), undefined) 113 | JSL_FLAGS_NODE += --conf=$(JSL_CONF_NODE) 114 | endif 115 | 116 | ifneq ($(origin JSL_CONF_WEB), undefined) 117 | JSL_FLAGS_WEB += --conf=$(JSL_CONF_WEB) 118 | endif 119 | 120 | # 121 | # Targets. For descriptions on what these are supposed to do, see the 122 | # Joyent Engineering Guide. 123 | # 124 | 125 | # 126 | # Instruct make to keep around temporary files. We have rules below that 127 | # automatically update git submodules as needed, but they employ a deps/*/.git 128 | # temporary file. Without this directive, make tries to remove these .git 129 | # directories after the build has completed. 130 | # 131 | .SECONDARY: $($(wildcard deps/*):%=%/.git) 132 | 133 | # 134 | # This rule enables other rules that use files from a git submodule to have 135 | # those files depend on deps/module/.git and have "make" automatically check 136 | # out the submodule as needed. 137 | # 138 | deps/%/.git: 139 | git submodule update --init deps/$* 140 | 141 | # 142 | # These recipes make heavy use of dynamically-created phony targets. The parent 143 | # Makefile defines a list of input files like BASH_FILES. We then say that each 144 | # of these files depends on a fake target called filename.bashchk, and then we 145 | # define a pattern rule for those targets that runs bash in check-syntax-only 146 | # mode. This mechanism has the nice properties that if you specify zero files, 147 | # the rule becomes a noop (unlike a single rule to check all bash files, which 148 | # would invoke bash with zero files), and you can check individual files from 149 | # the command line with "make filename.bashchk". 150 | # 151 | .PHONY: check-bash 152 | check-bash: $(BASH_FILES:%=%.bashchk) $(BASH_FILES:%=%.bashstyle) 153 | 154 | %.bashchk: % 155 | $(BASH) -n $^ 156 | 157 | %.bashstyle: % 158 | $(BASHSTYLE) $^ 159 | 160 | .PHONY: check-jsl check-jsl-node check-jsl-web 161 | check-jsl: check-jsl-node check-jsl-web 162 | 163 | check-jsl-node: $(JSL_FILES_NODE:%=%.jslnodechk) 164 | 165 | check-jsl-web: $(JSL_FILES_WEB:%=%.jslwebchk) 166 | 167 | %.jslnodechk: % $(JSL_EXEC) 168 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_NODE) $< 169 | 170 | %.jslwebchk: % $(JSL_EXEC) 171 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_WEB) $< 172 | 173 | .PHONY: check-jsstyle 174 | check-jsstyle: $(JSSTYLE_FILES:%=%.jsstylechk) 175 | 176 | %.jsstylechk: % $(JSSTYLE_EXEC) 177 | $(JSSTYLE) $(JSSTYLE_FLAGS) $< 178 | 179 | .PHONY: check 180 | check: check-jsl check-jsstyle check-bash 181 | @echo check ok 182 | 183 | .PHONY: clean 184 | clean:: 185 | -$(RMTREE) $(CLEAN_FILES) 186 | 187 | .PHONY: distclean 188 | distclean:: clean 189 | -$(RMTREE) $(DISTCLEAN_FILES) 190 | 191 | CSCOPE_FILES = cscope.in.out cscope.out cscope.po.out 192 | CLEAN_FILES += $(CSCOPE_FILES) 193 | 194 | .PHONY: xref 195 | xref: cscope.files 196 | $(CSCOPE) -bqR 197 | 198 | .PHONY: cscope.files 199 | cscope.files: 200 | find $(CSCOPE_DIRS) -name '*.c' -o -name '*.h' -o -name '*.cc' \ 201 | -o -name '*.js' -o -name '*.s' -o -name '*.cpp' > $@ 202 | 203 | # 204 | # The "docs" target is complicated because we do several things here: 205 | # 206 | # (1) Use restdown to build HTML and JSON files from each of DOC_FILES. 207 | # 208 | # (2) Copy these files into $(DOC_BUILD) (build/docs/public), which 209 | # functions as a complete copy of the documentation that could be 210 | # mirrored or served over HTTP. 211 | # 212 | # (3) Then copy any directories and media from docs/media into 213 | # $(DOC_BUILD)/media. This allows projects to include their own media, 214 | # including files that will override same-named files provided by 215 | # restdown. 216 | # 217 | # Step (3) is the surprisingly complex part: in order to do this, we need to 218 | # identify the subdirectories in docs/media, recreate them in 219 | # $(DOC_BUILD)/media, then do the same with the files. 220 | # 221 | DOC_MEDIA_DIRS := $(shell find docs/media -type d 2>/dev/null | grep -v "^docs/media$$") 222 | DOC_MEDIA_DIRS := $(DOC_MEDIA_DIRS:docs/media/%=%) 223 | DOC_MEDIA_DIRS_BUILD := $(DOC_MEDIA_DIRS:%=$(DOC_BUILD)/media/%) 224 | 225 | DOC_MEDIA_FILES := $(shell find docs/media -type f 2>/dev/null) 226 | DOC_MEDIA_FILES := $(DOC_MEDIA_FILES:docs/media/%=%) 227 | DOC_MEDIA_FILES_BUILD := $(DOC_MEDIA_FILES:%=$(DOC_BUILD)/media/%) 228 | 229 | # 230 | # Like the other targets, "docs" just depends on the final files we want to 231 | # create in $(DOC_BUILD), leveraging other targets and recipes to define how 232 | # to get there. 233 | # 234 | .PHONY: docs 235 | docs: \ 236 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.html) \ 237 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.json) \ 238 | $(DOC_MEDIA_FILES_BUILD) 239 | 240 | # 241 | # We keep the intermediate files so that the next build can see whether the 242 | # files in DOC_BUILD are up to date. 243 | # 244 | .PRECIOUS: \ 245 | $(DOC_FILES:%.restdown=docs/%.html) \ 246 | $(DOC_FILES:%.restdown=docs/%json) 247 | 248 | # 249 | # We do clean those intermediate files, as well as all of DOC_BUILD. 250 | # 251 | CLEAN_FILES += \ 252 | $(DOC_BUILD) \ 253 | $(DOC_FILES:%.restdown=docs/%.html) \ 254 | $(DOC_FILES:%.restdown=docs/%.json) 255 | 256 | # 257 | # Before installing the files, we must make sure the directories exist. The | 258 | # syntax tells make that the dependency need only exist, not be up to date. 259 | # Otherwise, it might try to rebuild spuriously because the directory itself 260 | # appears out of date. 261 | # 262 | $(DOC_MEDIA_FILES_BUILD): | $(DOC_MEDIA_DIRS_BUILD) 263 | 264 | $(DOC_BUILD)/%: docs/% | $(DOC_BUILD) 265 | $(CP) $< $@ 266 | 267 | docs/%.json docs/%.html: docs/%.restdown | $(DOC_BUILD) $(RESTDOWN_EXEC) 268 | $(RESTDOWN) $(RESTDOWN_FLAGS) -m $(DOC_BUILD) $< 269 | 270 | $(DOC_BUILD): 271 | $(MKDIR) $@ 272 | 273 | $(DOC_MEDIA_DIRS_BUILD): 274 | $(MKDIR) $@ 275 | 276 | # 277 | # The default "test" target does nothing. This should usually be overridden by 278 | # the parent Makefile. It's included here so we can define "prepush" without 279 | # requiring the repo to define "test". 280 | # 281 | .PHONY: test 282 | test: 283 | 284 | .PHONY: prepush 285 | prepush: check test 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-stackvis 2 | 3 | Stackvis is a command line tool and JavaScript library for visualizing call 4 | stacks. For an example, see 5 | http://us-east.manta.joyent.com/dap/public/stackvis/example.htm. This approach 6 | (and the code for the SVG-based flamegraph) is based heavily on Brendan Gregg's 7 | [FlameGraph](http://github.com/brendangregg/FlameGraph/) tools. 8 | 9 | 10 | ## Synopsis 11 | 12 | Profile a program for 30 seconds: 13 | 14 | # dtrace -n 'profile-97/pid == $YOURPID/{ @[jstack(80, 8192)] = count(); }' -c "sleep 30" > dtrace.out 15 | 16 | then translate the DTrace output into a flame graph: 17 | 18 | # stackvis < dtrace.out > flamegraph.htm 19 | 20 | Or, create the flame graph and share it on Joyent's Manta service: 21 | 22 | # stackvis < dtrace.out | stackvis share 23 | https://us-east.manta.joyent.com/dap/public/stackvis/298c9ae2-aec8-4993-8bc9-d621dcdbeb71/index.htm 24 | 25 | 26 | ## Details 27 | 28 | The default mode assumes input from a DTrace invocation like the above, and 29 | produces a D3-based visualization in a self-contained HTML file. You can 30 | explicitly specify input formats: 31 | 32 | * "dtrace" (the default) 33 | * "collapsed" (more easily grep'd through) 34 | * "perf" (from the Linux "perf" tool) 35 | * "stap" (from SystemTap) 36 | 37 | as well as output formats: 38 | 39 | * "collapsed" (see above) 40 | * "flamegraph-svg" (traditional SVG-based flame graph) 41 | * "flamegraph-d3" (the default) 42 | 43 | For example, to read "collapsed" output and produce a SVG flamegraph, use: 44 | 45 | # stackvis collapsed flamegraph-svg < collapsed.out > flamegraph.svg 46 | 47 | This module also provides the "stackcollapse" and "flamegraph" tools, which are 48 | essentially direct ports of the original FlameGraph tools. You can use them by 49 | first collecting data as above, then collapse common stacks: 50 | 51 | # stackcollapse < dtrace.out > collapsed.out 52 | 53 | then create a flame graph: 54 | 55 | # flamegraph < collapsed.out > graph.svg 56 | 57 | This approach is a little more verbose, but lets you filter out particular 58 | function names by grepping through the collapsed file. 59 | 60 | 61 | ## API 62 | 63 | The command-line tools are thin wrappers around the API, which is built upon a 64 | simple internal representation of stack traces and a bunch of Readers 65 | (lib/input-\*.json) and Writers (lib/output-\*.json) for various intermediate 66 | formats: 67 | 68 | - input-dtrace.js: reads stacks from the output of a DTrace profiling script 69 | - input-collapsed.js: reads data in the form used by the "stackcollapse" tool, 70 | where function offsets are stripped out, common stacks are collapsed, and 71 | there's one stack per line. 72 | - output-collapsed.js: writes stacks in above "collapsed" form 73 | - output-flamegraph-svg.js: writes stacks as a flame graph SVG 74 | - output-flamegraph-d3.js: writes stacks as a flame graph HTML file using D3 75 | 76 | Client code shouldn't load these directly. Instead, require 'stackvis' and use 77 | lookupReader and lookupWriter: 78 | ```javascript 79 | var mod_stackvis = require('stackvis'); 80 | var dtrace_reader = mod_stackvis.readerLookup('dtrace'); 81 | var collapsed_writer = mod_stackvis.writerLookup('collapsed'); 82 | ``` 83 | The main operation is translating from one representation to another (e.g., 84 | DTrace output to a flame graph) using pipeStacks() (which requires a Bunyan 85 | logger): 86 | ```javascript 87 | var mod_bunyan = require('bunyan'); 88 | var log = new mod_bunyan({ 'name': 'mytool', 'stream': process.stderr }); 89 | mod_stackvis.pipeStacks(log, process.stdin, dtrace_reader, collapsed_writer, 90 | process.stdout, function () { console.error('translation finished'); }); 91 | ``` 92 | This example instantiates a new dtrace_reader to read DTrace output from 93 | process.stdin and then emits the result in collapsed form to process.stdout 94 | through the collapsed_writer. 95 | 96 | ## Adding new readers and writers 97 | 98 | It's easy to add new readers (for new input sources) and writers (for new types 99 | of visualizations). See lib/stackvis.js for an overview of how these interfaces 100 | work. 101 | 102 | ## TODO 103 | 104 | - See about dealing with multiple "silos" of a single flame graph that are 105 | essentially the same, but differ in exactly one frame. 106 | - Experiment with flame graph coloring. Current options include random, 107 | gradient, and time-based. Another possibility is to use hue to denote the 108 | module and saturation to denote the size of a frame relative to others at the 109 | same level of depth. 110 | -------------------------------------------------------------------------------- /cmd/flamegraph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * cmd/flamegraph: emit a flame graph SVG from collapsed output 5 | */ 6 | 7 | var mod_bunyan = require('bunyan'); 8 | var mod_getopt = require('posix-getopt'); 9 | var mod_stackvis = require('../lib/stackvis'); 10 | 11 | var log = new mod_bunyan({ 12 | 'name': 'flamegraph', 13 | 'stream': process.stderr 14 | }); 15 | 16 | var reader = new mod_stackvis.readerLookup('collapsed'); 17 | var writer = new mod_stackvis.writerLookup('flamegraph-svg'); 18 | 19 | var parser = new mod_getopt.BasicParser('x:', process.argv); 20 | 21 | var args = {}; 22 | var option; 23 | while ((option = parser.getopt()) !== undefined) { 24 | switch (option.option) { 25 | case 'x': 26 | var arg = option.optarg; 27 | var eq = arg.indexOf('='); 28 | if (eq < 0) { 29 | console.error('warn: ignoring "%s" (no value)', arg); 30 | break; 31 | } 32 | args[arg.substr(0, eq)] = arg.substr(eq + 1); 33 | break; 34 | default: 35 | process.exit(1); 36 | break; 37 | } 38 | } 39 | 40 | mod_stackvis.pipeStacks(log, process.stdin, reader, writer, process.stdout, 41 | args, function () {}); 42 | process.stdin.resume(); 43 | -------------------------------------------------------------------------------- /cmd/stackcollapse: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * cmd/stackcollapse: emit collapsed stack traces from DTrace output 5 | */ 6 | 7 | var mod_bunyan = require('bunyan'); 8 | var mod_stackvis = require('../lib/stackvis'); 9 | 10 | var log = new mod_bunyan({ 11 | 'name': 'stackcollapse', 12 | 'stream': process.stderr 13 | }); 14 | 15 | var reader = mod_stackvis.readerLookup('dtrace'); 16 | var writer = mod_stackvis.writerLookup('collapsed'); 17 | 18 | mod_stackvis.pipeStacks(log, process.stdin, reader, writer, process.stdout, 19 | function () {}); 20 | process.stdin.resume(); 21 | -------------------------------------------------------------------------------- /cmd/stackcollapse-perf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * cmd/stackcollapse: emit collapsed stack traces from perf output 5 | */ 6 | 7 | var mod_bunyan = require('bunyan'); 8 | var mod_stackvis = require('../lib/stackvis'); 9 | 10 | var log = new mod_bunyan({ 11 | 'name': 'stackcollapse', 12 | 'stream': process.stderr 13 | }); 14 | 15 | var reader = mod_stackvis.readerLookup('perf'); 16 | var writer = mod_stackvis.writerLookup('collapsed'); 17 | 18 | mod_stackvis.pipeStacks(log, process.stdin, reader, writer, process.stdout, 19 | function () {}); 20 | process.stdin.resume(); 21 | -------------------------------------------------------------------------------- /cmd/stackcollapse-stap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * cmd/stackcollapse: emit collapsed stack traces from stap output 5 | */ 6 | 7 | var mod_bunyan = require('bunyan'); 8 | var mod_stackvis = require('../lib/stackvis'); 9 | 10 | var log = new mod_bunyan({ 11 | 'name': 'stackcollapse', 12 | 'stream': process.stderr 13 | }); 14 | 15 | var reader = mod_stackvis.readerLookup('stap'); 16 | var writer = mod_stackvis.writerLookup('collapsed'); 17 | 18 | mod_stackvis.pipeStacks(log, process.stdin, reader, writer, process.stdout, 19 | function () {}); 20 | process.stdin.resume(); 21 | -------------------------------------------------------------------------------- /cmd/stackvis: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * cmd/stackvis: convert stacks between different representations 5 | */ 6 | 7 | var mod_fs = require('fs'); 8 | var mod_path = require('path'); 9 | var mod_util = require('util'); 10 | 11 | var mod_bunyan = require('bunyan'); 12 | var mod_getopt = require('posix-getopt'); 13 | var mod_manta = require('manta'); 14 | var mod_uuid = require('node-uuid'); 15 | 16 | var mod_stackvis = require('../lib/stackvis'); 17 | 18 | var log = new mod_bunyan({ 19 | 'name': 'stackvis', 20 | 'stream': process.stderr 21 | }); 22 | 23 | function usage() 24 | { 25 | console.error('usage: stackvis input-format output-format'); 26 | console.error('or stackvis share [FILENAME]'); 27 | process.exit(2); 28 | } 29 | 30 | function main() 31 | { 32 | if (process.argv.length > 4) 33 | usage(); 34 | 35 | if (process.argv.length === 2 || 36 | process.argv[2] != 'share') 37 | cmdTranslate(); 38 | else 39 | cmdShare(); 40 | } 41 | 42 | function cmdShare() 43 | { 44 | var filename, stream; 45 | 46 | if (process.argv.length > 3) { 47 | filename = process.argv[3]; 48 | stream = mod_fs.createReadStream(filename); 49 | stream.on('open', function () { 50 | shareFinish(stream); 51 | }); 52 | stream.on('error', function (err) { 53 | console.error('open "%s": %s', filename, err.message); 54 | process.exit(1); 55 | }); 56 | } else if (process.stdin.isTTY) { 57 | console.error('error: cannot share contents from terminal'); 58 | process.exit(1); 59 | } else { 60 | shareFinish(process.stdin); 61 | } 62 | } 63 | 64 | function shareFinish(stream) 65 | { 66 | var client, objname, dirname; 67 | 68 | if (!process.env['MANTA_USER'] || !process.env['MANTA_URL']) { 69 | console.error('MANTA_USER and MANTA_URL must be set in ' + 70 | 'the environment.'); 71 | console.error('For details, see ' + 72 | 'http://apidocs.joyent.com/manta/#getting-started'); 73 | process.exit(2); 74 | } 75 | 76 | stream.pause(); 77 | objname = mod_util.format('/%s/public/stackvis/%s/index.htm', 78 | process.env['MANTA_USER'], mod_uuid.v4()); 79 | dirname = mod_path.dirname(objname); 80 | client = mod_manta.createBinClient({ 'log': log }); 81 | /* Not really that gentlemanly... */ 82 | process.removeAllListeners('uncaughtException'); 83 | 84 | client.mkdirp(dirname, function (err) { 85 | if (err) { 86 | console.error('mkdirp: %s %s', err.name, err.message); 87 | process.exit(1); 88 | } 89 | 90 | client.put(objname, stream, function (err2) { 91 | if (err2) { 92 | console.error('put: %s %s', 93 | err.name, err.message); 94 | process.exit(1); 95 | } 96 | 97 | console.log('%s%s', process.env['MANTA_URL'], objname); 98 | client.close(); 99 | }); 100 | }); 101 | } 102 | 103 | function cmdTranslate() 104 | { 105 | var reader, writer; 106 | var rname = 'dtrace'; 107 | var wname = 'flamegraph-d3'; 108 | 109 | if (process.argv.length > 2) { 110 | if (process.argv[2][0] == '-') 111 | usage(); 112 | rname = process.argv[2]; 113 | } 114 | 115 | if (process.argv.length > 3) 116 | wname = process.argv[3]; 117 | 118 | try { 119 | reader = new mod_stackvis.readerLookup(rname); 120 | writer = new mod_stackvis.writerLookup(wname); 121 | } catch (ex) { 122 | console.error(ex.message); 123 | usage(); 124 | } 125 | 126 | mod_stackvis.pipeStacks(log, process.stdin, reader, writer, 127 | process.stdout, function () {}); 128 | process.stdin.resume(); 129 | } 130 | 131 | main(); 132 | -------------------------------------------------------------------------------- /lib/color.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/color.js: color utility functions 3 | */ 4 | 5 | var mod_assert = require('assert'); 6 | 7 | /* 8 | * Convert from HSV to RGB. Ported from the Java implementation by Eugene 9 | * Vishnevsky: 10 | * 11 | * http://www.cs.rit.edu/~ncs/color/t_convert.html 12 | */ 13 | exports.convertHsvToRgb = function convertHsvToRgb(h, s, v) 14 | { 15 | var r, g, b; 16 | var i; 17 | var f, p, q, t; 18 | 19 | mod_assert.ok(h >= 0 && h <= 360, 'hue (' + h + ') out of range'); 20 | mod_assert.ok(s >= 0 && s <= 1, 'saturation (' + s + ') out of range'); 21 | mod_assert.ok(v >= 0 && v <= 1, 'value (' + v + ') out of range'); 22 | 23 | if (s === 0) { 24 | /* 25 | * A saturation of 0.0 is achromatic (grey). 26 | */ 27 | r = g = b = v; 28 | 29 | return ([ Math.round(r * 255), Math.round(g * 255), 30 | Math.round(b * 255) ]); 31 | } 32 | 33 | h /= 60; // sector 0 to 5 34 | 35 | i = Math.floor(h); 36 | f = h - i; // fractional part of h 37 | p = v * (1 - s); 38 | q = v * (1 - s * f); 39 | t = v * (1 - s * (1 - f)); 40 | 41 | switch (i) { 42 | case 0: 43 | r = v; 44 | g = t; 45 | b = p; 46 | break; 47 | 48 | case 1: 49 | r = q; 50 | g = v; 51 | b = p; 52 | break; 53 | 54 | case 2: 55 | r = p; 56 | g = v; 57 | b = t; 58 | break; 59 | 60 | case 3: 61 | r = p; 62 | g = q; 63 | b = v; 64 | break; 65 | 66 | case 4: 67 | r = t; 68 | g = p; 69 | b = v; 70 | break; 71 | 72 | default: // case 5: 73 | r = v; 74 | g = p; 75 | b = q; 76 | break; 77 | } 78 | 79 | return ([ Math.round(r * 255), 80 | Math.round(g * 255), Math.round(b * 255) ]); 81 | }; 82 | -------------------------------------------------------------------------------- /lib/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * demo.js: static-file node HTTP server for demos 3 | * 4 | * Usage: node demo.js [port] 5 | * 6 | * Sets up a web server on the given port (or port 80) serving static files 7 | * out of the given path. This demo is NOT secure and allows anyone with 8 | * network access to this server to read any files on your system. 9 | */ 10 | 11 | var mod_fs = require('fs'); 12 | var mod_http = require('http'); 13 | var mod_path = require('path'); 14 | var mod_url = require('url'); 15 | 16 | var dd_index = 'index.htm'; 17 | var dd_cwd = process.cwd(); 18 | var dd_port = 80; 19 | 20 | var i; 21 | 22 | for (i = 2; i < process.argv.length; i++) { 23 | dd_port = parseInt(process.argv[i], 10); 24 | if (isNaN(dd_port)) { 25 | console.error('usage: node demo.js [port]'); 26 | process.exit(1); 27 | } 28 | } 29 | 30 | mod_http.createServer(function (req, res) { 31 | var uri = mod_url.parse(req.url).pathname; 32 | var path; 33 | var filename; 34 | 35 | path = (uri == '/') ? dd_index : uri; 36 | 37 | filename = mod_path.join(dd_cwd, path); 38 | 39 | mod_fs.readFile(filename, function (err, file) { 40 | if (err) { 41 | res.writeHead(404); 42 | res.end(); 43 | return; 44 | } 45 | 46 | res.writeHead(200); 47 | res.end(file); 48 | }); 49 | }).listen(dd_port, function () { 50 | console.log('HTTP server started on port ' + dd_port); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/input-collapsed.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/input-collapsed.js: reads output from the "stackcollapse" script 3 | */ 4 | 5 | var mod_util = require('util'); 6 | var mod_events = require('events'); 7 | 8 | var mod_carrier = require('carrier'); 9 | 10 | exports.reader = CollapsedStreamReader; 11 | 12 | function CollapsedStreamReader(input, log) 13 | { 14 | var reader = this; 15 | 16 | this.csr_log = log; 17 | this.csr_linenum = 0; 18 | this.csr_carrier = mod_carrier.carry(input); 19 | this.csr_carrier.on('line', function (line) { 20 | reader.csr_linenum++; 21 | var match = /^(.*)\s+(\d+)$/.exec(line); 22 | if (!match) { 23 | log.warn('line ' + reader.csr_linenum + ': garbled'); 24 | return; 25 | } 26 | 27 | reader.emit('stack', match[1].split(','), 28 | parseInt(match[2], 10)); 29 | }); 30 | this.csr_carrier.on('end', function () { reader.emit('end'); }); 31 | } 32 | 33 | mod_util.inherits(CollapsedStreamReader, mod_events.EventEmitter); 34 | -------------------------------------------------------------------------------- /lib/input-dtrace.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/input-dtrace.js: reads output from a DTrace profiling script, which emits 3 | * stanzas that look like this: 4 | * 5 | * prog`foo+0x8 6 | * prog`main+0x21 7 | * prog`_start+0x80 8 | * 14 9 | * 10 | * This examples shows that that particular stacktrace was seen 14 times. You 11 | * can generate such output with: 12 | * 13 | * # dtrace -o stacks.out \ 14 | * -n 'profile-97/execname == "myprogram"/{ @[ustack()] = count(); }' 15 | */ 16 | 17 | var mod_util = require('util'); 18 | var mod_events = require('events'); 19 | 20 | var mod_carrier = require('carrier'); 21 | 22 | /* We always ignore the first 3 lines. */ 23 | var NHEADERLINES = 3; 24 | 25 | exports.reader = DTraceStreamReader; 26 | 27 | function DTraceStreamReader(input, log) 28 | { 29 | this.dsr_log = log; 30 | this.dsr_linenum = 0; 31 | this.dsr_stack = []; 32 | this.dsr_carrier = mod_carrier.carry(input); 33 | this.dsr_carrier.on('line', this.onLine.bind(this)); 34 | this.dsr_carrier.on('end', this.onEnd.bind(this)); 35 | 36 | mod_events.EventEmitter.call(this); 37 | } 38 | 39 | mod_util.inherits(DTraceStreamReader, mod_events.EventEmitter); 40 | 41 | DTraceStreamReader.prototype.onLine = function (line) 42 | { 43 | /* The first three lines are always ignored. */ 44 | if (++this.dsr_linenum <= NHEADERLINES) 45 | return; 46 | 47 | var match = /^\s+(\d+)\s*$/.exec(line); 48 | if (match) { 49 | if (this.dsr_stack.length === 0) { 50 | this.dsr_log.warn('line ' + this.dsr_linenum + 51 | ': found count with no stack'); 52 | return; 53 | } 54 | 55 | this.emit('stack', this.dsr_stack, parseInt(match[1], 10)); 56 | this.dsr_stack = []; 57 | return; 58 | } 59 | 60 | /* 61 | * In general, lines may have leading or trailing whitespace and the 62 | * following components: 63 | * 64 | * module`function+offset 65 | * 66 | * We try to avoid assuming too much about the form in order to support 67 | * various annotations provided by ustack helpers, but we want to strip 68 | * off the offset. 69 | */ 70 | var frame = line; 71 | frame = frame.replace(/^\s+/, ''); 72 | frame = frame.replace(/\s+$/, ''); 73 | /* JSSTYLED */ 74 | frame = frame.replace(/\+.*/, ''); 75 | 76 | /* 77 | * Remove both function and template parameters from demangled C++ 78 | * frames, but skip the first two characters because they're used by the 79 | * Node.js ustack helper as separators. 80 | */ 81 | /* JSSTYLED */ 82 | frame = frame.replace(/(..)[(<].*/, '$1'); 83 | 84 | if (line.length === 0) { 85 | if (this.dsr_stack.length !== 0) 86 | this.dsr_log.warn('line ' + this.dsr_linenum + 87 | ': unexpected blank line'); 88 | return; 89 | } 90 | 91 | this.dsr_stack.unshift(frame); 92 | }; 93 | 94 | DTraceStreamReader.prototype.onEnd = function () 95 | { 96 | if (this.dsr_stack.length !== 0) 97 | this.dsr_log.warn('line ' + this.dsr_linenum + 98 | ': unexpected end of stream'); 99 | 100 | this.emit('end'); 101 | }; 102 | -------------------------------------------------------------------------------- /lib/input-perf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/input-perf.js: reads output from a perf profiling script, which emits 3 | * stanzas that look like this: 4 | * 5 | * foo 15150 10062.190770: cycles: 6 | * 400675 bar (/tmp/stackvis/foo) 7 | * 400603 foo (/tmp/stackvis/foo) 8 | * 40071f main (/tmp/stackvis/foo) 9 | * 7fb3db1bf76d __libc_start_main (/lib/x86_64-linux-gnu/libc-2.15.so) 10 | * 11 | * You can generate such output with: 12 | * 13 | * # perf record -F 997 -g ./myprogram 14 | * # perf script > perf.out 15 | */ 16 | 17 | var mod_util = require('util'); 18 | var mod_events = require('events'); 19 | 20 | var mod_carrier = require('carrier'); 21 | 22 | exports.reader = PerfStreamReader; 23 | 24 | function PerfStreamReader(input, log) 25 | { 26 | this.dsr_log = log; 27 | this.dsr_linenum = 0; 28 | this.dsr_prefix = ''; 29 | this.dsr_stack = []; 30 | this.dsr_carrier = mod_carrier.carry(input); 31 | this.dsr_carrier.on('line', this.onLine.bind(this)); 32 | this.dsr_carrier.on('end', this.onEnd.bind(this)); 33 | 34 | mod_events.EventEmitter.call(this); 35 | } 36 | 37 | mod_util.inherits(PerfStreamReader, mod_events.EventEmitter); 38 | 39 | PerfStreamReader.prototype.onLine = function (line) 40 | { 41 | ++this.dsr_linenum; 42 | 43 | /* Lines beginning with # are always ignored. */ 44 | if (/^#/.exec(line)) 45 | return; 46 | 47 | /* Get process name from summary line, to use as prefix */ 48 | var match = /(^\w+)\s+/.exec(line); 49 | if (match) { 50 | this.dsr_prefix = match[1]; 51 | return; 52 | } 53 | 54 | /* 55 | * In general, lines may have leading or trailing whitespace and the 56 | * following components: 57 | * 58 | * loc function (module) 59 | * 60 | * We try to avoid assuming too much about the form in order to support 61 | * various annotations provided by ustack helpers. 62 | */ 63 | var frame = line; 64 | frame = frame.replace(/^\s+/, ''); 65 | frame = frame.replace(/\s+$/, ''); 66 | 67 | if (frame.length === 0) { 68 | if (this.dsr_stack.length === 0) { 69 | this.dsr_log.warn('line ' + this.dsr_linenum + 70 | ': found empty line with no stack'); 71 | return; 72 | } 73 | 74 | this.emit('stack', this.dsr_stack, 1); 75 | this.dsr_prefix = ''; 76 | this.dsr_stack = []; 77 | return; 78 | } 79 | 80 | frame = frame.replace(/^\w+ /, ''); 81 | frame = frame.replace(/ \(\S+\)$/, ''); 82 | 83 | /* 84 | * Remove both function and template parameters from demangled C++ 85 | * frames, but skip the first two characters because they're used by the 86 | * Node.js ustack helper as separators. 87 | */ 88 | /* JSSTYLED */ 89 | frame = frame.replace(/(..)[(<].*/, '$1'); 90 | 91 | if (line.length === 0) { 92 | if (this.dsr_stack.length !== 0) 93 | this.dsr_log.warn('line ' + this.dsr_linenum + 94 | ': unexpected blank line'); 95 | return; 96 | } 97 | 98 | /* Add prefix */ 99 | if (this.dsr_prefix.length > 0) { 100 | frame = this.dsr_prefix + '`' + frame; 101 | } 102 | 103 | this.dsr_stack.unshift(frame); 104 | }; 105 | 106 | PerfStreamReader.prototype.onEnd = function () 107 | { 108 | if (this.dsr_stack.length !== 0) 109 | this.dsr_log.warn('line ' + this.dsr_linenum + 110 | ': unexpected end of stream'); 111 | 112 | this.emit('end'); 113 | }; 114 | -------------------------------------------------------------------------------- /lib/input-stap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/input-stap.js: reads output from a stap profiling script, which emits 3 | * stanzas that look like this: 4 | * 5 | * ubt["bar+0x32 [foo] 6 | * foo+0x57 [foo] 7 | * main+0x48 [foo] 8 | * __libc_start_main+0xed [libc-2.15.so] 9 | * _start+0x29 [foo]"]=0x77 10 | * 11 | * You can generate such output with: 12 | * 13 | * # stap \ 14 | * -e "global ubt; \ 15 | * probe timer.profile { ubt[sprint_ubacktrace()] += 1 }; \ 16 | * probe timer.s(30) { exit() }" \ 17 | * -o stap.out 18 | * 19 | * If stap warns about missing unwind data for a module, and stap 20 | * suggests adding '-d /lib/libquux.so', which you know to be a shared 21 | * library used by the 'foo' binary, add the following to the above 22 | * command: 23 | * 24 | * -d /path/to/foo $(ldd /path/to/foo | awk 'NF==4 { print "-d", $3 }') 25 | * 26 | * to deal with all warnings related to shared libraries used by 'foo', 27 | * all at once. 28 | */ 29 | 30 | var mod_util = require('util'); 31 | var mod_events = require('events'); 32 | 33 | var mod_carrier = require('carrier'); 34 | 35 | exports.reader = PerfStreamReader; 36 | 37 | function PerfStreamReader(input, log) 38 | { 39 | this.dsr_log = log; 40 | this.dsr_linenum = 0; 41 | this.dsr_addingframes = false; 42 | this.dsr_prefixes = []; 43 | this.dsr_stack = []; 44 | this.dsr_carrier = mod_carrier.carry(input); 45 | this.dsr_carrier.on('line', this.onLine.bind(this)); 46 | this.dsr_carrier.on('end', this.onEnd.bind(this)); 47 | 48 | mod_events.EventEmitter.call(this); 49 | } 50 | 51 | mod_util.inherits(PerfStreamReader, mod_events.EventEmitter); 52 | 53 | PerfStreamReader.prototype.onLine = function (line) 54 | { 55 | ++this.dsr_linenum; 56 | 57 | var match; 58 | if (!this.dsr_addingframes) { 59 | /* Skip array name */ 60 | line.replace(/^\w+\[/, ''); 61 | 62 | /* Find and add prefixes */ 63 | while (true) { 64 | /* JSSTYLED */ 65 | match = /(?:"([^"]*)",)(.*$)/.exec(line); 66 | if (!match) 67 | break; 68 | this.dsr_prefixes.push(match[1]); 69 | line = match[2]; 70 | } 71 | 72 | /* Find first frame */ 73 | /* JSSTYLED */ 74 | match = /(?:"(.*$))/.exec(line); 75 | if (!match) { 76 | this.dsr_log.warn('line ' + this.dsr_linenum + 77 | ': no first frame found'); 78 | return; 79 | } 80 | line = match[1]; 81 | this.dsr_addingframes = true; 82 | } 83 | 84 | /* Look for count */ 85 | var count; 86 | /* JSSTYLED */ 87 | match = /(^.*)"\]=(\w+$)/.exec(line); 88 | if (match) { 89 | line = match[1]; 90 | count = parseInt(match[2], 16); 91 | this.dsr_addingframes = false; 92 | } 93 | 94 | /* 95 | * In general, frames have one of the following sets of components: 96 | * 97 | * address 98 | * address [module+offset] 99 | * function+offset [module] 100 | * 101 | * We try to avoid assuming too much about the form in order to support 102 | * various annotations provided by ustack helpers. 103 | */ 104 | var frame = line; 105 | frame = frame.replace(/ \[(\S+)\]$/, ''); 106 | /* JSSTYLED */ 107 | frame = frame.replace(/\+.*/, ''); 108 | 109 | /* 110 | * Remove both function and template parameters from demangled C++ 111 | * frames, but skip the first two characters because they're used by the 112 | * Node.js ustack helper as separators. 113 | */ 114 | /* JSSTYLED */ 115 | frame = frame.replace(/(..)[(<].*/, '$1'); 116 | 117 | if (line.length === 0) { 118 | if (this.dsr_stack.length !== 0) 119 | this.dsr_log.warn('line ' + this.dsr_linenum + 120 | ': unexpected blank line'); 121 | return; 122 | } 123 | 124 | /* Add prefixes */ 125 | if (this.dsr_prefixes.length > 0) { 126 | frame = this.dsr_prefixes.join('`') + '`' + frame; 127 | } 128 | 129 | this.dsr_stack.unshift(frame); 130 | 131 | if (!this.dsr_addingframes) { 132 | this.emit('stack', this.dsr_stack, count); 133 | this.dsr_prefixes = []; 134 | this.dsr_stack = []; 135 | } 136 | }; 137 | 138 | PerfStreamReader.prototype.onEnd = function () 139 | { 140 | if (this.dsr_stack.length !== 0) 141 | this.dsr_log.warn('line ' + this.dsr_linenum + 142 | ': unexpected end of stream'); 143 | 144 | this.emit('end'); 145 | }; 146 | -------------------------------------------------------------------------------- /lib/output-collapsed.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/output-collapsed.js: emits StackSets in collapsed format, compatible with 3 | * Brendan Gregg's FlameGraph tool. 4 | */ 5 | 6 | var mod_assert = require('assert'); 7 | 8 | /* 9 | * Arguments: 10 | * 11 | * stacks StackSet Stacks to visualize 12 | * 13 | * output WritableStream Output file 14 | */ 15 | exports.emit = function emitCollapsed(args, callback) 16 | { 17 | mod_assert.ok(args.stacks && args.stacks.constructor && 18 | args.stacks.constructor.name == 'StackSet', 19 | 'required "stacks" argument must be a StackSet'); 20 | mod_assert.ok(args.output && args.output.write && 21 | typeof (args.output.write) == 'function', 22 | 'required "output" argument must be a function'); 23 | 24 | args.stacks.eachStackByCount(function (frames, count) { 25 | args.output.write(frames.join(',') + ' ' + count + '\n'); 26 | }); 27 | callback(); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/output-flamegraph-d3.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/output-flamegraph-d3.js: emits a D3-based HTML page for the flame graph. 3 | * See lib/stackvis.js for interface details. 4 | */ 5 | 6 | var mod_assert = require('assert'); 7 | var mod_fs = require('fs'); 8 | var mod_path = require('path'); 9 | 10 | var mod_hogan = require('hogan.js'); 11 | var mod_vasync = require('vasync'); 12 | var mod_verror = require('verror'); 13 | 14 | var VError = mod_verror.VError; 15 | 16 | exports.emit = function emitIcicleData(args, callback) 17 | { 18 | mod_assert.ok(args.stacks && args.stacks.constructor && 19 | args.stacks.constructor.name == 'StackSet', 20 | 'required "stacks" argument must be a StackSet'); 21 | mod_assert.ok(args.output && args.output.write && 22 | typeof (args.output.write) == 'function', 23 | 'required "output" argument must be a function'); 24 | mod_assert.ok(args.log, 'required "log" argument must be a logger'); 25 | 26 | var stacks = args.stacks; 27 | var output = args.output; 28 | var tree = {}; 29 | var filecontents = {}; 30 | 31 | stacks.eachStackByStack(function (frames, count) { 32 | var subtree = tree; 33 | var node, i; 34 | 35 | for (i = 0; i < frames.length; i++) { 36 | if (!subtree.hasOwnProperty(frames[i])) 37 | subtree[frames[i]] = { 38 | svUnique: 0, 39 | svTotal: 0, 40 | svChildren: {} 41 | }; 42 | 43 | node = subtree[frames[i]]; 44 | node.svTotal += count; 45 | subtree = node.svChildren; 46 | } 47 | 48 | node.svUnique += count; 49 | }); 50 | 51 | tree = { 52 | '': { 53 | svUnique: 0, 54 | svTotal: Object.keys(tree).reduce( 55 | function (p, c) { return (p + tree[c].svTotal); }, 0), 56 | svChildren: tree 57 | } 58 | }; 59 | 60 | mod_vasync.forEachParallel({ 61 | 'inputs': [ 'icicle.css', 'icicle.js', 'icicle.htm', 'd3.v2.js' ], 62 | 'func': function (filename, stepcb) { 63 | var path = mod_path.join(__dirname, '../share', filename); 64 | var key = filename.replace(/\./g, '_'); 65 | mod_fs.readFile(path, function (err, contents) { 66 | if (err) 67 | err = new VError(err, 'failed to load "%s"', 68 | filename); 69 | else 70 | filecontents[key] = contents.toString('utf8'); 71 | stepcb(err); 72 | }); 73 | } 74 | }, function (err) { 75 | if (err) { 76 | callback(err); 77 | return; 78 | } 79 | 80 | var compiled, rendered; 81 | 82 | filecontents['title'] = 'Flame graph'; 83 | filecontents['rawdata'] = JSON.stringify(tree, null, '\t'); 84 | compiled = mod_hogan.compile(filecontents['icicle_htm']); 85 | rendered = compiled.render(filecontents); 86 | output.write(rendered); 87 | callback(); 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /lib/output-flamegraph-svg.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/output-flamegraph-svg.js: emits StackSets as Flame Graphs in SVG format. 3 | * This is a pretty direct port of Brendan Gregg's FlameGraph tool. 4 | */ 5 | 6 | var mod_assert = require('assert'); 7 | var mod_color = require('./color'); 8 | var mod_xml = require('./xml'); 9 | 10 | exports.emit = function emitFlameGraph(args, callback) 11 | { 12 | var emitter = new FlameGraphEmitter(args); 13 | emitter.run(callback); 14 | }; 15 | 16 | /* 17 | * Arguments: 18 | * 19 | * stacks StackSet Stacks to visualize 20 | * 21 | * output WritableStream Output file 22 | * 23 | * log Bunyan Logger Logger 24 | * 25 | * font_face String Font face for text 26 | * ["Verdana"] 27 | * 28 | * font_size Integer Font size for text 29 | * [12] 30 | * 31 | * title String Title of graph 32 | * ["Flame Graph"] 33 | * 34 | * width Integer Width of graph, in pixels 35 | * [1200] 36 | * 37 | * frame_height Integer Height of each frame, in pixels 38 | * [16] 39 | * 40 | * min_width Number Minimum width of each frame, in pixels 41 | * [0.1] 42 | * 43 | * coloring String Defines how to color each box. Options 44 | * ["time-based"] currently include: 45 | * 46 | * "random" Each box gets a random flame-like color. 47 | * 48 | * "gradient" Each row gets the same color. The colors from 49 | * bottom to top form a gradient from dark red to 50 | * yellow. 51 | * 52 | * "time-based" Each row gets the same hue. Saturation varies 53 | * within each row according to size (width). 54 | */ 55 | function FlameGraphEmitter(args) 56 | { 57 | mod_assert.ok(args.stacks && args.stacks.constructor && 58 | args.stacks.constructor.name == 'StackSet', 59 | 'required "stacks" argument must be a StackSet'); 60 | mod_assert.ok(args.output && args.output.write && 61 | typeof (args.output.write) == 'function', 62 | 'required "output" argument must be a function'); 63 | mod_assert.ok(args.log, 'required "log" argument must be a logger'); 64 | 65 | if (args.coloring) 66 | mod_assert.ok(args.coloring == 'random' || 67 | args.coloring == 'gradient' || 68 | args.coloring == 'time-based', 69 | '"coloring" must be "random", "gradient", or "time-based"'); 70 | 71 | this.fge_stacks = args.stacks; 72 | this.fge_output = args.output; 73 | this.fge_log = args.log; 74 | 75 | this.fge_params = { 76 | 'coloring': args.coloring || 'time-based', 77 | 'font_face': args.font_face || 'Verdana', 78 | 'font_size': Math.floor(args.font_size) || 12, 79 | 'frame_height': Math.floor(args.frame_height) || 16, 80 | 'min_width': parseFloat(args.min_width) || 0.1, 81 | 'title': args.title || 'Flame Graph', 82 | 'width': Math.floor(args.width) || 1200 83 | }; 84 | } 85 | 86 | FlameGraphEmitter.prototype.run = function (callback) 87 | { 88 | /* 89 | * Because the input data comes from a profiler rather than measurements 90 | * at each function entry and exit, the x-axis of our flame graph is not 91 | * meaningful. We know that a given function (at depth D) invoked 92 | * several other functions (at depth D+1), and we know how long was 93 | * spent in each of these functions, but we don't know in what order 94 | * they were called. We have to pick an order to present them, so we do 95 | * it alphabetically. Having done this, one can think of the data as 96 | * though we invoked the alphabetically first function first, then the 97 | * second function next, and so on. Using this mental model, we say 98 | * that a given frame starts at a given "time" (in samples) and lasts 99 | * for a certain "time" (also a number of samples). It's important to 100 | * remember that this doesn't have anything to do with real time, but 101 | * rather the way we're presenting the profiling data. 102 | * 103 | * With this in mind, we process the stacks in order of the above notion 104 | * of time, which is left-to-right in the final flame graph. The final 105 | * output is fge_boxes, which maps a (frame, depth, end) tuple (which 106 | * uniquely identifies a particular box in the flame graph) to an 107 | * integer indicating when that invocation of that frame started (i.e. 108 | * how wide the box is). As part of constructing this, we also maintain 109 | * fge_starts, which maps a (frame, depth) tuple to an integer 110 | * indicating the start time for the most recent invocation of this 111 | * frame at this depth. 112 | */ 113 | var flow = this.flow.bind(this); 114 | 115 | this.fge_boxes = {}; 116 | this.fge_starts = {}; 117 | this.fge_last = []; 118 | this.fge_time = 0; 119 | this.fge_maxdepth = 0; 120 | 121 | /* 122 | * We keep track of the number of samples at each level of depth for 123 | * coloring purposes. 124 | */ 125 | this.fge_depthsamples = []; 126 | 127 | this.fge_stacks.eachStackByStack(flow); 128 | flow([], 0); 129 | 130 | this.draw(callback); 131 | }; 132 | 133 | FlameGraphEmitter.prototype.flow = function (frames, count) 134 | { 135 | var i, nsameframes, starts_key, ends_key; 136 | 137 | /* 138 | * Prepend an empty frame to every real stack to represent the "all 139 | * samples" synthetic frame. The final invocation with frames == [] 140 | * does not correspond to a real stack, but rather causes us to compute 141 | * data for the "all samples" frame. 142 | */ 143 | if (frames.length !== 0) 144 | frames = [''].concat(frames); 145 | 146 | if (frames.length - 1 > this.fge_maxdepth) 147 | this.fge_maxdepth = frames.length - 1; 148 | 149 | for (i = 0; i < this.fge_last.length && i < frames.length; i++) { 150 | if (this.fge_last[i] != frames[i]) 151 | break; 152 | } 153 | 154 | nsameframes = i; 155 | 156 | for (i = this.fge_last.length - 1; i >= nsameframes; i--) { 157 | /* 158 | * Each of these frames was present in the previous stack, but 159 | * not this one, so we mark them having ended here. 160 | */ 161 | starts_key = [ this.fge_last[i], i ].join('--'); 162 | ends_key = [ this.fge_last[i], i, this.fge_time ].join('--'); 163 | this.fge_boxes[ends_key] = this.fge_starts[starts_key]; 164 | this.fge_depthsamples[i] += this.fge_time - 165 | this.fge_starts[starts_key]; 166 | delete (this.fge_starts[starts_key]); 167 | } 168 | 169 | for (i = nsameframes; i < frames.length; i++) { 170 | /* 171 | * Each of these frames was not present in the previous stack, 172 | * so we mark them having started here. 173 | */ 174 | starts_key = [ frames[i], i ].join('--'); 175 | this.fge_starts[starts_key] = this.fge_time; 176 | 177 | if (this.fge_depthsamples[i] === undefined) 178 | this.fge_depthsamples[i] = 0; 179 | } 180 | 181 | this.fge_time += count; 182 | this.fge_last = frames; 183 | }; 184 | 185 | FlameGraphEmitter.prototype.color = function (depth, samples) 186 | { 187 | var r = 205, rplus = 50; 188 | var g = 0, gplus = 230; 189 | var b = 0, bplus = 55; 190 | 191 | if (this.fge_params['coloring'] == 'random') { 192 | return ('rgb(' + [ 193 | r + Math.floor(Math.random() * rplus), 194 | g + Math.floor(Math.random() * gplus), 195 | b + Math.floor(Math.random() + bplus) 196 | ].join(',') + ')'); 197 | } 198 | 199 | if (this.fge_params['coloring'] == 'gradient') { 200 | var ratio = depth / this.fge_maxdepth; 201 | return ('rgb(' + [ 202 | r + Math.floor(ratio * rplus), 203 | g + Math.floor(ratio * gplus), 204 | b + Math.floor(ratio * bplus) 205 | ].join(',') + ')'); 206 | } 207 | 208 | var h = 0, hplus = 60; 209 | var s = 30, splus = 70; 210 | var v = 80, vplus = 20; 211 | 212 | var hratio = depth / this.fge_maxdepth; 213 | var sratio = samples / this.fge_depthsamples[depth]; 214 | 215 | var rh = h + hratio * hplus; 216 | var rs = (s + sratio * splus) / 100; 217 | var rv = (v + hratio * vplus) / 100; 218 | var rgb = mod_color.convertHsvToRgb(rh, rs, rv); 219 | 220 | return ('rgb(' + rgb.join(',') + ')'); 221 | }; 222 | 223 | FlameGraphEmitter.prototype.draw = function (callback) 224 | { 225 | var xml = new mod_xml.XmlEmitter(this.fge_output); 226 | 227 | var fontface = this.fge_params['font_face']; 228 | var fontsize = this.fge_params['font_size']; 229 | 230 | var xpad = 10; 231 | var ypadtop = fontsize * 4 + fontsize * 2 + 10; 232 | var ypadbtm = fontsize * 2 + 10; 233 | 234 | var width = this.fge_params['width']; 235 | var widthpersample = (width - 2 * xpad) / this.fge_time; 236 | var height = this.fge_maxdepth * this.fge_params['frame_height'] + 237 | ypadtop + ypadbtm; 238 | 239 | var black = 'rgb(0, 0, 0)'; 240 | 241 | xml.emitDoctype('svg', 'PUBLIC "-//W3C//DTD SVG 1.1//EN"', 242 | 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'); 243 | 244 | xml.emitStart('svg', { 245 | 'version': '1.1', 246 | 'width': width, 247 | 'height': height, 248 | 'onload': 'init(evt)', 249 | 'viewBox': [ 0, 0, width, height ].join(' '), 250 | 'xmlns': 'http://www.w3.org/2000/svg' 251 | }); 252 | 253 | xml.emitStart('defs'); 254 | xml.emitStart('linearGradient', { 255 | 'id': 'background', 256 | 'y1': 0, 257 | 'y2': 1, 258 | 'x1': 0, 259 | 'x2': 0 260 | }); 261 | xml.emitEmpty('stop', { 'stop-color': '#eeeeee', 'offset': '5%' }); 262 | xml.emitEmpty('stop', { 'stop-color': '#eeeeb0', 'offset': '95%' }); 263 | xml.emitEnd('linearGradient'); 264 | xml.emitEnd('defs'); 265 | 266 | xml.emitStart('style', { 'type': 'text/css' }); 267 | xml.emitCData( 268 | ' rect[rx]:hover { stroke:black; stroke-width:1; }\n' + 269 | ' text:hover { stroke:black; stroke-width:1; ' + 270 | 'stroke-opacity:0.35; }\n' 271 | ); 272 | xml.emitEnd('style'); 273 | 274 | xml.emitStart('script', { 'type': 'text/ecmascript' }); 275 | xml.emitCData( 276 | ' var details;\n' + 277 | ' function init(evt) {\n' + 278 | ' details = document.getElementById("details").\n' + 279 | ' firstChild;\n' + 280 | ' }\n' + 281 | ' function s(info) { details.nodeValue = info; }\n' + 282 | ' function c() { details.nodeValue = " "; }\n' 283 | ); 284 | xml.emitEnd('script'); 285 | 286 | emitRectangle(xml, 0, 0, width, height, 'url(#background)', {}); 287 | 288 | emitString(xml, black, fontface, fontsize + 5, Math.floor(width / 2), 289 | fontsize * 2, this.fge_params['title'], 'middle', {}); 290 | 291 | emitString(xml, black, fontface, fontsize, xpad, 292 | ypadtop - ypadbtm, 'Function:', 'left', {}); 293 | 294 | emitString(xml, black, fontface, fontsize, xpad + 60, 295 | ypadtop - ypadbtm, ' ', 'left', { 'id': 'details' }); 296 | 297 | for (var ident in this.fge_boxes) { 298 | var parts = ident.split('--'); 299 | var func = parts[0]; 300 | var depth = parseInt(parts[1], 10); 301 | var endtime = parseInt(parts[2], 10); 302 | 303 | var starttime = this.fge_boxes[ident]; 304 | var nsamples = endtime - starttime; 305 | 306 | var x1 = xpad + starttime * widthpersample; 307 | var x2 = xpad + endtime * widthpersample; 308 | var boxwidth = x2 - x1; 309 | 310 | if (boxwidth < this.fge_params['min_width']) 311 | continue; 312 | 313 | var y1 = height - ypadbtm - (depth + 1) * 314 | this.fge_params['frame_height'] + 1; 315 | var y2 = height - ypadbtm - depth * 316 | this.fge_params['frame_height']; 317 | 318 | var info; 319 | if (func.length === 0 && depth === 0) { 320 | info = 'all samples (' + nsamples + ' samples, 100%)'; 321 | } else { 322 | var pct = ((100 * nsamples) / this.fge_time).toFixed(2); 323 | info = func + ' (' + nsamples + ' samples, ' + 324 | pct + '%)'; 325 | } 326 | 327 | var color = this.color(depth, nsamples); 328 | 329 | emitRectangle(xml, x1, y1, x2, y2, color, { 330 | 'rx': 2, 331 | 'ry': 2, 332 | 'onmouseover': 's("' + info + '")', 333 | 'onmouseout': 'c()' 334 | }); 335 | 336 | if (boxwidth > 50) { 337 | var nchars = Math.floor(boxwidth / (0.7 * fontsize)); 338 | 339 | var text; 340 | if (nchars < func.length) 341 | text = func.substr(0, nchars) + '..'; 342 | else 343 | text = func; 344 | 345 | emitString(xml, black, fontface, fontsize, x1 + 3, 346 | 3 + (y1 + y2) / 2, text, 'left', { 347 | 'onmouseover': 's("' + info + '")', 348 | 'onmouseout': 'c()' 349 | }); 350 | } 351 | } 352 | 353 | xml.emitEnd('svg'); 354 | 355 | /* 356 | * XXX It's a little disingenuous to invoke the callback now because we 357 | * don't actually know whether our output has been successfully written 358 | * or just buffered inside node. We really should implement flow 359 | * control here by keeping track of how many rectangles we've emitted, 360 | * and if we find that our output has been buffered, we simply stop 361 | * until the underlying stream emits "drain", at which point we pick up 362 | * where we left off. 363 | */ 364 | callback(); 365 | }; 366 | 367 | function emitRectangle(xml, x1, y1, x2, y2, fill, extra) 368 | { 369 | var attrs = { 370 | 'x': x1.toFixed(1), 371 | 'y': y1.toFixed(1), 372 | 'width': (x2 - x1).toFixed(1), 373 | 'height': (y2 - y1).toFixed(1), 374 | 'fill': fill 375 | }; 376 | 377 | for (var key in extra) 378 | attrs[key] = extra[key]; 379 | 380 | xml.emitEmpty('rect', attrs); 381 | } 382 | 383 | function emitString(xml, color, font, size, x, y, str, loc, extra) 384 | { 385 | var attrs = { 386 | 'text-anchor': loc, 387 | 'x': x, 388 | 'y': y, 389 | 'font-size': size, 390 | 'font-family': font, 391 | 'fill': color 392 | }; 393 | 394 | for (var key in extra) 395 | attrs[key] = extra[key]; 396 | 397 | xml.emitStart('text', attrs, { 'bare': true }); 398 | xml.emitCData(str); 399 | xml.emitEnd('text', { 'bare': true }); 400 | } 401 | -------------------------------------------------------------------------------- /lib/stackvis.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/stackvis.js: Stackvis library interface 3 | */ 4 | 5 | var mod_assert = require('assert'); 6 | var mod_jsprim = require('jsprim'); 7 | 8 | exports.readerLookup = readerLookup; 9 | exports.writerLookup = writerLookup; 10 | exports.pipeStacks = pipeStacks; 11 | 12 | /* 13 | * Readers read stacktraces stored in specific formats and emit them in a common 14 | * form. Consumers don't use readers directly, but rather pass them to 15 | * pipeStacks(). 16 | * 17 | * Each reader is defined in its own module. It's expected to export a single 18 | * field called "reader" that's a constructor function, which we return here. 19 | * Reader constructors should be invoked as new constructor(ReadableStream, 20 | * BunyanLog). The object itself should then emit "stack" and "end" events. 21 | * The "stack" event includes an array of frames and a count for the number of 22 | * times that stack was seen in the input: 23 | * 24 | * reader.on('stack', function (frames, count) { 25 | * console.log(count + ' ' + frames.join(', ')); 26 | * }); 27 | * 28 | * Readers may emit the same stack more than once, which isn't what most 29 | * consumers want. That's why the main API is pipeStacks, which collapses 30 | * common stacks. 31 | */ 32 | function readerLookup(name) 33 | { 34 | return (moduleLookup('input', name).reader); 35 | } 36 | 37 | /* 38 | * Writers take stacktraces stored in our common form and emit them in some 39 | * other specific format like a data file or a SVG visualization. Like readers, 40 | * each writer is defined in its own module, and callers use them via 41 | * pipeStacks() rather than directly. 42 | * 43 | * Since writers do not emit events, they're not constructors. Each writer just 44 | * defines a single field called "emit", invoked as emit(args, callback). 45 | * "args" must contain these fields: 46 | * 47 | * stacks StackSet Stacks to visualize, as produced by 48 | * collapseStacks. 49 | * 50 | * output WritableStream Output file 51 | * 52 | * log Bunyan Logger Logger 53 | * 54 | * as well as any other module-specific parameters. 55 | */ 56 | function writerLookup(name) 57 | { 58 | return (moduleLookup('output', name)); 59 | } 60 | 61 | function moduleLookup(type, name) 62 | { 63 | var filename = './' + type + '-' + name; 64 | return (require(filename)); 65 | } 66 | 67 | /* 68 | * Reads stacks from "instream" (a ReadableStream) using a new "readercons" 69 | * Reader object, collapses the stacks, and emits them to "outstream" using the 70 | * given writer. This is the main way to convert stacks from one representation 71 | * (e.g., DTrace output) to another (e.g., a flamegraph). 72 | * 73 | * This is the primary interface for consumers, though it's *not* a stable 74 | * interface yet. 75 | */ 76 | function pipeStacks(log, instream, readercons, writer, outstream, args, 77 | callback) 78 | { 79 | if (typeof (args) === 'function') { 80 | callback = args; 81 | args = {}; 82 | } 83 | args = args || {}; 84 | 85 | var reader = new readercons(instream, log); 86 | 87 | collapseStacks(reader, function (err, stacks) { 88 | if (err) { 89 | log.error(err); 90 | return; 91 | } 92 | 93 | var _args = mod_jsprim.deepCopy(args); 94 | _args.stacks = stacks; 95 | _args.output = outstream; 96 | _args.log = log; 97 | writer.emit(_args, function (err2) { 98 | /* 99 | * It's stupid that we need to check whether we're 100 | * writing to stdout, but this is the same thing Node's 101 | * stream.pipe() method does, because for some reason 102 | * you can't "end" the stdout stream. 103 | */ 104 | if (!err2 && !outstream._isStdio) 105 | outstream.end(); 106 | 107 | callback(err2); 108 | }); 109 | }); 110 | } 111 | 112 | /* 113 | * Collects "stack" events from the given reader, collapses common stacks, and 114 | * returns them asynchronously via "callback". This could reasonably be a 115 | * public interface, but for now we assume that the only consumers would be 116 | * translators which would use the slightly higher-level pipeStacks() instead. 117 | */ 118 | function collapseStacks(reader, callback) 119 | { 120 | var stacks = new StackSet(); 121 | 122 | reader.on('stack', function (stack, count) { 123 | stacks.addStack(stack, count); 124 | }); 125 | 126 | reader.on('end', function () { callback(null, stacks); }); 127 | } 128 | 129 | /* 130 | * Internal representation for a collapsed set of stacks. 131 | */ 132 | function StackSet() 133 | { 134 | this.ss_counts = {}; /* maps serialized stack -> count */ 135 | this.ss_stacks = {}; /* maps serialized stack -> stack */ 136 | } 137 | 138 | StackSet.prototype.addStack = function (stack, count) 139 | { 140 | mod_assert.ok(Array.isArray(stack)); 141 | mod_assert.equal(typeof (count), 'number'); 142 | 143 | var key = stack.join(','); 144 | 145 | if (!this.ss_counts.hasOwnProperty(key)) { 146 | this.ss_counts[key] = 0; 147 | this.ss_stacks[key] = stack; 148 | } 149 | 150 | this.ss_counts[key] += count; 151 | }; 152 | 153 | /* 154 | * Iterates all stacks in order of decreasing count. The callback function is 155 | * invoked as callback(frames, count) for each unique stack. 156 | */ 157 | StackSet.prototype.eachStackByCount = function (callback) 158 | { 159 | var set = this; 160 | var keys = Object.keys(this.ss_stacks); 161 | 162 | keys.sort(function (a, b) { 163 | return (set.ss_counts[b] - set.ss_counts[a]); 164 | }); 165 | 166 | keys.forEach(function (key) { 167 | callback(set.ss_stacks[key], set.ss_counts[key]); 168 | }); 169 | }; 170 | 171 | /* 172 | * Iterates all stacks in alphabetical order by full stack. 173 | */ 174 | StackSet.prototype.eachStackByStack = function (callback) 175 | { 176 | var set = this; 177 | var keys = Object.keys(this.ss_stacks); 178 | 179 | keys.sort().forEach(function (key) { 180 | callback(set.ss_stacks[key], set.ss_counts[key]); 181 | }); 182 | }; 183 | -------------------------------------------------------------------------------- /lib/xml.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lib/xml.js: XML utility routines 3 | */ 4 | 5 | var mod_assert = require('assert'); 6 | 7 | exports.XmlEmitter = XmlEmitter; 8 | 9 | /* 10 | * Basic interface for emitting well-formed XML. This isn't bulletproof, but it 11 | * does escape values (not tags or keys) and checks for basic errors. 12 | */ 13 | function XmlEmitter(stream) 14 | { 15 | this.xe_stream = stream; 16 | this.xe_stack = []; 17 | } 18 | 19 | XmlEmitter.prototype.emitDoctype = function (name, type, path) 20 | { 21 | this.xe_stream.write('\n'); 22 | this.xe_stream.write('\n'); 24 | }; 25 | 26 | XmlEmitter.prototype.escape = function (str) 27 | { 28 | /* BEGIN JSSTYLED */ 29 | return (str.toString().replace(/&/g, '&'). 30 | replace(//g, '>'). 32 | replace(/"/g, '"')); 33 | /* END JSSTYLED */ 34 | }; 35 | 36 | XmlEmitter.prototype.emitIndent = function () 37 | { 38 | var str = ''; 39 | var i; 40 | 41 | for (i = 0; i < this.xe_stack.length; i++) 42 | str += ' '; 43 | 44 | this.xe_stream.write(str); 45 | }; 46 | 47 | XmlEmitter.prototype.emitEmpty = function (name, attrs) 48 | { 49 | this.emitIndent(); 50 | this.xe_stream.write('<' + name + ' '); 51 | this.emitAttrs(attrs); 52 | this.xe_stream.write('/>\n'); 53 | }; 54 | 55 | XmlEmitter.prototype.emitAttrs = function (attrs) 56 | { 57 | var key; 58 | 59 | if (!attrs) 60 | return; 61 | 62 | for (key in attrs) 63 | this.xe_stream.write(key + '=\"' + 64 | this.escape(attrs[key]) + '\" '); 65 | }; 66 | 67 | XmlEmitter.prototype.emitStart = function (name, attrs, opts) 68 | { 69 | this.emitIndent(); 70 | this.xe_stack.push(name); 71 | 72 | this.xe_stream.write('<' + name + ' '); 73 | this.emitAttrs(attrs); 74 | this.xe_stream.write('>'); 75 | 76 | if (!opts || !opts['bare']) 77 | this.xe_stream.write('\n'); 78 | }; 79 | 80 | XmlEmitter.prototype.emitEnd = function (name, opts) 81 | { 82 | var check = this.xe_stack.pop(); 83 | 84 | mod_assert.equal(name, check); 85 | 86 | if (!opts || !opts['bare']) 87 | this.emitIndent(); 88 | 89 | this.xe_stream.write('' + name + '>\n'); 90 | }; 91 | 92 | XmlEmitter.prototype.emitCData = function (data) 93 | { 94 | this.xe_stream.write(this.escape(data)); 95 | }; 96 | 97 | XmlEmitter.prototype.emitComment = function (content) 98 | { 99 | this.xe_stream.write('\n'); 100 | }; 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stackvis", 3 | "version": "0.5.0", 4 | "description": "stack visualization tools", 5 | "main": "./lib/stackvis.js", 6 | "bin": { 7 | "flamegraph": "./cmd/flamegraph", 8 | "stackcollapse": "./cmd/stackcollapse", 9 | "stackcollapse-perf": "./cmd/stackcollapse-perf", 10 | "stackcollapse-stap": "./cmd/stackcollapse-stap", 11 | "stackvis": "./cmd/stackvis" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/davepacheco/node-stackvis.git" 16 | }, 17 | "dependencies": { 18 | "bunyan": "1.8.1", 19 | "carrier": "0.1.7", 20 | "hogan.js": "2.0.0", 21 | "manta": "3.0.0", 22 | "node-uuid": "1.4.1", 23 | "posix-getopt": "1.0.0", 24 | "vasync": "1.4.0", 25 | "verror": "1.3.6", 26 | "jsprim": "0.5.1" 27 | }, 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /share/icicle.css: -------------------------------------------------------------------------------- 1 | /* 2 | * icicle.css: styles for Icicle visualization 3 | */ 4 | body { 5 | font-family: sans-serif; 6 | font-size: small; 7 | padding-left: 20px; 8 | padding-right: 20px; 9 | color: #333333; 10 | } 11 | 12 | a { 13 | color: #CC2900; 14 | } 15 | 16 | div#chart { 17 | width: 100%; 18 | margin-top: 20px; 19 | } 20 | 21 | rect.svBox { 22 | stroke: #fff; 23 | } 24 | 25 | rect.svBox:hover { 26 | cursor: pointer; 27 | } 28 | 29 | .svBoxLabel { 30 | cursor: pointer; 31 | fill: #333333; 32 | } 33 | 34 | div.svTooltip { 35 | opacity: 0; 36 | z-index: 2; 37 | position: absolute; 38 | height: 3em; 39 | white-space: nowrap; 40 | padding-top: 8px; 41 | padding-left: 12px; 42 | padding-right: 12px; 43 | padding-bottom: 18px; 44 | font-size: small; 45 | background: #ffffee; 46 | border: 0px; 47 | border-radius: 2px; 48 | pointer-events: none; 49 | } 50 | 51 | div.svPopout { 52 | position: absolute; 53 | border-radius: 2px; 54 | border: 2px #333333 solid; 55 | padding: 6px; 56 | background-color: #ffffff; 57 | opacity: 0; 58 | z-index: -1; 59 | width: 50%; 60 | } 61 | 62 | .svXAxisLabel { 63 | font-size: large; 64 | font-weight: bold; 65 | font-variant: small-caps; 66 | text-align: center; 67 | width: 100%; 68 | letter-spacing: 0.3em; 69 | margin-bottom: 20px; 70 | } 71 | 72 | .svYAxisLabel { 73 | font-size: large; 74 | font-weight: bold; 75 | font-variant: small-caps; 76 | text-align: center; 77 | letter-spacing: 0.3em; 78 | white-space: nowrap; 79 | } 80 | -------------------------------------------------------------------------------- /share/icicle.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{! If you're reading this, you're looking at a template. The following }} 5 | {{! HTML comment applies to files generated from this template, not this }} 6 | {{! template itself. }} 7 | 11 | 12 |