├── .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('\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 | {{title}} 13 | 16 | 27 | 28 | 29 |

{{title}}

30 |
31 | Hover over a block for summary information. Click a block for details. 32 | Reset view 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /share/icicle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * icicle.js: implements icicle visualization for stacks 3 | */ 4 | 5 | /* Configuration */ 6 | var svSvgWidth = null; /* image width (null to auto-compute) */ 7 | var svSvgHeight = null; /* image height (null to auto-compute) */ 8 | var svAxisLabelWidth = 45; /* width of axis labels */ 9 | var svChartWidth = null; /* width of chart part of image */ 10 | var svChartHeight = null; /* height of chart part of image */ 11 | var svGrowDown = false; /* if true, stacks are drawn growing down */ 12 | var svTransitionTime = 2000; /* time for transition */ 13 | var svCornerPixels = 3; /* radius of rounded corners */ 14 | var svTextPaddingLeft = 5; /* padding-left on rectangle labels */ 15 | var svTextPaddingRight = 10; /* pading-right on rectangle labels */ 16 | var svTextPaddingTop = '1.2em'; /* padding-top on rectangle labels */ 17 | var svColorMode = 'mono'; /* coloring mode */ 18 | var svDetailMode = 'popout'; /* detail display mode ("zoom" or "popout") */ 19 | 20 | /* Program state */ 21 | var svRawData; /* raw data, filled in by wrapper code */ 22 | var svTooltipBox; /* tooltip box (D3 selection) */ 23 | var svPopoutBox; /* popout detail box (D3 selection) */ 24 | var svFlameGraph; /* main flame graph object */ 25 | var svContext = { 26 | 'detailClose': svDetailClose, 27 | 'detailOpen': svDetailOpen, 28 | 'mouseout': function () { 29 | svTooltipBox.text('').style('opacity', null); 30 | }, 31 | 'mouseover': function (d, det) { 32 | var text, left, top; 33 | 34 | /* escape the key */ 35 | svTooltipBox.text(det['label']); 36 | text = svTooltipBox.html(); 37 | 38 | text = '' + text + '
'; 39 | text += 'Top of stack: ' + 40 | det['pctUnique'] + '% of all samples ' + 41 | '(' + det['nunique'] + ' of ' + 42 | det['nallsamples'] + ' total samples)
'; 43 | text += 'On stack: ' + 44 | det['pctSamples'] + '% of all samples ' + 45 | '(' + det['nsamples'] + ' of ' + 46 | det['nallsamples'] + ' total samples)'; 47 | 48 | left = det['x'] + 'px'; 49 | top = (det['y'] - 50 | parseInt(svTooltipBox.style('height'), 10)) + 'px'; 51 | svTooltipBox.html(text); 52 | svTooltipBox.style('left', left); 53 | svTooltipBox.style('top', top); 54 | svTooltipBox.style('opacity', 0.9); 55 | } 56 | }; 57 | 58 | window.onload = svInit; 59 | 60 | function svInit() 61 | { 62 | svTooltipBox = d3.select('#svTooltip'); 63 | svPopoutBox = d3.select('#svPopout'); 64 | 65 | svFillData(svRawData); 66 | svFlameGraph = new FlameGraph(d3.select('#chart'), svRawData, 67 | svSvgWidth, svSvgHeight, svContext, { 68 | 'coloring': svColorMode, 69 | 'growDown': svGrowDown, 70 | 'axisLabels': true 71 | }); 72 | } 73 | 74 | function svFillData(tree) 75 | { 76 | var key, rem; 77 | 78 | for (key in tree) { 79 | svFillData(tree[key].svChildren); 80 | 81 | rem = tree[key].svUnique; 82 | if (rem > 0) { 83 | tree[key].svChildren[''] = { 84 | 'svSynthetic': true, 85 | 'svUnique': rem, 86 | 'svTotal': rem, 87 | 'svChildren': {} 88 | }; 89 | } 90 | } 91 | } 92 | 93 | function svColorMono(d, det) 94 | { 95 | if (d.data.value.svSynthetic) 96 | return ('#ffffff'); 97 | 98 | var s = 20, splus = 80; 99 | var sratio; 100 | 101 | /* 102 | * This version colors the block based on its percentage contribution of 103 | * overall time. 104 | */ 105 | sratio = d.data.value.svUnique / det['maxUnique']; 106 | 107 | var rh = 24; 108 | var rs = (s + sratio * splus) / 100; 109 | var rv = 0.95; 110 | var rgb = convertHsvToRgb(rh, rs, rv); 111 | return ('rgb(' + rgb.join(',') + ')'); 112 | } 113 | 114 | function svDetailClose() 115 | { 116 | if (svDetailMode != 'zoom') { 117 | svPopoutBox.html(''); 118 | svPopoutBox.style('opacity', null); 119 | svPopoutBox.style('z-index', null); 120 | } else { 121 | svFlameGraph.zoomSet({ 'x': 0, 'dx': 1, 'y': 0 }); 122 | } 123 | } 124 | 125 | /* 126 | * Input: "d", a D3 node from the layout, typically resembling: 127 | * parent: ..., // parent D3 node 128 | * data: { 129 | * key: ..., // function name 130 | * value: { 131 | * svTotal: ..., 132 | * svUnique: ..., 133 | * svChildren: ... 134 | * } 135 | * } 136 | * Output: an object describing the raw flame graph data, matching the form: 137 | * "": { 138 | * svTotal: ... 139 | * svUnique: ... 140 | * svChildren: { 141 | * key1: { // function name 142 | * svTotal: ... 143 | * svUnique: ... 144 | * svChildren: ... 145 | * }, 146 | * ... 147 | * } 148 | * } 149 | */ 150 | function svMakeSubgraphData(d) 151 | { 152 | /* 153 | * First, construct everything from the current node to all of its 154 | * leafs. 155 | */ 156 | var tree, oldtree; 157 | 158 | tree = {}; 159 | tree[d.data.key] = d.data.value; 160 | 161 | while (d.parent !== undefined) { 162 | oldtree = tree; 163 | tree = {}; 164 | tree[d.parent.data.key] = { 165 | 'svUnique': d.parent.data.value.svUnique, 166 | 'svTotal': d.parent.data.value.svTotal, 167 | 'svChildren': oldtree 168 | }; 169 | d = d.parent; 170 | } 171 | 172 | return (tree); 173 | } 174 | 175 | function svDetailOpen(d) 176 | { 177 | if (svDetailMode != 'zoom') { 178 | svPopoutBox.html(''); 179 | /* jsl:ignore */ 180 | new FlameGraph(svPopoutBox, svMakeSubgraphData(d), null, null, 181 | svContext, { 182 | 'coloring': svColorMode, 183 | 'growDown': svGrowDown 184 | }); 185 | /* jsl:end */ 186 | svPopoutBox.style('z-index', 1); 187 | svPopoutBox.style('opacity', 1); 188 | } else { 189 | svFlameGraph.zoomSet(d); 190 | } 191 | } 192 | 193 | /* 194 | * Build a flame graph rooted at the given "node" (a D3 selection) with the 195 | * given "rawdata" tree. The graph will have size defined by "pwidth" and 196 | * "pheight". "context" is used for notifications about UI actions. 197 | */ 198 | function FlameGraph(node, rawdata, pwidth, pheight, context, options) 199 | { 200 | var axiswidth, chartheight, rect, scale, nodeid, axis, data; 201 | var fg = this; 202 | 203 | this.fg_context = context; 204 | this.fg_maxdepth = 0; 205 | this.fg_maxunique = 0; 206 | this.fg_depthsamples = []; 207 | this.computeDepth(rawdata, 0); 208 | 209 | if (options.axisLabels) 210 | axiswidth = this.fg_axiswidth = svAxisLabelWidth; 211 | else 212 | axiswidth = this.fg_axiswidth = 0; 213 | 214 | this.fg_svgwidth = pwidth !== null ? pwidth : 215 | parseInt(node.style('width'), 10); 216 | this.fg_svgheight = pheight !== null ? pheight : 25 * this.fg_maxdepth; 217 | this.fg_chartwidth = this.fg_svgwidth - axiswidth; 218 | chartheight = this.fg_chartheight = this.fg_svgheight - axiswidth; 219 | 220 | this.fg_xscale = 221 | d3.scale.linear().range([0, this.fg_chartwidth]); 222 | this.fg_yscale = 223 | d3.scale.linear().range([0, this.fg_chartheight]); 224 | 225 | this.fg_svg = node.append('svg:svg'); 226 | this.fg_svg.attr('width', this.fg_svgwidth); 227 | this.fg_svg.attr('height', this.fg_svgheight); 228 | 229 | /* Create a background rectangle that resets the view when clicked. */ 230 | rect = this.fg_svg.append('svg:rect'); 231 | rect.attr('class', 'svBackground'); 232 | rect.attr('width', this.fg_svgwidth); 233 | rect.attr('height', this.fg_svgheight); 234 | rect.attr('fill', '#ffffff'); 235 | rect.on('click', this.detailClose.bind(this)); 236 | 237 | /* Configure the partition layout. */ 238 | this.fg_part = d3.layout.partition(); 239 | this.fg_part.children( 240 | function (d) { return (d3.entries(d.value.svChildren)); }); 241 | this.fg_part.value(function (d) { return (d.value.svTotal); }); 242 | this.fg_part.sort(function (d1, d2) { 243 | return (d1.data.key.localeCompare(d2.data.key)); 244 | }); 245 | 246 | /* Configure the color function. */ 247 | if (options.coloring == 'random') { 248 | scale = d3.scale.category20c(); 249 | this.fg_color = function (d) { return (scale(d.data.key)); }; 250 | } else { 251 | this.fg_color = function (d) { 252 | return (svColorMono(d, { 'maxUnique': fg.fg_maxunique })); 253 | }; 254 | } 255 | 256 | /* Configure the actual D3 components. */ 257 | nodeid = this.fg_nodeid = function (d) { 258 | return (encodeURIComponent([ 259 | d.data.key, 260 | fg.fg_yscale(d.y), 261 | fg.fg_xscale(d.x) ].join('@'))); 262 | }; 263 | this.fg_rectwidth = function (d) { return (fg.fg_xscale(d.dx)); }; 264 | this.fg_height = function (d) { return (fg.fg_yscale(d.dy)); }; 265 | this.fg_textwidth = function (d) { 266 | return (Math.max(0, fg.fg_rectwidth(d) - svTextPaddingRight)); 267 | }; 268 | this.fg_x = function (d) { 269 | return (fg.fg_xscale(d.x) + fg.fg_axiswidth); }; 270 | 271 | if (options.growDown) 272 | this.fg_y = 273 | function (d) { return (fg.fg_yscale(d.y)); }; 274 | else 275 | this.fg_y = function (d) { 276 | return (chartheight - fg.fg_yscale(d.y) - 277 | 2 * fg.fg_height(d)); 278 | }; 279 | 280 | data = this.fg_part(d3.entries(rawdata)[0]); 281 | this.fg_rects = this.fg_svg.selectAll('rect').data(data). 282 | enter().append('svg:rect'). 283 | attr('class', function (d) { 284 | return (d.data.value.svSynthetic ? 'svBoxSynthetic' : 'svBox'); 285 | }). 286 | attr('x', this.fg_x). 287 | attr('y', this.fg_y). 288 | attr('rx', svCornerPixels). 289 | attr('ry', svCornerPixels). 290 | attr('height', this.fg_height). 291 | attr('width', this.fg_rectwidth). 292 | attr('fill', this.fg_color). 293 | on('click', this.detailOpen.bind(this)). 294 | on('mouseover', this.mouseover.bind(this)). 295 | on('mouseout', this.mouseout.bind(this)); 296 | this.fg_clips = this.fg_svg.selectAll('clipPath').data(data). 297 | enter().append('svg:clipPath'). 298 | attr('id', nodeid). 299 | append('svg:rect'). 300 | attr('x', this.fg_x). 301 | attr('y', this.fg_y). 302 | attr('width', this.fg_textwidth). 303 | attr('height', this.fg_height); 304 | this.fg_text = this.fg_svg.selectAll('text').data(data). 305 | enter().append('text'). 306 | attr('class', 'svBoxLabel'). 307 | attr('x', this.fg_x). 308 | attr('y', this.fg_y). 309 | attr('dx', svTextPaddingLeft). 310 | attr('dy', svTextPaddingTop). 311 | attr('clip-path', function (d) { 312 | return ('url("#' + nodeid(d) + '")'); 313 | }). 314 | on('click', this.detailOpen.bind(this)). 315 | on('mouseover', this.mouseover.bind(this)). 316 | on('mouseout', this.mouseout.bind(this)). 317 | text(function (d) { return (d.data.key); }); 318 | 319 | if (options.axisLabels) { 320 | axis = this.fg_svg.append('text'); 321 | axis.attr('class', 'svYAxisLabel'); 322 | axis.attr('x', -this.fg_svgheight); 323 | axis.attr('dx', '8em'); 324 | axis.attr('y', '30px'); 325 | axis.attr('transform', 'rotate(-90)'); 326 | axis.text('Call Stacks'); 327 | 328 | axis = this.fg_svg.append('text'); 329 | axis.attr('class', 'svYAxisLabel'); 330 | axis.attr('x', '30px'); 331 | axis.attr('dx', '8em'); 332 | /* 333 | * Magic constants here: 334 | * 30 is the height of the label (since we're specifying the 335 | * top coordinate), and 25 is the height of each block 336 | * (because there's an invisible row we want to cover up). 337 | */ 338 | axis.attr('y', this.fg_svgheight - 30 - 25); 339 | axis.attr('width', this.fg_svgwidth - 30); 340 | axis.text('Percentage of Samples'); 341 | } 342 | } 343 | 344 | FlameGraph.prototype.computeDepth = function (tree, depth) 345 | { 346 | var key, rem; 347 | 348 | if (depth > this.fg_maxdepth) 349 | this.fg_maxdepth = depth; 350 | 351 | if (depth >= this.fg_depthsamples.length) 352 | this.fg_depthsamples[depth] = 0; 353 | 354 | for (key in tree) { 355 | if (tree[key].svUnique > this.fg_maxunique) 356 | this.fg_maxunique = tree[key].svUnique; 357 | this.fg_depthsamples[depth] += tree[key].svTotal; 358 | this.computeDepth(tree[key].svChildren, depth + 1); 359 | 360 | rem = tree[key].svUnique; 361 | if (rem > 0 && tree[key].svChildren[''] === undefined) { 362 | tree[key].svChildren[''] = { 363 | 'svSynthetic': true, 364 | 'svUnique': rem, 365 | 'svTotal': rem, 366 | 'svChildren': {} 367 | }; 368 | } 369 | } 370 | }; 371 | 372 | FlameGraph.prototype.detailClose = function () 373 | { 374 | if (this.fg_context !== null) 375 | this.fg_context.detailClose(); 376 | }; 377 | 378 | FlameGraph.prototype.detailOpen = function (d) 379 | { 380 | if (!d.data.value.svSynthetic && this.fg_context !== null) 381 | this.fg_context.detailOpen(d); 382 | }; 383 | 384 | FlameGraph.prototype.mouseover = function (d) 385 | { 386 | if (d.data.value.svSynthetic || this.fg_context === null) 387 | return; 388 | 389 | var nsamples, nunique; 390 | var pctSamples, pctUnique; 391 | var detail; 392 | var fg = this; 393 | 394 | nsamples = d.data.value.svTotal; 395 | pctSamples = (100 * nsamples / this.fg_depthsamples[0]).toFixed(1); 396 | 397 | nunique = d.data.value.svUnique; 398 | pctUnique = (100 * nunique / this.fg_depthsamples[0]).toFixed(1); 399 | 400 | detail = { 401 | 'label': d.data.key, 402 | 'nsamples': d.data.value.svTotal, 403 | 'nunique': d.data.value.svUnique, 404 | 'nallsamples': this.fg_depthsamples[0], 405 | 'pctSamples': pctSamples, 406 | 'pctUnique': pctUnique, 407 | 'x': d3.event.pageX, 408 | 'y': d3.event.pageY 409 | }; 410 | 411 | this.fg_hoverto = setTimeout(function () { 412 | fg.fg_hoverto = null; 413 | fg.fg_context.mouseover(d, detail); 414 | }, 500); 415 | }; 416 | 417 | FlameGraph.prototype.mouseout = function (d) 418 | { 419 | if (this.fg_hoverto) 420 | clearTimeout(this.fg_hoverto); 421 | if (this.fg_context !== null) 422 | this.fg_context.mouseout(d); 423 | }; 424 | 425 | FlameGraph.prototype.zoomSet = function (cd) 426 | { 427 | var fg = this; 428 | 429 | this.fg_xscale.domain([cd.x, cd.x + cd.dx]); 430 | this.fg_rectwidth = function (d) { 431 | return (fg.fg_xscale(d.x + d.dx) - fg.fg_xscale(d.x)); 432 | }; 433 | this.fg_textwidth = function (d) { 434 | return (Math.max(0, 435 | fg.fg_xscale(d.x + d.dx) - 436 | fg.fg_xscale(d.x) - svTextPaddingRight)); 437 | }; 438 | this.fg_rects.transition().duration(svTransitionTime). 439 | attr('x', this.fg_x). 440 | attr('width', this.fg_rectwidth); 441 | this.fg_clips.transition().duration(svTransitionTime). 442 | attr('x', this.fg_x). 443 | attr('width', this.fg_textwidth); 444 | this.fg_text.transition().duration(svTransitionTime). 445 | attr('x', this.fg_x); 446 | }; 447 | 448 | 449 | /* 450 | * This function is copied directly from lib/color.js. It would be better if we 451 | * could share code between Node.js and web JS. 452 | */ 453 | function convertHsvToRgb(h, s, v) 454 | { 455 | var r, g, b; 456 | var i; 457 | var f, p, q, t; 458 | 459 | if (s === 0) { 460 | /* 461 | * A saturation of 0.0 is achromatic (grey). 462 | */ 463 | r = g = b = v; 464 | 465 | return ([ Math.round(r * 255), Math.round(g * 255), 466 | Math.round(b * 255) ]); 467 | } 468 | 469 | h /= 60; // sector 0 to 5 470 | 471 | i = Math.floor(h); 472 | f = h - i; // fractional part of h 473 | p = v * (1 - s); 474 | q = v * (1 - s * f); 475 | t = v * (1 - s * (1 - f)); 476 | 477 | switch (i) { 478 | case 0: 479 | r = v; 480 | g = t; 481 | b = p; 482 | break; 483 | 484 | case 1: 485 | r = q; 486 | g = v; 487 | b = p; 488 | break; 489 | 490 | case 2: 491 | r = p; 492 | g = v; 493 | b = t; 494 | break; 495 | 496 | case 3: 497 | r = p; 498 | g = q; 499 | b = v; 500 | break; 501 | 502 | case 4: 503 | r = t; 504 | g = p; 505 | b = v; 506 | break; 507 | 508 | default: // case 5: 509 | r = v; 510 | g = p; 511 | b = q; 512 | break; 513 | } 514 | 515 | return ([ Math.round(r * 255), 516 | Math.round(g * 255), Math.round(b * 255) ]); 517 | } 518 | -------------------------------------------------------------------------------- /tools/catest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # catest: a simple testing tool and framework. See usage below for details. 5 | # 6 | 7 | shopt -s xpg_echo 8 | 9 | # 10 | # Global configuration 11 | # 12 | cat_arg0=$(basename $0) # canonical name of "catest" 13 | cat_outbase="catest.$$" # output directory name 14 | cat_tstdir="tst" # top-level test directory 15 | cat_node="node" # relative path to "node" 16 | 17 | # 18 | # Options and arguments 19 | # 20 | cat_tests="" # list of tests (absolute paths) 21 | opt_a=false # run all tests 22 | opt_k=false # keep output of successful tests 23 | opt_o="/var/tmp" # parent directory for output directory 24 | opt_t= # TAP format output file 25 | 26 | # 27 | # Current state 28 | # 29 | cat_outdir= # absolute path to output directory 30 | cat_tapfile= # absolute path of TAP output file 31 | cat_ntests= # total number of tests 32 | cat_nfailed=0 # number of failed tests run 33 | cat_npassed=0 # number of successful tests run 34 | cat_nrun=0 # total number of tests run 35 | 36 | # 37 | # fail MSG: emits the given error message to stderr and exits non-zero. 38 | # 39 | function fail 40 | { 41 | echo "$cat_arg0: $@" >&2 42 | 43 | [[ -n $cat_tapfile ]] && echo "Bail out! $@" >> $cat_tapfile 44 | 45 | exit 1 46 | } 47 | 48 | # 49 | # usage [MSG]: emits the given message, if any, and a usage message, then exits. 50 | # 51 | function usage 52 | { 53 | [[ $# -ne 0 ]] && echo "$cat_arg0: $@\n" >&2 54 | 55 | cat <&2 56 | Usage: $cat_arg0 [-k] [-o dir] [-t file] test1 ... 57 | $cat_arg0 [-k] [-o dir] [-t file] -a 58 | 59 | In the first form, runs specified tests. In the second form, runs all tests 60 | found under "$cat_tstdir" of the form "tst*." for supported extensions. 61 | 62 | TESTS 63 | 64 | Tests are just files to be executed by some interpreter. In most cases, a 65 | test succeeds if it exits successfully and fails otherwise. You can also 66 | specify the expected stdout of the test in a file with the same name as the 67 | test plus a ".out" suffix, in which case the test will also fail if the 68 | actual output does not match the expected output. 69 | 70 | Supported interpreter extensions are "sh" (bash) and "js" (node). 71 | 72 | This framework does not provide per-test setup/teardown facilities, but 73 | test files can do whatever they want, including making use of common 74 | libraries for setup and teardown. 75 | 76 | TEST OUTPUT 77 | 78 | Summary output is printed to stdout. TAP output can be emitted with "-t". 79 | 80 | Per-test output is placed in a new temporary directory inside the directory 81 | specified by the -o option, or /var/tmp if -o is not specified. 82 | 83 | Within the output directory will be a directory for each failed test which 84 | includes a README describing why the test failed (e.g., exited non-zero), a 85 | copy of the test file itself, the actual stdout and stderr of the test, and 86 | the expected stdout of the test (if specified). 87 | 88 | If -k is specified, the output directory will also include a directory for 89 | each test that passed including the stdout and stderr from the test. 90 | 91 | The following options may be specified: 92 | 93 | -a Runs all tests under $cat_tstdir 94 | (ignores other arguments) 95 | -h Output this message 96 | -k Keep output from all tests, not just failures 97 | -o directory Specifies the output directory for tests 98 | (default: /var/tmp) 99 | -t file Emit summary output in TAP format 100 | 101 | USAGE 102 | 103 | exit 2 104 | } 105 | 106 | # 107 | # abspath FILE: emits a canonical, absolute path to the given file or directory. 108 | # 109 | function abspath 110 | { 111 | local dir=$(dirname $1) base=$(basename $1) 112 | 113 | if [[ $base = ".." ]]; then 114 | cd "$dir"/.. > /dev/null || fail "abspath '$1': failed to chdir" 115 | pwd 116 | cd - > /dev/null || fail "abspath '$1': failed to chdir back" 117 | else 118 | cd "$dir" || fail "abspath '$1': failed to chdir" 119 | echo "$(pwd)/$base" 120 | cd - > /dev/null || fail "abspath '$1': failed to chdir back" 121 | fi 122 | } 123 | 124 | # 125 | # cleanup_test TESTDIR "success" | "failure": cleans up the output directory 126 | # for this test 127 | # 128 | function cleanup_test 129 | { 130 | local test_odir="$1" result=$2 131 | local newdir 132 | 133 | if [[ $result = "success" ]]; then 134 | newdir="$(dirname $test_odir)/success.$cat_npassed" 135 | else 136 | newdir="$(dirname $test_odir)/failure.$cat_nfailed" 137 | fi 138 | 139 | mv "$test_odir" "$newdir" 140 | echo $newdir 141 | } 142 | 143 | # 144 | # emit_failure TEST ODIR REASON: indicate that a test has failed 145 | # 146 | function emit_failure 147 | { 148 | local test_label=$1 odir=$2 reason=$3 149 | 150 | if [[ $cat_tapfile ]]; then 151 | echo "not ok $(($cat_nrun+1)) $test_label" >> $cat_tapfile 152 | fi 153 | 154 | echo "FAILED." 155 | echo "$test_path failed: $reason" > "$odir/README" 156 | 157 | [[ -n "$odir" ]] && echo ">>> failure details in $odir\n" 158 | ((cat_nfailed++)) 159 | } 160 | 161 | # 162 | # emit_pass TEST: indicate that a test has passed 163 | # 164 | function emit_pass 165 | { 166 | local test_label=$1 167 | 168 | if [[ $cat_tapfile ]]; then 169 | echo "ok $((cat_nrun+1)) $test_label" >> $cat_tapfile 170 | fi 171 | 172 | echo "success." 173 | ((cat_npassed++)) 174 | } 175 | 176 | # 177 | # Executes a single test 178 | # 179 | # Per-test actions: 180 | # - Make a directory for that test 181 | # - cd into that directory and exec the test 182 | # - Redirect standard output and standard error to files 183 | # - Tests return 0 to indicate success, non-zero to indicate failure 184 | # 185 | function execute_test 186 | { 187 | [[ $# -eq 1 ]] || fail "Missing test to execute" 188 | local test_path=$1 189 | local test_name=$(basename $1) 190 | local test_dir=$(dirname $1) 191 | local test_label=$(echo $test_path | sed -e s#^$SRC/##) 192 | local test_odir="$cat_outdir/test.$cat_nrun" 193 | local ext=${test_name##*.} 194 | local faildir 195 | local EXEC 196 | 197 | echo "Executing test $test_label ... \c " 198 | mkdir "$test_odir" >/dev/null || fail "failed to create test directory" 199 | cp "$test_path" "$test_odir" 200 | 201 | case "$ext" in 202 | "sh") EXEC=bash ;; 203 | "js") EXEC=node ;; 204 | *) faildir=$(cleanup_test "$test_odir" "failure") 205 | emit_failure "$test_label" "$faildir" "unknown file extension" 206 | return 0 207 | ;; 208 | esac 209 | 210 | pushd "$test_dir" >/dev/null 211 | $EXEC $test_name >$test_odir/$$.out 2>$test_odir/$$.err 212 | execres=$? 213 | popd > /dev/null 214 | 215 | if [[ $execres != 0 ]]; then 216 | faildir=$(cleanup_test "$test_odir" "failure") 217 | emit_failure "$test_label" "$faildir" "test returned $execres" 218 | return 0 219 | fi 220 | 221 | if [[ -f $test_path.out ]] && \ 222 | ! diff $test_path.out $test_odir/$$.out > /dev/null ; then 223 | cp $test_path.out $test_odir/$test_name.out 224 | faildir=$(cleanup_test "$test_odir" "failure") 225 | emit_failure "$test_label" "$faildir" "stdout mismatch" 226 | return 0 227 | fi 228 | 229 | cleanup_test "$test_odir" "success" > /dev/null 230 | emit_pass "$test_label" 231 | } 232 | 233 | while getopts ":o:t:akh?" c $@; do 234 | case "$c" in 235 | a|k) eval opt_$c=true ;; 236 | o|t) eval opt_$c="$OPTARG" ;; 237 | h) usage ;; 238 | :) usage "option requires an argument -- $OPTARG" ;; 239 | *) usage "invalid option: $OPTARG" ;; 240 | esac 241 | done 242 | 243 | shift $((OPTIND-1)) 244 | [[ $# -eq 0 && $opt_a == "false" ]] && \ 245 | usage "must specify \"-a\" or list of tests" 246 | 247 | # 248 | # Initialize paths and other environment variables. 249 | # 250 | export SRC=$(abspath $(dirname $0)/..) 251 | export PATH=$SRC/deps/ctf2json:$PATH 252 | [[ -n $HOST ]] || export HOST=$(hostname) 253 | 254 | # 255 | # We create and set CATMPDIR as a place for the tests to store temporary files. 256 | # 257 | export CATMPDIR="/var/tmp/catest.$$_tmpfiles" 258 | 259 | if [[ $opt_a = "true" ]]; then 260 | cat_tests=$(find $SRC/$cat_tstdir \ 261 | -name 'tst*.js' -o -name 'tst*.sh') || \ 262 | fail "failed to locate tests in $SRC/$cat_tstdir" 263 | else 264 | for t in $@; do 265 | [[ -f $t ]] || fail "cannot find test $t" 266 | cat_tests="$(abspath $t) $cat_tests" 267 | done 268 | fi 269 | 270 | mkdir -p "$opt_o/$cat_outbase" 271 | cat_outdir=$(abspath $opt_o/$cat_outbase) 272 | 273 | mkdir -p $CATMPDIR || fail "failed to create $CATMPDIR" 274 | 275 | cat_ntests=$(echo $cat_tests | wc -w) 276 | printf "Configuration:\n" 277 | printf " SRC: $SRC\n" 278 | printf " Output directory: $cat_outdir\n" 279 | printf " Temp directory: $CATMPDIR\n" 280 | if [[ -n "$opt_t" ]]; then 281 | cat_tapfile=$(abspath $opt_t) 282 | printf " TAP output: $cat_tapfile\n" 283 | fi 284 | printf " Keep successful test output: $opt_k\n" 285 | printf " Found %d test(s) to run\n\n" $cat_ntests 286 | 287 | # 288 | # Validate parameters and finish setup. 289 | # 290 | [[ $cat_ntests -gt 0 ]] || fail "no tests found" 291 | 292 | if [[ -n "$cat_tapfile" ]]; then 293 | echo "1..$(($cat_ntests))" > $cat_tapfile || \ 294 | fail "failed to emit TAP output" 295 | fi 296 | 297 | # 298 | # Start the test run. 299 | # 300 | printf "===================================================\n\n" 301 | 302 | for t in $(echo $cat_tests | sort); do 303 | execute_test $t 304 | ((cat_nrun++)) 305 | done 306 | 307 | printf "\n===================================================\n\n" 308 | printf "Results:\n" 309 | printf "\tTests passed:\t%2d/%2d\n" $cat_npassed $cat_nrun 310 | printf "\tTests failed:\t%2d/%2d\n" $cat_nfailed $cat_nrun 311 | printf "\n===================================================\n" 312 | 313 | if [[ $opt_k == "false" ]]; then 314 | echo "Cleaning up output from successful tests ... \c " 315 | rm -rf $cat_outdir/success.* 316 | rm -rf $CATMPDIR 317 | echo "done." 318 | fi 319 | 320 | exit $cat_nfailed 321 | -------------------------------------------------------------------------------- /tools/jsl.node.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration File for JavaScript Lint 3 | # 4 | # This configuration file can be used to lint a collection of scripts, or to enable 5 | # or disable warnings for scripts that are linted via the command line. 6 | # 7 | 8 | ### Warnings 9 | # Enable or disable warnings based on requirements. 10 | # Use "+WarningName" to display or "-WarningName" to suppress. 11 | # 12 | +ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent 13 | +ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity 14 | +ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement 15 | +anon_no_return_value # anonymous function does not always return value 16 | +assign_to_function_call # assignment to a function call 17 | -block_without_braces # block statement without curly braces 18 | +comma_separated_stmts # multiple statements separated by commas (use semicolons?) 19 | +comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) 20 | +default_not_at_end # the default case is not at the end of the switch statement 21 | +dup_option_explicit # duplicate "option explicit" control comment 22 | +duplicate_case_in_switch # duplicate case in switch statement 23 | +duplicate_formal # duplicate formal argument {name} 24 | +empty_statement # empty statement or extra semicolon 25 | +identifier_hides_another # identifer {name} hides an identifier in a parent scope 26 | -inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement 27 | +incorrect_version # Expected /*jsl:content-type*/ control comment. The script was parsed with the wrong version. 28 | +invalid_fallthru # unexpected "fallthru" control comment 29 | +invalid_pass # unexpected "pass" control comment 30 | +jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax 31 | +leading_decimal_point # leading decimal point may indicate a number or an object member 32 | +legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax 33 | +meaningless_block # meaningless block; curly braces have no impact 34 | +mismatch_ctrl_comments # mismatched control comment; "ignore" and "end" control comments must have a one-to-one correspondence 35 | +misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma 36 | +missing_break # missing break statement 37 | +missing_break_for_last_case # missing break statement for last case in switch 38 | +missing_default_case # missing default case in switch statement 39 | +missing_option_explicit # the "option explicit" control comment is missing 40 | +missing_semicolon # missing semicolon 41 | +missing_semicolon_for_lambda # missing semicolon for lambda assignment 42 | +multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs 43 | +nested_comment # nested comment 44 | +no_return_value # function {name} does not always return a value 45 | +octal_number # leading zeros make an octal number 46 | +parseint_missing_radix # parseInt missing radix parameter 47 | +partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag 48 | +redeclared_var # redeclaration of {name} 49 | +trailing_comma_in_array # extra comma is not recommended in array initializers 50 | +trailing_decimal_point # trailing decimal point may indicate a number or an object member 51 | +undeclared_identifier # undeclared identifier: {name} 52 | +unreachable_code # unreachable code 53 | -unreferenced_argument # argument declared but never referenced: {name} 54 | -unreferenced_function # function is declared but never referenced: {name} 55 | +unreferenced_variable # variable is declared but never referenced: {name} 56 | +unsupported_version # JavaScript {version} is not supported 57 | +use_of_label # use of label 58 | +useless_assign # useless assignment 59 | +useless_comparison # useless comparison; comparing identical expressions 60 | -useless_quotes # the quotation marks are unnecessary 61 | +useless_void # use of the void type may be unnecessary (void is always undefined) 62 | +var_hides_arg # variable {name} hides argument 63 | +want_assign_or_call # expected an assignment or function call 64 | +with_statement # with statement hides undeclared variables; use temporary variable instead 65 | 66 | 67 | ### Output format 68 | # Customize the format of the error message. 69 | # __FILE__ indicates current file path 70 | # __FILENAME__ indicates current file name 71 | # __LINE__ indicates current line 72 | # __COL__ indicates current column 73 | # __ERROR__ indicates error message (__ERROR_PREFIX__: __ERROR_MSG__) 74 | # __ERROR_NAME__ indicates error name (used in configuration file) 75 | # __ERROR_PREFIX__ indicates error prefix 76 | # __ERROR_MSG__ indicates error message 77 | # 78 | # For machine-friendly output, the output format can be prefixed with 79 | # "encode:". If specified, all items will be encoded with C-slashes. 80 | # 81 | # Visual Studio syntax (default): 82 | +output-format __FILE__(__LINE__): __ERROR__ 83 | # Alternative syntax: 84 | #+output-format __FILE__:__LINE__: __ERROR__ 85 | 86 | 87 | ### Context 88 | # Show the in-line position of the error. 89 | # Use "+context" to display or "-context" to suppress. 90 | # 91 | +context 92 | 93 | 94 | ### Control Comments 95 | # Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for 96 | # the /*@keyword@*/ control comments and JScript conditional comments. (The latter is 97 | # enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, 98 | # although legacy control comments are enabled by default for backward compatibility. 99 | # 100 | -legacy_control_comments 101 | 102 | 103 | ### Defining identifiers 104 | # By default, "option explicit" is enabled on a per-file basis. 105 | # To enable this for all files, use "+always_use_option_explicit" 106 | -always_use_option_explicit 107 | 108 | # Define certain identifiers of which the lint is not aware. 109 | # (Use this in conjunction with the "undeclared identifier" warning.) 110 | # 111 | # Common uses for webpages might be: 112 | +define __dirname 113 | +define clearInterval 114 | +define clearTimeout 115 | +define console 116 | +define exports 117 | +define global 118 | +define process 119 | +define require 120 | +define setInterval 121 | +define setTimeout 122 | +define Buffer 123 | +define JSON 124 | +define Math 125 | 126 | ### JavaScript Version 127 | # To change the default JavaScript version: 128 | #+default-type text/javascript;version=1.5 129 | #+default-type text/javascript;e4x=1 130 | 131 | ### Files 132 | # Specify which files to lint 133 | # Use "+recurse" to enable recursion (disabled by default). 134 | # To add a set of files, use "+process FileName", "+process Folder\Path\*.js", 135 | # or "+process Folder\Path\*.htm". 136 | # 137 | 138 | -------------------------------------------------------------------------------- /tools/jsl.web.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration File for JavaScript Lint 0.3.0 3 | # Developed by Matthias Miller (http://www.JavaScriptLint.com) 4 | # 5 | # This configuration file can be used to lint a collection of scripts, or to enable 6 | # or disable warnings for scripts that are linted via the command line. 7 | # 8 | 9 | ### Warnings 10 | # Enable or disable warnings based on requirements. 11 | # Use "+WarningName" to display or "-WarningName" to suppress. 12 | # 13 | +no_return_value # function {0} does not always return a value 14 | +duplicate_formal # duplicate formal argument {0} 15 | +equal_as_assign # test for equality (==) mistyped as assignment (=)?{0} 16 | +var_hides_arg # variable {0} hides argument 17 | +redeclared_var # redeclaration of {0} {1} 18 | +anon_no_return_value # anonymous function does not always return a value 19 | +missing_semicolon # missing semicolon 20 | +meaningless_block # meaningless block; curly braces have no impact 21 | +comma_separated_stmts # multiple statements separated by commas (use semicolons?) 22 | +unreachable_code # unreachable code 23 | +missing_break # missing break statement 24 | +missing_break_for_last_case # missing break statement for last case in switch 25 | +comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) 26 | -inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement 27 | +useless_void # use of the void type may be unnecessary (void is always undefined) 28 | -useless_quotes # quotation marks are unnecessary 29 | +multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs 30 | +use_of_label # use of label 31 | -block_without_braces # block statement without curly braces 32 | +leading_decimal_point # leading decimal point may indicate a number or an object member 33 | +trailing_decimal_point # trailing decimal point may indicate a number or an object member 34 | +octal_number # leading zeros make an octal number 35 | +nested_comment # nested comment 36 | +misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma 37 | +ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement 38 | +empty_statement # empty statement or extra semicolon 39 | -missing_option_explicit # the "option explicit" control comment is missing 40 | +partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag 41 | +dup_option_explicit # duplicate "option explicit" control comment 42 | +useless_assign # useless assignment 43 | +ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity 44 | +ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent) 45 | -missing_default_case # missing default case in switch statement 46 | +duplicate_case_in_switch # duplicate case in switch statements 47 | +default_not_at_end # the default case is not at the end of the switch statement 48 | +legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax 49 | +jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax 50 | +useless_comparison # useless comparison; comparing identical expressions 51 | +with_statement # with statement hides undeclared variables; use temporary variable instead 52 | +trailing_comma_in_array # extra comma is not recommended in array initializers 53 | +assign_to_function_call # assignment to a function call 54 | +parseint_missing_radix # parseInt missing radix parameter 55 | -unreferenced_argument # argument declared but never referenced: {name} 56 | 57 | 58 | ### Output format 59 | # Customize the format of the error message. 60 | # __FILE__ indicates current file path 61 | # __FILENAME__ indicates current file name 62 | # __LINE__ indicates current line 63 | # __ERROR__ indicates error message 64 | # 65 | # Visual Studio syntax (default): 66 | +output-format __FILE__(__LINE__): __ERROR__ 67 | # Alternative syntax: 68 | #+output-format __FILE__:__LINE__: __ERROR__ 69 | 70 | 71 | ### Context 72 | # Show the in-line position of the error. 73 | # Use "+context" to display or "-context" to suppress. 74 | # 75 | +context 76 | 77 | 78 | ### Semicolons 79 | # By default, assignments of an anonymous function to a variable or 80 | # property (such as a function prototype) must be followed by a semicolon. 81 | # 82 | #+lambda_assign_requires_semicolon # deprecated setting 83 | 84 | 85 | ### Control Comments 86 | # Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for 87 | # the /*@keyword@*/ control comments and JScript conditional comments. (The latter is 88 | # enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, 89 | # although legacy control comments are enabled by default for backward compatibility. 90 | # 91 | +legacy_control_comments 92 | 93 | 94 | ### JScript Function Extensions 95 | # JScript allows member functions to be defined like this: 96 | # function MyObj() { /*constructor*/ } 97 | # function MyObj.prototype.go() { /*member function*/ } 98 | # 99 | # It also allows events to be attached like this: 100 | # function window::onload() { /*init page*/ } 101 | # 102 | # This is a Microsoft-only JavaScript extension. Enable this setting to allow them. 103 | # 104 | #-jscript_function_extensions # deprecated setting 105 | 106 | 107 | ### Defining identifiers 108 | # By default, "option explicit" is enabled on a per-file basis. 109 | # To enable this for all files, use "+always_use_option_explicit" 110 | -always_use_option_explicit 111 | 112 | # Define certain identifiers of which the lint is not aware. 113 | # (Use this in conjunction with the "undeclared identifier" warning.) 114 | # 115 | # Common uses for webpages might be: 116 | +define setTimeout 117 | +define clearTimeout 118 | +define setInterval 119 | +define clearInterval 120 | +define JSON 121 | +define document 122 | +define window 123 | +define alert 124 | +define XMLHttpRequest 125 | +define d3 126 | 127 | ### Files 128 | # Specify which files to lint 129 | # Use "+recurse" to enable recursion (disabled by default). 130 | # To add a set of files, use "+process FileName", "+process Folder\Path\*.js", 131 | # or "+process Folder\Path\*.htm". 132 | # 133 | #+process jsl-test.js 134 | --------------------------------------------------------------------------------