• Stars
    star
    378
  • Rank 113,272 (Top 3 %)
  • Language
    JavaScript
  • License
    GNU General Publi...
  • Created almost 11 years ago
  • Updated about 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A dependency-free Javascript Tracer

hxTracer


I wrote this javascript tracer to gain a better insight into how a project worked. Hopefully it'll help others too. Keep reading for an explanation on how it works.

How do I use it?


Simply require this module somewhere:

var hxTracer = require('hxtracer');

Or, include it on a webpage.

The tracer starts off disabled, when you get near a code path you want to trace:

hxTracer.start();

then when finished:

hxTracer.stop();

eg:

hxTracer.start();
cache.storeObject('test', { key: 'foobar' }, function(err) {
	hxTracer.stop();
});

Or, the tracer can be toggled on/off by sending the process a PIPE signal:

/repos/redis-benchmark:ps aux | grep node
username    14116 15.5  0.1 662156 21992 pts/0    Sl+  22:27   0:00 node test.js
/repos/redis-benchmark:kill -PIPE 14116

When your project runs into the tracer it will spit tracing info out to stderr like this:

TRACER OVERHEAD 0.13ms per timing
...
TRACER FNI 4398.08ms 6440456  /repos/redis-benchmark/Cache.js.prototype.findObject
TRACER FNI 4398.14ms 6440456    /repos/redis-benchmark/Cache.js.prototype.generateCacheKey
TRACER FNR 4398.19ms 6440456    /repos/redis-benchmark/Cache.js.prototype.generateCacheKey
TRACER FNI 4398.25ms 6440456    /repos/redis-benchmark/Cache.js.prototype.get
TRACER FNR 4398.29ms 6440456    /repos/redis-benchmark/Cache.js.prototype.get
TRACER FNR 4398.33ms 6440456  /repos/redis-benchmark/Cache.js.prototype.findObject
...
TRACER EV LAG 59ms
...
TRACER FNI 4403.47ms 6440456  /repos/redis-benchmark/Cache.js.prototype._processGetQueue
TRACER FNI 4404.02ms 6440456    /repos/redis-benchmark/Cache.js.prototype.mget
TRACER FNI 4404.12ms 6440456      /repos/redis-benchmark/RedisClient.js.prototype.mget
TRACER FNI 4404.28ms 6440456        /repos/redis-benchmark/RedisClient.js.prototype._isClientConnected
TRACER FNR 4404.35ms 6440456        /repos/redis-benchmark/RedisClient.js.prototype._isClientConnected
TRACER FNR 4404.60ms 6440456      /repos/redis-benchmark/RedisClient.js.prototype.mget
TRACER FNR 4404.64ms 6440456    /repos/redis-benchmark/Cache.js.prototype.mget
TRACER FNR 4404.70ms 6440456  /repos/redis-benchmark/Cache.js.prototype._processGetQueue
...
[Test finished in 203ms]
TRACER CBR 4775.91ms 6440456  /repos/redis-benchmark/node_modules/async/lib/async.js.parallel
TRACER CBR 4776.05ms 6442576    /repos/redis-benchmark/node_modules/async/lib/async.js.map
TRACER CBR 4776.16ms 6444720      /repos/redis-benchmark/node_modules/async/lib/async.js.each
TRACER CBR 4776.26ms 6446872        /repos/redis-benchmark/Cache.js.prototype.findObject
TRACER CBR 4776.36ms 6449016          /repos/redis-benchmark/Cache.js.prototype.get
TRACER EV LAG 71ms
TRACER TOTAL OVERHEAD 7.27ms
TRACER TOTAL 202.02ms 202.33ms 202.02ms 1 /repos/redis-benchmark/node_modules/async/lib/async.js.parallel
TRACER TOTAL 200.73ms 201.29ms 200.73ms 1 /repos/redis-benchmark/node_modules/async/lib/async.js.map
TRACER TOTAL 199.87ms 200.74ms 199.87ms 1 /repos/redis-benchmark/node_modules/async/lib/async.js.each
TRACER TOTAL 31.30ms 106.57ms 183.26ms 100 /repos/redis-benchmark/Cache.js.prototype.findObject
TRACER TOTAL 30.69ms 106.31ms 182.95ms 100 /repos/redis-benchmark/Cache.js.prototype.get
TRACER TOTAL 3.53ms 3.68ms 3.53ms 1 /repos/redis-benchmark/Cache.js.prototype.mget
TRACER TOTAL 3.20ms 3.51ms 3.20ms 1 /repos/redis-benchmark/RedisClient.js.prototype.mget
TRACER TOTAL 2.56ms 2.56ms 2.56ms 1 /repos/redis-benchmark/Cache.js.prototype._processGetQueue
TRACER TOTAL 0.04ms 0.37ms 27.89ms 100 /repos/redis-benchmark/Cache.js.prototype.generateCacheKey
TRACER TOTAL 0.20ms 0.20ms 0.20ms 1 /repos/redis-benchmark/RedisClient.js.prototype._isClientConnected

Where: FNI is Function Invoked, FNR is Function Returned, CBI is Callback Invoked, CBR is Callback Returned. The columns provided by TRACER TOTAL are: min duration, average duration, max duration, call count, file.

If you want the tracer to stop at extra places, you can:

hxTracer.log('Cache Callback');

Tracing a Node.js project


Setting the Scene

Here's some code I'd like to trace, annotated with a few points which are, or might be, interesting.

var async = require('async');
var library = require('lib/library');

// Point A
async.map(listOfCodes, function(code, callback) {
  // Point B
  library.findObjectsByCode(code, function(err, results) {
    // Point C
    if (err) return callback(null, [ ]);
    return callback(null, results); // Point D
  });
  return; // Point E
}, function(err, results) {
  // Point F
  results = results || [ ];
  console.log(Array.prototype.concat.apply([ ], results);
  return; // Point G
});
return; // Point H

When tracing this, I would like to know what code paths are being followed, the order in which they are being executed, how long it is taking to get between the various code points annotated above, and how much time in total is being spent in various functions.

Examining the 'module' module.

This is a trimmed down version of the structure of a module object:

> console.log(module);
{ exports: { ... },
  parent: { ..reference to the module that referenced this module.. },
  children: [ { ..reference to modules required by this one.. } ] }

The exports property contains a reference to the cached module.exports for the given module.

Apart from the starting module, whose parent will be undefined, the parent and children properties form circular references:

> [ some module ].parent.children.indexOf([ some module ]) >= 0
true
> console.log(JSON.stringify(module, null, 2))
TypeError: Converting circular structure to JSON

This is both awesome and terrible at the same time - it means that any included module in a given project can obtain the references to every loaded module in the entire project. It also means that any module can alter the exports or behaviour of any other module in the project:

var async = require('async');
module.parent.children[0].exports.foo = 'bar';
console.log(async.foo);

So how can we abuse this power to trace an entire project?

Scoping out a project

First we need to start by locating the core module that spawned the whole project. From anywhere, grab the 'global' (well, within scope of each file) module object and traverse to the top of the tree:

var parentModule = module;
while (parentModule.parent) parentModule = parentModule.parent;

Then we've just got to work our way back down the tree obtaining references to every modules exports. In this example, every module's exports get run through a bootstrap function:

function processModules(item) {
  if (item.exports && !item._traced) {
    bootstrap(item, 'exports', item.filename);
    item._traced = true;
  }
  if (item.children) {
    item.children.map(processModules);
  }
}
processModules(parentModule);

As it stands, this will bootstrap every function in every module - tracing our 3rd party modules at a high level can be great, digging deeper into them is just going to create a lot of noise and make it harder to focus on our own code. We can limit the coverage to something a bit more sensible:

if ( ((path.indexOf('node_modules') !== -1) && (path.split('node_modules')[1].split('/').length > 3)) ||
     (path.split('.').length > 5) ) return;

Infecting enough code for a basic trace

So first up is attacking the exported objects - the most common exports patterns that I'm interested in are these two:

function SomeClass();
module.exports = SomeClass;
SomeClass.prototype.foo = function() { .... };

and

module.exports = {
  foo = function() { .... }
};

So we've simply going to traverse every modules exports - if the value is a Function then we're going to go after both it, it's prototype and any static properties attached to it. We're then going to iterate over every property on the value, looking for other Function references or objects and repeat the process:

function bootstrap(item, path) {
  var original = item[prop];
  if (item[prop] instanceof Function) {
    // Infect the actual function
    infect(item, prop, path);
    // Infect the functions prototype
    if (item[prop].prototype) {
      bootstrap(item[prop], 'prototype', path+'.prototype');
    }
    // Clone the static properties on the original function
    for (var i in original) {
      item[prop][i] = original[i];
    }
  }
  if (item[prop] instanceof Object) {
    // Bootstrap any other properties
    for (var i in item[prop]) {
      bootstrap(item[prop], i, path+'.'+i);
    }
  }
  return item;
}

Now we want to alter every one of these functions so that we can work out what order events are occuring, and how long it is taking to get between different code points. All we've got to do is record the original function reference then replace it with one of our own which does some tracing and calls on to the original. This is the basic format I've gone with:

function infect(item, i, path) {
  var original = item[i];
  item[i] = function() {
    return (function() {
      // ... trace stuff
      // ... invoke the original function, passing the correct scope
      // ... trace some more
      // ... return the result
    }).apply(this, Array.prototype.slice.call(arguments));
  };

  // Make sure we don't lose any prototypes!
  item[i].prototype = original.prototype;
}

This is cool, we're now tracing a bunch of functions across our project, but it's not covering some of the more interesting parts like anonymous functions or functions that are passed around to other modules.

Tracing the anonymous functions

The trick here is not to think too hard about how to get these in scope. Most of the anonymous functions we're going to be interested in are passed into the functions we've already traced, so tweaking them is remarkably simple. This example is an extension of the previous one:

function infect(item, i, path) {
  var original = item[i];
  item[i] = function() {
    return (function() {
      var functionArgs = Array.prototype.slice.call(arguments);
      functionArgs = functionArgs.map(function(arg) {
        if (!(arg instanceof Function)) return arg;
        return function() {
          // ... trace stuff
          // ... invoke the original function, passing the correct scope
          // ... trace some more
          // ... return the result
        };
      });

      // ... trace stuff
      // ... invoke the original function, passing the correct scope
      // ... trace some more
      // ... return the result
    }).apply(this, Array.prototype.slice.call(arguments));
  };

  // Make sure we don't lose any prototypes!
  item[i].prototype = original.prototype;
}

The idea is simple - whenever a traced function is invoked, if it's passed a function then we change it for a new one which does our tracing and continues on to where it was meant to go. Wonderful.

Allowing Dependency Injection

Some modules like to stringify functions to see how many parameters have been named, then altering how those functions are invoked accordingly. If we want this to continue working with our tracer we need to go a bit further and tweak our new functions parameters to match those of the original:

var dependencies = original.toString().match(/^function .*?\((.*?)\)/);
if (dependencies) {
  var newFunc = item[i].toString();
  newFunc = '(function() { return '+newFunc.replace('function ()', 'function ('+dependencies[1]+')')+ '; })()';
  item[i] = eval(newFunc);
}

This part is less than pleasant, but it gets the job done.

Measuring Time

We are going to need sub-millisecond accuracy when measuring how long it takes to get from A to B. Node provides this:

> process.hrtime();
[ 99343, 41943674 ]

Those two numbers are seconds then nanoseconds and are described in the documentation as It is relative to an arbitrary time in the past. It is not related to the time of day and therefore not subject to clock drift. The primary use is for measuring performance between intervals.. We're going to combine these numbers, move the decimal place to milliseconds, round off the nanoseconds and reduce the number down to something easier to read: 1234.56ms

function meaningfulTime() {
  var parts = process.hrtime();
  return (((parts[0]*1000)+(parts[1]/1000000))%10000).toFixed(2) + 'ms';
}

More Repositories

1

jsonapi-server

A config driven NodeJS framework implementing json:api and GraphQL
JavaScript
488
star
2

culture

Coding culture for HX
92
star
3

jsonapi-client

Easily consume a json:api service in Javascript.
JavaScript
68
star
4

esq

Simple query builder for elasticsearch
JavaScript
56
star
5

jsonapi-store-relationaldb

A relational database handler for jsonapi-server
JavaScript
33
star
6

wsdl2.js

A NodeJS tool to consume a WSDL file and output a neat, manageable Javascript library.
JavaScript
25
star
7

node-librato-metrics

JavaScript
21
star
8

chrome-extension-starter

Basic types of chrome extension, examples and reading materials
JavaScript
16
star
9

jsonapi-store-mongodb

A mongodb database handler for jsonapi-server
JavaScript
15
star
10

ui-toolkit

DEPRECATED - CSS & React components
JavaScript
14
star
11

flex-less

Flexbox LESS Mixin
CSS
14
star
12

jsonapi-store-elasticsearch

An elasticsearch database handler for jsonapi-server
JavaScript
10
star
13

iinChecker

Issuer identification number checker which returns details about a credit/debit card
JavaScript
8
star
14

mocha-performance

Runtime performance analysis off the back of a mocha test suite.
JavaScript
7
star
15

node-zabbix-sender

Its a node - zabbix - sender
JavaScript
6
star
16

cluster-manager

package to manage apps that want to use nodejs cluster module.
JavaScript
6
star
17

node-cookie-derail

Decode, and optionally verify the signature of, unencrypted Ruby on Rails cookies.
JavaScript
6
star
18

react-data-attributes-mixin

Take data from props and convert it to HTML data-* attributes
JavaScript
6
star
19

barometer

Framework agnostic browser performance metrics
JavaScript
5
star
20

truman

Simple test fixtures for single page apps
JavaScript
4
star
21

featherLog

Control what you log - Feather, a lightweight logging facility for node.js
JavaScript
4
star
22

praetorian

Structured JSON validator
JavaScript
4
star
23

deployment-helpers

A collection of scripts that can be used as part of a deployment process.
JavaScript
4
star
24

make-up

All of the checks for all of the linters.
JavaScript
3
star
25

holidayextras.github.com

HX Tech Team Blog
HTML
3
star
26

WorkerJS

Making concurrent Javascript easier
JavaScript
3
star
27

redis_mon

A lightweight redis monitor script
JavaScript
2
star
28

hxMetrics

instrument NodeJS modules without touching their source code
JavaScript
2
star
29

react-time-select

React Bootstrap select box populated with a configurable and localizable time range
JavaScript
2
star
30

varnish-bits

Couple of scripts etc we use for varnish
Shell
2
star
31

node-pester

Will pester you, if you're running the wrong version of Node.js for your app
JavaScript
2
star
32

plugin-jsonapi

A hapijs plugin for a jsonapi style output to all requests.
JavaScript
2
star
33

static-site-generator

An open source plugin that brings together a content API together with metalsmith in order to generate static sites.
JavaScript
1
star
34

node-queuemetrics

Interface into Queuemetrics' API
JavaScript
1
star
35

partner-api-docs

Holiday Extras Partner API Docs.
JavaScript
1
star
36

recruitment-tasks

technical tasks for HX Engineer applications
1
star
37

snyk-report

Assists in generating human readable snyk reports for CI integration
JavaScript
1
star
38

react-component-template

A starter template for a React component. Clone this code and push up under a different repo to build your own components.
JavaScript
1
star
39

react-date-time-group

Datepicker + time selection component
JavaScript
1
star
40

partner-api-examples

Worked examples in different languages of how to integrate with Holiday Extras Partner API
1
star
41

bigjson

Python
1
star
42

example-js-project

An example codebase demonstrating our preferred project and build process setup
JavaScript
1
star
43

mocha-exports-to-describe

Converts mocha exports tests to use the describe syntax
JavaScript
1
star