├── .gitignore ├── .npmignore ├── README.md ├── package.json ├── src ├── index.coffee ├── polyfill.coffee └── result.coffee └── test └── profile_test.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node async-profile profiles CPU usage in node apps. 2 | 3 | It lets you see at a glance how much CPU time is being taken up by a given part of your app, even if that 4 | part of your app is also doing asynchronous IO. 5 | 6 | I built it at [Bugsnag](https://bugsnag.com) to help us understand why our background processors were 7 | using 100% CPU all the time. 8 | 9 | # Installation 10 | 11 | This currently only works on node 0.10. 0.11 support should be easy to add, and much lower overhead :). 12 | 13 | 14 | ``` 15 | npm install async-profile 16 | ``` 17 | 18 | # Usage 19 | 20 | Call `AsyncProfile.profile` with a function. That function will be called asynchronously, and all of the timeouts and network events it causes will also be profiled. A summary will then be printed. 21 | 22 | ```javascript 23 | var AsyncProfile = require('async-profile') 24 | 25 | AsyncProfile.profile(function () { 26 | 27 | // doStuff 28 | setTimeout(function () { 29 | // doAsyncStuff 30 | }); 31 | 32 | }); 33 | ``` 34 | 35 | For more options see [the advanced usage section](#Advanced) 36 | 37 | ## Interpreting the output 38 | 39 | The output looks something like this: (taken from a profile of [bugsnag](https://bugsnag.com)'s backend) 40 | 41 | ``` 42 | total: 1.823ms (in 2.213ms real time, CPU load: 0.8, wait time: 3.688ms) 43 | 0.879: 0.011ms at Function.Project.fromCache (/0/bugsnag/event-worker/lib/project.coffee:12:16) (0.072ms) 44 | 0.970: 0.363ms [no mark] (0.250ms) 45 | 1.589: 0.002ms at /0/bugsnag/event-worker/workers/notify.coffee:29:13 (0.000ms) 46 | 1.622: 0.010ms at /0/bugsnag/event-worker/workers/notify.coffee:30:13 (0.000ms) 47 | 1.668: 0.043ms at Event.hash (/0/bugsnag/event-worker/lib/event/event.coffee:238:16) (0.061ms) 48 | 1.780: 0.064ms at /0/bugsnag/event-worker/lib/event/event.coffee:246:21 (0.098ms) 49 | 2.016: 0.064ms at Object.exports.count (/0/bugsnag/event-worker/lib/throttling.coffee:12:14) (0.122ms) 50 | 2.250: 0.052ms REDIS EVAL SCRIPT (0.123) 51 | 2.506: 0.166ms at throttleProjectEvent (/0/bugsnag/event-worker/lib/throttling.coffee:125:14) (0.295ms) 52 | 2.433: 0.002ms at throttleProjectEvent (/0/bugsnag/event-worker/lib/throttling.coffee:125:14) (0.000ms) 53 | 2.211: 0.002ms at throttleAccountEvent (/0/bugsnag/event-worker/lib/throttling.coffee:73:14) (0.000ms) 54 | 1.947: 0.002ms at Object.exports.count (/0/bugsnag/event-worker/lib/throttling.coffee:12:14) (0.000ms) 55 | 1.593: 0.001ms at Event.hash (/0/bugsnag/event-worker/lib/event/event.coffee:238:16) (0.000ms) 56 | 0.775: 0.003ms at Function.Project.fromCache (/0/bugsnag/event-worker/lib/project.coffee:12:16) (0.000ms) 57 | ``` 58 | 59 | The first line contains 4 numbers: 60 | 61 | * `total` — the total amount of time spent running CPU. 62 | * `real time` — the amount of time between the first callack starting and the last callback ending. 63 | * `CPU load` — is just `total / real time`. As node is singlethreaded, this number ranges between 0 (CPU wasn't doing anything) and 1 (CPU was running the whole time). 64 | * `wait time` — the sum of the times between each callback being created and being called. High wait times can happen either because you're waiting for a lot of parallel IO events, or because you're waiting for other callbacks to stop using the CPU. 65 | 66 | Each subsequent line contains 4 bits of information: 67 | * `start`: The time since you called `new AsyncProfile()` and when this callback started running. 68 | * `cpu time`: The amount of CPU time it took to execute this callback. 69 | * `location`: The point in your code at which this callback was created. (see also [marking](#marking)). 70 | * `overhead`: The amount of CPU time it took to calculate `location` (see also [speed](#speed)) which has been subtraced from the `cpu time` column. 71 | 72 | Additionally the indentation lets you re-construct the tree of callbacks. 73 | 74 | ## Marking 75 | 76 | Sometimes it's hard to figure out exactly what's running when, particularly as the point at which the underlying async callback is created might not 77 | correspond to the location of a callback function in your code. At any point while the profiler is running you can mark the current callback to 78 | make it easy to spot in the profiler output. 79 | 80 | ```javascipt 81 | AsyncProfile.mark 'SOMETHING EASY TO SPOT' 82 | ``` 83 | 84 | For example in the above output, I've done that for the callback that was running `redis.eval` and marked it as `'REDIS EVAL SCRIPT'`. 85 | 86 | # Advanced 87 | 88 | If you need advanced behaviour, you need to create the profiler manually, and then run some code. The profiler will be active for any callbacks created synchronously after it was. 89 | 90 | ```javascript 91 | 92 | setTimeout(function () { 93 | p = new AsyncProfiler(); 94 | 95 | setTimeout(function () { 96 | // doStuff 97 | 98 | }); 99 | }); 100 | 101 | 102 | ``` 103 | 104 | ## Speed 105 | 106 | Like all profilers, this one comes with some overhead. In fact, by default it has so much overhead that I had to calculate it and then subtract it from the results :p. 107 | 108 | There is some overhead not included in the overhead numbers, but it should hopefully be fairly insignficant (1-10μs or so per async call) and also not included in the profiler output. 109 | 110 | You can make the profiler faster by creating it with the fast option. This disables both stack-trace calculation, and overhead calculation. 111 | 112 | ```javascript 113 | new AsyncProfile({fast: true}) 114 | ``` 115 | 116 | ## Stopping 117 | *also known as "help, it's not displaying anything"* 118 | 119 | If your process happens to make an infinite cascade of callbacks (often this happens with promises libraries), then you will have to manually stop the profiler manually. For example using a promise you might want to do something like: 120 | 121 | ```javascript 122 | 123 | var p = new AsyncProfile() 124 | Promise.try(doWork).finally(function () { 125 | p.stop(); 126 | }); 127 | ``` 128 | 129 | ## Custom reports 130 | 131 | You can pass a callback into the constructor to generate your own output. The default callback looks like this: 132 | 133 | ```javascript 134 | new AsyncProfile({ 135 | callback: function (result) { 136 | result.print(); 137 | } 138 | ); 139 | ``` 140 | 141 | The result object looks like this: 142 | 143 | ```javascript 144 | { 145 | start: [1, 000000000], # process.hrtime() 146 | end: [9, 000000000], # process.hrtime() 147 | ticks: [ 148 | { 149 | queue: [1, 000000000], # when the callback was created 150 | start: [2, 000000000], # when the callback was called 151 | end: [3, 000000000], # when the callback finished 152 | overhead: [0, 000100000], # how much time was spent inside the profiler itself 153 | parent: { ... }, # the tick that was running when the callback was created 154 | } 155 | ] 156 | } 157 | ``` 158 | 159 | This gives you a flattened tree of ticks, sorted by `queue` time. The parent will always come before its children in the array. 160 | 161 | # Common problems 162 | 163 | ## No output is produced 164 | 165 | Try manually [stopping](#stopping) the profiler. You might have an infinite chain of callbacks, or no callbacks at all. 166 | 167 | ## Some callbacks are missing 168 | 169 | We're using [`async-listener`](https://www.npmjs.org/package/async-listener) under the hood, and it sometimes can't "see" beyond 170 | some libraries (like redis or mongo) that use connection queues. 171 | 172 | The solution is to manually create a bridge over the asynchronous call. You can look at the code to see how I did it for mongo and 173 | redis. Pull requests are welcome. 174 | 175 | ## Crashes on require with async-listener polyfill warning. 176 | 177 | Either you're using node 0.11 (congrats!) or you're including 178 | [`async-listener`](https://www.npmjs.org/package/async-listener) from multiple 179 | places. 180 | 181 | You can fix this by sending a pull request :). 182 | 183 | 184 | # Meta-fu 185 | 186 | async-profile is licensed under the MIT license. Comments, pull-requests and issue reports are welcome. 187 | 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "async-listener": "0.4.5" 4 | }, 5 | "description": "Node.js async CPU profiler", 6 | "devDependencies": { 7 | "coffee-script": "latest", 8 | "mocha": "latest" 9 | }, 10 | "engines": { 11 | "node": ">=0.8.0" 12 | }, 13 | "homepage": "https://github.com/ConradIrwin/async-profile", 14 | "licenses": [ 15 | { 16 | "type": "MIT", 17 | "url": "https://github.com/ConradIrwin/async-profile" 18 | } 19 | ], 20 | "main": "lib/index.js", 21 | "maintainers": [ 22 | { 23 | "email": "conrad@bugsnag.com", 24 | "name": "Conrad Irwin" 25 | } 26 | ], 27 | "name": "async-profile", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/ConradIrwin/async-profile" 31 | }, 32 | "scripts": { 33 | "prepublish": "coffee -c -o lib src/*", 34 | "test": "mocha --compilers coffee:coffee-script/register test" 35 | }, 36 | "version": "0.4.0" 37 | } 38 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | Result = require './result' 2 | Polyfills = require './polyfill' 3 | 4 | class AsyncProfile 5 | 6 | @active = [] 7 | 8 | constructor: (opts) -> 9 | @callback = opts?.callback || (result) -> result.print() 10 | @fast = opts?.fast || false 11 | @awaiting = 0 12 | @ticks = [] 13 | @tick = null 14 | @start = null 15 | @end = null 16 | 17 | AsyncProfile.active.push(@) 18 | 19 | @listener = process.addAsyncListener( 20 | () => @create() 21 | { 22 | before: (_, tick) => @before(tick) 23 | after: (_, tick) => @after(tick) 24 | error: (tick, err) => @after(tick) 25 | } 26 | ) 27 | 28 | # Create a new tick to be called later 29 | create: -> 30 | return if @end 31 | overhead = if @tick && !@fast 32 | process.hrtime() 33 | @awaiting += 1 34 | tick = {queue: process.hrtime(), parent: @tick, overhead: [0,0]} 35 | @start ||= tick.queue 36 | tick.stack = @stack() unless @fast 37 | @ticks.push(tick) 38 | if overhead 39 | overhead = process.hrtime(overhead) 40 | @tick.overhead[0] += overhead[0] 41 | @tick.overhead[1] += overhead[1] 42 | 43 | tick 44 | 45 | # Called at the beginning of a tick 46 | before: (tick) -> 47 | return if @end 48 | @awaiting -= 1 49 | tick.previous = @tick 50 | @tick = tick 51 | @tick.start = process.hrtime() 52 | 53 | # Called at the end of a tick 54 | after: (tick) -> 55 | return if @end 56 | @tick.end ||= process.hrtime() 57 | 58 | @stop() if @awaiting == 0 59 | previous = @tick.previous 60 | @tick.previous = null 61 | @tick = previous 62 | 63 | # Cheaply capture the stack (without file/line info) 64 | stack: -> 65 | orig = Error.prepareStackTrace 66 | Error.prepareStackTrace = (_, stack) -> stack 67 | err = new Error() 68 | Error.captureStackTrace(err, arguments.callee) 69 | stack = err.stack 70 | Error.prepareStackTrace = orig 71 | return err.stack 72 | 73 | # Mark the current tick (see also @mark) 74 | mark: (context) -> 75 | if @tick 76 | @tick.mark = context 77 | 78 | # Stop profiling and call the callback 79 | stop: -> 80 | return if @end 81 | @tick.end ||= process.hrtime() if @tick 82 | @end = @tick?.end || process.hrtime() 83 | process.removeAsyncListener(@listener) 84 | 85 | i = AsyncProfile.active.indexOf(@) 86 | AsyncProfile.active.splice(i, 1) 87 | 88 | process.nextTick => 89 | @callback(new Result(@)) 90 | 91 | # Profile the provided function 92 | @profile: (fn, args...) -> 93 | process.nextTick(() -> 94 | new AsyncProfile(args...) 95 | process.nextTick(fn) 96 | ) 97 | 98 | # mark the current tick 99 | @mark: (context) -> 100 | for profile in AsyncProfile.active 101 | profile.mark(context) 102 | 103 | # stop any current profilers 104 | @stop: (context) -> 105 | for profile in AsyncProfile.active 106 | profile.stop(context) 107 | 108 | module.exports = AsyncProfile 109 | -------------------------------------------------------------------------------- /src/polyfill.coffee: -------------------------------------------------------------------------------- 1 | require 'async-listener' 2 | wrapCallback = require 'async-listener/glue' 3 | 4 | module.exports = () -> # extends AsyncListener 5 | @wrap = (fn) -> 6 | wrapCallback(fn) 7 | 8 | @bridge = (fn) -> 9 | () -> 10 | index = arguments.length - 1 11 | if typeof arguments[index] == "function" 12 | arguments[index] = wrapCallback(arguments[index]) 13 | fn.apply(this, arguments) 14 | 15 | @bridgeRedisPackage = (redis) -> 16 | fn = redis.RedisClient.prototype.send_command 17 | redis.RedisClient.prototype.send_command = (command, args, callback) -> 18 | if Array.isArray(args) 19 | if typeof callback == 'function' 20 | callback = AsyncProfile.wrap(callback) 21 | else if !callback 22 | index = args.length - 1 23 | if typeof args[index] == "function" 24 | args[index] = AsyncProfile.wrap(args[index]) 25 | 26 | fn.call(this, command, args, callback) 27 | 28 | @bridgeMongoDb = (db) -> 29 | db._executeInsertCommand = AsyncProfile.bridge(db._executeInsertCommand) 30 | db._executeQueryCommand = AsyncProfile.bridge(db._executeQueryCommand) 31 | db._executeUpdateCommand = AsyncProfile.bridge(db._executeUpdateCommand) 32 | db._executeRemoveCommand = AsyncProfile.bridge(db._executeRemoveCommand) 33 | 34 | -------------------------------------------------------------------------------- /src/result.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Result 3 | 4 | constructor: (profile) -> 5 | 6 | @ticks = profile.ticks 7 | @start = profile.start 8 | @end = profile.end 9 | 10 | header: () -> 11 | sum = [0, 0] 12 | wait = [0, 0] 13 | min = [Infinity, Infinity] 14 | max = [0, 0] 15 | for _, tick of @ticks 16 | continue if tick.ignore 17 | 18 | if tick.queue[0] < min[0] || (tick.queue[0] == min[0] && tick.queue[1] < min[1]) 19 | min = tick.queue 20 | if tick.end[0] > max[0] || (tick.end[0] == max[0] && tick.queue[1] > max[1]) 21 | max = tick.end 22 | 23 | sum[0] += tick.end[0] - tick.start[0] - tick.overhead[0] 24 | sum[1] += tick.end[1] - tick.start[1] - tick.overhead[1] 25 | wait[0] += tick.start[0] - tick.queue[0] 26 | wait[1] += tick.start[1] - tick.queue[1] 27 | 28 | total = [sum[0] + wait[0], sum[1] + wait[1]] 29 | 30 | "total: #{@time(sum)}ms (in #{@diff(max, min)}ms real time, CPU load: #{(@time(sum) / @diff(max, min)).toFixed(1)}, wait time: #{@time(wait)}ms)" 31 | 32 | print: (parent=null, from=0, indent="") -> 33 | 34 | if parent == null 35 | for i in [from...@ticks.length] 36 | @ticks[i].ignore = true unless @ticks[i].queue && @ticks[i].start && @ticks[i].end 37 | 38 | console.log @header() 39 | 40 | for i in [from...@ticks.length] 41 | tick = @ticks[i] 42 | continue if tick.parent != parent 43 | continue if tick.ignore 44 | 45 | if tick.stack && !tick.mark 46 | tick.mark = @getLineFromStack(tick.stack) 47 | 48 | time = [tick.end[0] - tick.start[0] - tick.overhead[0], tick.end[1] - tick.start[1] - tick.overhead[1]] 49 | 50 | console.log "#{@diff(tick.start, @start)}: #{@time(time)}ms #{indent} #{tick.mark || "[no mark]"} (#{@time(tick.overhead)}) " 51 | 52 | @print(tick, 0, indent + " ") 53 | 54 | if parent == null 55 | console.log "" 56 | 57 | getLineFromStack: (stack) -> 58 | if Error.prepareStackTrace 59 | stack = Error.prepareStackTrace(new Error("ohai"), stack) 60 | else 61 | stack = "new Error('ohai')\n" + 62 | stack.map((f) -> " at #{f.toString()}\n").join("") 63 | 64 | lines = stack.split("\n") 65 | for l in lines 66 | return l.replace(/^\s*/,'') if l.indexOf(process.cwd()) > -1 && l.indexOf('node_modules') < l.indexOf(process.cwd()) 67 | 68 | for l in lines 69 | return l.replace(/^\s*/,'') if l.indexOf(process.cwd()) > -1 && l.indexOf('async-profile') == -1 70 | 71 | for l in lines.slice(1) 72 | return l.replace(/^\s*/,'') if l.indexOf('async-profile') == -1 73 | 74 | diff: (after, before) -> 75 | @time([after[0] - before[0], after[1] - before[1]]) 76 | 77 | time: (delta) -> 78 | ((1000 * delta[0]) + (delta[1] / 1000000)).toFixed(3) 79 | 80 | stop: () -> 81 | return if @end 82 | @end ||= process.hrtime() 83 | process.removeAsyncListener(@listener) 84 | @opts.callback(@) 85 | 86 | module.exports = Result 87 | -------------------------------------------------------------------------------- /test/profile_test.coffee: -------------------------------------------------------------------------------- 1 | AsyncProfile = require '../src' 2 | assert = require 'assert' 3 | describe 'AsyncProfile', -> 4 | 5 | it 'should call the callback', (done) -> 6 | 7 | new AsyncProfile( 8 | callback: (result) -> 9 | done() 10 | ) 11 | process.nextTick -> 12 | 13 | it 'should wait for all ticks', (done) -> 14 | new AsyncProfile( 15 | callback: (result) -> 16 | assert.equal result.ticks.length, 4 17 | result.print() 18 | done() 19 | ) 20 | 21 | process.nextTick -> 22 | process.nextTick -> 23 | process.nextTick -> 24 | process.nextTick -> 25 | 26 | it 'should work with the helper', (done) -> 27 | AsyncProfile.profile((->), callback: (result) -> 28 | assert.equal result.ticks.length, 1 29 | 30 | assert.deepEqual result.start, result.ticks[0].queue 31 | assert.deepEqual result.end, result.ticks[0].end 32 | done() 33 | ) 34 | 35 | it 'should let you mark ticks', (done) -> 36 | new AsyncProfile( 37 | callback: (result) -> 38 | assert.equal result.ticks.length, 2 39 | 40 | assert.deepEqual result.ticks[0].mark, 'one' 41 | assert.deepEqual result.ticks[1].mark, 'two' 42 | done() 43 | ) 44 | 45 | process.nextTick -> 46 | AsyncProfile.mark 'one' 47 | process.nextTick -> 48 | AsyncProfile.mark 'two' 49 | --------------------------------------------------------------------------------