UNPKG

strong-trace

Version:

StrongTrace Node.js Tracer

465 lines (419 loc) 12.1 kB
"use strict"; module.exports = Tracer module.exports.getTracer = function getTracer() { return global.__concurix_tracer } var fs = require("fs") var seed = (Math.random() - 0.5) * Math.pow(2, 32) var idgen = require("./lib/rolling_int_generator")(seed) var TransactionLog = require("./transactionlog") var TraceAggregator = require("./traceaggregator") function Tracer(config) { this.stopped = false this.config = config this.files = {} this.functions = {} this.functionCount = 0 this.stack = [] this.enterStack = [] this.depth = 0 this.lrtime = config.lrtime || process.hrtime this.addFile("INTERNAL") this.linkResumeId = this.addFunction("INTERNAL", "linkResume") this.workStartId = this.addFunction("INTERNAL", "workStart") this.transactionlog = new TransactionLog() this.aggregator = new TraceAggregator(this) global.__concurix_tracer = this } Tracer.prototype.stop = function stop() { this.stopped = true } Tracer.prototype.resume = function resume() { this.stopped = false } Tracer.prototype.reap = function reap() { return this.aggregator.reap() } Tracer.prototype.getId = idgen.next.bind(idgen) var versionCache = {} // TODO windows friendly?? var folderRe = /(.*\/node_modules\/[^/]+\/).+$/ Tracer.prototype.addCoreFile = function addCoreFile(name) { if (this.files[name]) { return } this.files[name] = { name: name, version: process.versions.node } } Tracer.prototype.addFile = function addFile(filename) { if (this.files[filename]) { return } var pkg var match = folderRe.exec(filename) if (match) { pkg = versionCache[match[1]] if (pkg == null) { try { var packageJson = match[1] + "package.json" pkg = JSON.parse(fs.readFileSync(packageJson).toString()) versionCache[match[1]] = {name: pkg.name, version: pkg.version} } catch (e) { // I guess there wasn't one. *shrug* } } } this.files[filename] = { filename: filename } if (pkg !== undefined) { this.files[filename].name = pkg.name this.files[filename].version = pkg.version } } Tracer.prototype.addFunction = function addFunction(filename, name, start, complexity) { this.functionCount++ var id = this.functionCount.toString(36) this.functions[id] = { id: id, module: this.files[filename], name: name } if (start) { this.functions[id].start = start } if (complexity) { this.functions[id].complexity = complexity } return id } function LogEntry(type) { this.type = type } LogEntry.prototype.type = "" LogEntry.prototype.id = "" LogEntry.prototype.name = "" LogEntry.prototype.ts = [] LogEntry.prototype.fnId = "" LogEntry.prototype.isTx = false Tracer.prototype._schedule = function _schedule(id, name, isTx) { // link to function we're in on the stack if (this.stopped || !this.config.trace || !this.config.follow_links) { return } if (this.stack.length > this.config.max_stack_length) { // This is an ugly solution but helps prevent tracing from coming // off the rails on a very complex synchronous work block return false } var fnId = this.enterStack[this.enterStack.length - 1] if (fnId == null) { // TODO if we schedule/resume prior to an `enter` we have no function id // Synthetically bind it to "uninstrumented js" -- which may be problematic fnId = "1" } var entry = new LogEntry("schedule") entry.id = id.toString(32) entry.name = name entry.ts = this.lrtime() entry.fnId = fnId if (isTx) { entry.isTx = true } this.stack.push(entry) } Tracer.prototype._resume = function _resume(id, name, isTx) { // link to the stack (not in fn yet?) if (this.stopped || !this.config.trace || !this.config.follow_links) { return } var fnId = this.enterStack[this.enterStack.length - 1] var entry = new LogEntry("resume") entry.id = id.toString(32) entry.name = name entry.ts = this.lrtime() entry.fnId = fnId if (isTx) { entry.isTx = true } this.stack.push(entry) } Tracer.prototype.tag = function tag(tagName, extra) { // Give this block an arbitrary tag -- informational only if (this.stopped || !this.config.trace) { return } if (extra != null) { tagName += " " + extra } var entry = new LogEntry("tag") entry.name = tagName this.stack.push(entry) } Tracer.prototype._getFile = function _getFile(id) { if (id == null) { return } var info = this.functions[id] if (info == null) { return } return info.module.name || info.module.filename } Tracer.prototype.enter = function enter(id) { if (this.stopped || !this.config.trace) { return false } if (this.stack.length > this.config.max_stack_length) { // This is an ugly solution but helps prevent tracing from coming // off the rails on a very complex synchronous work block return false } if (this.depth >= this.config.max_scope_depth) { // Another ugly solution to prevent things from becoming molasses return false } var info = this.functions[id] var file = info.module.name || info.module.filename if (this.config.enable_cca) { var parent = this.enterStack[this.enterStack.length - 1] if (file === this._getFile(parent)) { // continuous contiguous aggregation (cca) return false } else if (this.depth && file === "process" && info.name === "nextTick") { // ignore pops to nextTick to help CCA. return false } } var traceme = true // TODO check tracemask // entering a function this.enterStack.push(id) var entry = new LogEntry("enter") entry.id = id entry.ts = this.lrtime() this.stack.push(entry) this.depth++ return traceme } Tracer.prototype.exit = function exit(id) { if (!this.config.trace) { return } var exitedAt = this.lrtime() // exiting a function. we expect *exactly* one per enter. // However, `throw`s can cause us to miss exits, so look for that here. var expectedId = this.enterStack[this.enterStack.length - 1] if (expectedId !== id) { var idx = this.enterStack.lastIndexOf(id) var unfinished = this.enterStack.splice(idx) // Skip the current exit because it is handled in the default case below // go back through and close all unclosed enters for (var i = unfinished.length - 1; i > 0; i--) { var entry = new LogEntry("exit") entry.id = unfinished[i] entry.ts = exitedAt this.stack.push(entry) this.depth-- } } else { this.enterStack.pop() } var entry = new LogEntry("exit") entry.id = id entry.ts = exitedAt this.stack.push(entry) this.prevFile = null this.depth-- if (this.depth <= 0) { this.aggregator.saveStack(this.stack) this.stack = [] this.enterStack = [] } } // A linker is a one-shot hop across the event loop // A forced link Tracer.prototype.link = function link(name, callback) { var id = idgen.next() this._schedule(id, name) return this.wrapResume(id, callback, name) } // This one does not instrument the wrapper because the wrapped function should // already be instrumented. Tracer.prototype.wrapLinker = function wrapLinker(fn, nameFn) { var self = this // linkWrapper is the proxy for the replaced function e.g. setTimeout var wrapper = function linkWrapper() { var linkName = "" if (typeof nameFn === "function") { linkName = nameFn.apply(null, arguments) } else if (typeof nameFn === "string") { linkName = nameFn } var id = idgen.next() self._schedule(id, linkName) var args = new Array(arguments.length) for (var i = 0; i < arguments.length; i++) { if (typeof args[i] === "function") { // will return a wrapper for the callbacks to link them to here args[i] = self.link(linkName, args[i]) } else { args[i] = arguments[i] } } return fn.apply(this, args) } return wrapper } // This one instruments the wrapper because the wrapped function won't be // instrumented because it is a core library. Tracer.prototype.wrapCoreLinker = function wrapCoreLinker(lib, name, fn, nameFn) { // TODO Does this need to somehow maintain function.length? // it is modifying function signature... // maybe have versions for 0->n function length? this.addCoreFile(lib) var fnId = this.addFunction(lib, name) // TODO avoid double wrap? var self = this // linkWrapper is the proxy for the replaced function e.g. setTimeout var wrapper = function linkWrapper() { var traceme = self.enter(fnId) var linkName = lib + "." + name if (typeof nameFn === "function") { linkName = nameFn.apply(null, arguments) } else if (typeof nameFn === "string") { linkName = nameFn } var args = new Array(arguments.length) for (var i = 0; i < arguments.length; i++) { if (typeof args[i] === "function") { // will return a wrapper for the callbacks to link them to here args[i] = self.link(linkName, args[i]) } else { args[i] = arguments[i] } } var rvalue try { rvalue = fn.apply(this, args) } catch (e) { if (traceme) { self.exit(fnId) } throw (e) } if (traceme) { self.exit(fnId) } return rvalue } return wrapper } Tracer.prototype.wrapCoreLinkerFirstArgString = function wrapCoreLinkerFirstArgString(lib, name, fn) { // could get a slight perf boost by doing the real work of the wrapper naming // here in a permanent way vs checking args, but for now, this. var linkName = function (str) { return lib + "." + name + " " + str } return this.wrapCoreLinker(lib, name, fn, linkName) } // TBD accept nameFn to tag block with fn/info? Tracer.prototype.wrapCoreSync = function wrapCoreSync(lib, name, fn) { this.addCoreFile(lib) var fnId = this.addFunction(lib, name) var self = this var wrapper = function wrapper() { var traceme = self.enter(fnId) var rvalue try { rvalue = fn.apply(this, arguments) } catch (e) { if (traceme) { self.exit(fnId) } throw (e) } if (traceme) { self.exit(fnId) } return rvalue } return wrapper } // TODO unwrap? // This is a one-time wrapper for what is usually a callback Tracer.prototype.wrapResume = function wrapResume(id, fn, name, txId) { var self = this return function res() { var traceme = self.enter(self.linkResumeId) if (txId !== undefined) { self.endTransaction(txId) } else { self._resume(id, name) } var rvalue try { rvalue = fn.apply(this, arguments) } catch (e) { if (traceme) { self.exit(self.linkResumeId) } throw (e) } if (traceme) { self.exit(self.linkResumeId) } return rvalue } } Tracer.prototype.wrapWorkStart = function wrapWorkStart(cb, workName) { var self = this return function wrapped() { var traceme = self.enter(self.linkResumeId) self.tag(workName) var rvalue try { rvalue = cb.apply(this, arguments) } catch (e) { if (traceme) { self.exit(self.linkResumeId) } throw (e) } if (traceme) { self.exit(self.linkResumeId) } return rvalue } } // There is currently no cache for transactions. This means that it can't // currently look for orphaned transactions. // TODO keep all named links as transactions? Tracer.prototype.transactionLink = function transactionLink(name, callback) { var txId = this.startTransaction(name) return this.wrapResume(txId[0], callback, name, txId) } Tracer.prototype.startTransaction = function startTransaction(transaction) { this.tag(transaction) var txId = [idgen.next(), Date.now(), transaction] this._schedule(txId[0], transaction, true) return txId } Tracer.prototype.endTransaction = function endTransaction(txId, extra) { if (txId == null) { return } if (this.config.transactions) { this.transactionlog.update(txId[2], Date.now() - txId[1]) } this._resume(txId[0], txId[2], true) this.tag(txId[2], extra) }