strong-trace
Version:
StrongTrace Node.js Tracer
465 lines (419 loc) • 12.1 kB
JavaScript
"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)
}