Writing an asynchronous traceing tool

You

  • Published npm module
  • Read nodecore code
  • Read v8 code

Who Am I

Andreas Madsen

Once Node.js core contributer

#9 – 5353 line changes
Cluster, Child Process, Domain, Stream v2

Besides that

Study: DTU - Mathematics and Technology
Work: Admazely ApS

The problem

Code
function failure() {
  throw new Error('simple error');
}

setTimeout(failure, Math.random() * 1000);

setTimeout(failure, Math.random() * 1000);
Result
/example/code.js:2
  throw new Error('simple error');
        ^
Error: simple error
    at failure [as _onTimeout] (/example/code.js:2:9)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
    at process._makeCallback (node.js:306:20)
In practice
events.js:71
        throw arguments[1]; // Unhandled 'error' event
                       ^
Error: connect ECONNREFUSED
    at errnoException (net.js:770:11)
    at Object.afterConnect [as oncomplete] (net.js:761:19)
If we could just get this
events.js:71
        throw arguments[1]; // Unhandled 'error' event
                       ^
Error: connect ECONNREFUSED
    at errnoException (net.js:770:11)
    at Object.afterConnect [as oncomplete] (net.js:761:19)
    at new ClientRequest (http.js:1216:16)
    at Object.exports.request (http.js:1607:10)
    at Worker.exec (/example/file.js:121:37) // Important

Minimal solution

function failure() {
  throw new Error('simple error');
}

function setTimeout(callback, time /*, and more */) {
  // Create stack trace and remove first line
  var trackingStack = (new Error()).stack.slice('Error'.length);
  global.setTimeout(function () {
    try { callback(); } catch (err) {
      // Overwrite err.stack by adding trackingStack
      err.stack += trackingStack;
      throw err;
    }
  }, time);
}

setTimeout(failure, Math.random() * 1000);

setTimeout(failure, Math.random() * 1000);
/example/code.js:12
      throw err;
            ^
Error: simple error
    at failure (/example/code.js:2:9)
    at null._onTimeout (/example/code.js:9:11)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
    at process._makeCallback (node.js:306:20)
    at setTimeout (/example/code.js:7:24)
    at Object.<anonymous> (/example/code.js:19:1) // Important
/example/code.js:12
      throw err;
            ^
Error: simple error
    at failure /example/code.js:2:9)
    at null._onTimeout (/example/code.js:9:11)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
    at process._makeCallback (node.js:306:20)
    at setTimeout (/example/code.js:7:24)
    at Object.<anonymous> (/example/code.js:17:1) // Important

Existing solutions

  • npm install trycatch
  • npm install long-stack-traces
  • npm install longjohn
  • and more ...

Missing generic overwrite


Try catch
var original = process.nextTick;
process.nextTick = function () {
  try {
    original.apply(this, arguments); // Call must throw
  } catch (err) {
    throw internal.wrapLogic(err); // Changed throw origin
  }
};
Called wrapper
fs.readFile('./file.js', function (err) {
  if (err) throw external.wrapLogic(err); // Require origin knowledge
});

Minimal wrapping


wrap(process, 'nextTick');
wrap(global, 'setTimeout');
wrap(global, 'setInterval');
Object.keys(fs).forEach(function(methodName) {
  wrap(fs, methodName);
});
// what about: net, tls, http, https, dgram, dns ...

Need to solve

  • V8 stack trace hook
  • Complete wrap logic
  • On demand wrap logic

v8 stack trace API

code.google.com/p/v8/wiki/JavaScriptStackTraceApi

  • Error.stackTraceLimit
  • Error.prepareStackTrace
Error.prepareStackTrace = function (error, structureStack) { var stack = structureStack.map(function (callSite) { return callSite.getFileName() + ':' + callSite.getLineNumber(); }); return stack.join('\n'); }; print(new Error('Example').stack);


v8: /src/messages.js

  • captureStackTrace
  • prepareStackTrace
  • FormatStackTrace

Monkey patch everything

First thought: Manually written
list of modules and their
methods

[
  "timer": ["setTimeout", "setInterval"],
  "fs": ["readFile", "writeFile"],
  "dns": ["lookup", "resolve"]
]

hard to maintain

JSON version of
the node core documentation
contains all the information

nodejs.org/api/all.json

Lazy monkey patching

To require all modules is expensive and noisy.

require is a function and can't be patched Module.prototype.require can!

/node/src/node.js and /node/lib/module.js

var Module = require('module');

var original = Module.prototype.require;

Module.prototype.require = function (path) {
  var module = original.call(this, path);

  if (isCoreModule(path)) {
    patchAll(module, path);
  }

  return module;
};

Putting it all together

var hook = require('async-hook'); // monkey patch everything
var chain = require('stack-chain'); // Error.prepearStackTrace wrapper

var previousStack = null;

hook.all.attach(function(name, callback) {
  // Async function executed
  var stack = chain.getCallSiteArray();
  stack.push.apply(stack, previousStack);

  return function () {
    // this is executed in another tick
    previousStack = stack;
    return callback.apply(this, arguments);
  };
});

chain.extend.attach(function (error, frames) {
  frames.push.apply(frames, previousStack);
  return frames;
});

chain.filter.attach(function (error, frames) {
  return frames.filter(function (callSite) {
    return (callSite.getFileName() === module.filename);
  });
});

Use this knowledge for good and only good purposes

with(this) void eval('--> I welcome you to \r throw "any question at me"')

{
  "slides" : "andreasmadsen.github.com/talk-tracing-tool",
  "twitter": "@andreas_madsen",
  "github" : "/AndreasMadsen",
  "module" : "trace"
}