UNPKG

10.5 kBJavaScriptView Raw
1/*
2 * Copyright (c) 2012 Dmitri Melikyan
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the
6 * "Software"), to deal in the Software without restriction, including
7 * without limitation the rights to use, copy, modify, merge, publish,
8 * distribute, sublicense, and/or sell copies of the Software, and to permit
9 * persons to whom the Software is furnished to do so, subject to the
10 * following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included
13 * in all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
18 * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
19 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
20 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
21 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 */
23
24
25if(global.nodetime) return global.nodetime;
26
27var fs = require('fs');
28var os = require('os');
29var util = require('util');
30var path = require('path');
31var events = require('events');
32var cluster = require('cluster');
33var crypto = require('crypto');
34var agentio = require('agent.io');
35var proxy = require('./proxy');
36var samples = require('./samples');
37var metrics = require('./metrics');
38var info = require('./info');
39var sender = require('./sender');
40var stdout = require('./stdout');
41var dtrace = require('./dtrace');
42var filter = require('./filter');
43var PredicateFilter = filter.PredicateFilter;
44var v8profiler = require('./v8-profiler');
45
46
47var Nodetime = function() {
48 this.initialized = false;
49 this.version = '0.4.0';
50 this.master = cluster.isMaster;
51 this.paused = true;
52 this.pauseAt = undefined;
53 this.nextId = Math.round(Math.random() * Math.pow(10, 6));
54 this.filterFunc = undefined;
55 this.times = {};
56 this.timekit = undefined;
57
58 events.EventEmitter.call(this);
59};
60
61util.inherits(Nodetime, events.EventEmitter);
62exports = module.exports = global.nodetime = new Nodetime();
63
64
65Nodetime.prototype.profile = function(opt) {
66 if(this.initialized) return;
67 this.initialized = true;
68
69 var self = this;
70
71 if(!opt) opt = {};
72
73 // registered accounts
74 this.accountKey = opt.accountKey;
75 this.appName = opt.appName || 'Default Application';
76 if(this.accountKey) {
77 this.sessionId = 'pro:' + this.accountKey + ':' + sha1(this.appName);
78 }
79
80 this.history = opt.history !== undefined ? opt.history : true;
81 this.headless = opt.headless;
82 this.dtrace = opt.dtrace;
83 this.stdout = opt.stdout;
84 if(this.stdout && opt.headless === undefined) this.headless = true;
85 this.debug = opt.debug;
86 this.server = opt.server || "https://nodetime.com";
87
88
89 // try to load timekit
90 try {
91 this.timekit = require('timekit');
92 if(!this.timekit.time() || !this.timekit.cputime()) throw new Error('timekit broken');
93 }
94 catch(err) {
95 this.timekit = undefined;
96 this.error(err);
97 }
98
99
100 // node >= 0.8
101 this.hasHrtime = process.hasOwnProperty('hrtime');
102
103
104 // prepare probes
105 var probes = {};
106 var files = fs.readdirSync(path.dirname(require.resolve('./nodetime')) + '/probes');
107 files.forEach(function(file) {
108 var m = file.match('^(.*)+\.js$');
109 if(m && m.length == 2) probes[m[1]] = true;
110 });
111
112 proxy.after(module.__proto__, 'require', function(obj, args, ret) {
113 if(ret.__required__) return;
114
115 var builtin = true;
116 if(!args[0].match(/^[^\/\\]+$/)) {
117 builtin = false;
118 }
119
120 if(!builtin) {
121 (fs.hasOwnProperty('exists') ? fs : path).exists(args[0] + '.probe', function(exists) {
122 if(exists) {
123 ret.__required__ = true;
124 require(args[0] + '.probe')(ret);
125 }
126 });
127 }
128 else if(probes[args[0]]) {
129 ret.__required__ = true;
130 require('./probes/' + args[0])(ret);
131 }
132 });
133
134
135 // setup agent and request sessionId if not given
136 if(!this.headless) {
137 this.agent = agentio.createClient({server: this.server, group: this.sessionId, debug: this.debug});
138
139 this.agent.on('message', function(msg) {
140 if(isValidCommand(msg)) {
141 if(msg.cmd === 'newSession') {
142 self.sessionId = msg.args;
143 self.agent.group = self.sessionId;
144 self.message("profiler console for this instance is at \033[33m" + self.server + "/" + self.sessionId + "\033[0m");
145
146 try {
147 self.emit('session', self.sessionId);
148
149 // session expires on server after 20 minutes
150 setTimeout(function() {
151 self.sessionId = undefined;
152 }, 1200000);
153 }
154 catch(err) {
155 self.error(err);
156 }
157 }
158 else if(msg.cmd === 'resume') {
159 self.resume();
160 }
161 else if(msg.cmd === 'filter') {
162 if(msg.args) {
163 var pf = new PredicateFilter();
164 if(pf.preparePredicates(msg.args)) {
165 self.filter(function(sample) {
166 return pf.filter(sample);
167 });
168 }
169 }
170 else {
171 self.filter(undefined);
172 }
173 }
174 else if(msg.cmd === 'profileCpu') {
175 try {
176 if(typeof msg.args === 'number' && msg.args > 0 && msg.args <= 60) {
177 v8profiler.startCpuProfiler(msg.args);
178 }
179 }
180 catch(err) {
181 self.error(err);
182 }
183 }
184 else if(msg.cmd === 'takeHeapSnapshot') {
185 try {
186 v8profiler.takeHeapSnapshot();
187 }
188 catch(err) {
189 self.error(err);
190 }
191 }
192
193 }
194 else {
195 self.log("invalid command from server");
196 }
197 });
198
199 if(!this.sessionId) {
200 this.log("requesting session from server");
201 this.agent.send({cmd: 'createSession'});
202 }
203 }
204
205
206 // broadcast sessionId to all workers in a cluster
207 if(!this.headless && !this.sessionId) {
208 if(this.master) {
209 proxy.after(cluster, 'fork', function(obj, args, worker) {
210 if(self.sessionId) {
211 worker.send({nodetimeSessionId: self.sessionId});
212 self.log('master ' + process.pid + ' sent sessionId ' + self.sessionId + ' to worker ' + worker.pid)
213 }
214 else {
215 self.once('session', function(sessionId) {
216 worker.send({nodetimeSessionId: sessionId});
217 self.log('master ' + process.pid + ' sent sessionId ' + sessionId + ' to worker ' + worker.pid)
218 });
219 }
220 });
221 }
222 else {
223 process.on('message', function(msg) {
224 if(!msg || !msg.nodetimeSessionId) return;
225
226 self.sessionId = msg.nodetimeSessionId;
227 self.log('worker ' + process.pid + ' received sessionId ' + msg.nodetimeSessionId + ' from master');
228 });
229 }
230 }
231
232 metrics.init();
233 proxy.init();
234 if(this.stdout) stdout.init();
235 if(this.dtrace) dtrace.init();
236 if(!this.headless) sender.init();
237 filter.init();
238 samples.init();
239 info.init();
240 v8profiler.init();
241
242 // expose tools for non-builtin modules
243 this.dev = {
244 proxy: proxy,
245 samples: samples,
246 info: info
247 };
248
249
250 // always activate profiler at startup and pause if not resumed for 3 minutes
251 this.resume(300);
252 setInterval(function() {
253 if(!self.paused && self.millis() > self.pauseAt)
254 self.pause();
255 }, 1000);
256};
257
258
259Nodetime.prototype.pause = function() {
260 if(!this.initialized) return;
261
262 this.paused = true;
263 this.pauseAt = undefined;
264
265 this.filterFunc = undefined;
266
267 this.message('profiler paused');
268};
269
270
271Nodetime.prototype.resume = function(seconds) {
272 if(!this.initialized) return;
273
274 if(!seconds) seconds = 180;
275
276 this.pauseAt = this.millis() + seconds * 1000;
277 this.paused = false;
278
279 this.message('profiler resumed for ' + seconds + ' seconds');
280};
281
282
283Nodetime.prototype.filter = function(func) {
284 this.filterFunc = func;
285};
286
287
288Nodetime.prototype.time = function(label, context) {
289 if(this.paused || !this.initialized) return;
290
291 this.times[label] = {
292 time: samples.time("Custom", label, true),
293 stackTrace: samples.stackTrace(),
294 context: context
295 };
296};
297
298
299Nodetime.prototype.timeEnd = function(label, context) {
300 if(this.paused || !this.initialized) return;
301
302 var time = this.times[label];
303 delete this.times[label];
304 if(!time) throw new Error('No such label: ' + label);
305
306 if(!time.time.done()) return;
307
308 var obj = {'Type': 'Custom'};
309
310 // merge start context
311 if(time.context) {
312 for(var prop in time.context) {
313 obj[prop] = time.context[prop];
314 }
315 }
316
317 // merge end context
318 if(context) {
319 for(var prop in context) {
320 obj[prop] = context[prop];
321 }
322 }
323
324 // add stack trace
325 obj['Stack trace'] = time.stackTrace;
326
327 samples.add(time.time, obj, 'Custom: ' + label);
328};
329
330
331Nodetime.prototype.metric = function(scope, name, value, unit, op, persist) {
332 if(!this.initialized || (!this.history && this.paused)) return;
333
334 metrics.add(scope, name, value, unit, op, persist);
335};
336
337
338Nodetime.prototype.hrtime = function() {
339 if(this.timekit) {
340 return this.timekit.time();
341 }
342 else if(this.hasHrtime) {
343 var ht = process.hrtime();
344 return ht[0] * 1000000 + Math.round(ht[1] / 1000);
345 }
346 else {
347 return new Date().getTime() * 1000;
348 }
349};
350
351
352Nodetime.prototype.micros = function() {
353 return this.timekit ? this.timekit.time() : new Date().getTime() * 1000;
354};
355
356
357Nodetime.prototype.millis = function() {
358 return this.timekit ? this.timekit.time() / 1000 : new Date().getTime();
359};
360
361
362Nodetime.prototype.cputime = function() {
363 return this.timekit ? this.timekit.cputime() : undefined;
364};
365
366
367Nodetime.prototype.log = function(msg) {
368 if(this.debug && msg) console.log('nodetime:', msg);
369};
370
371
372Nodetime.prototype.error = function(e) {
373 if(this.debug && e) console.error('nodetime error:', e, e.stack);
374};
375
376
377Nodetime.prototype.dump = function(obj) {
378 if(this.debug) console.log(util.inspect(obj, false, 10, true));
379};
380
381
382Nodetime.prototype.message = function(msg) {
383 util.log("\033[1;31mNodetime:\033[0m " + msg);
384};
385
386
387var isValidCommand = function(obj) {
388 if(!obj) return false;
389 if(typeof obj.cmd !== 'string' || obj.cmd.length > 256) return false;
390
391 return true;
392};
393
394
395var sha1 = function(str) {
396 var hash = crypto.createHash('sha1');
397 hash.update(str);
398 return hash.digest('hex');
399};