Fondue documentation

  1. What's fondue?
  2. How to trace some code
    1. Example: a script that inspects itself
    2. Example: a script that inspects another script
  3. How to access the data (Fondue's API)
    1. API Reference
  4. Getting fancy (tracing whole programs, whole web sites, etc)

You can follow along and play with Fondue in your browser using The Fondue playground.

What's fondue?

Fondue is a real-time program tracing library for JavaScript. You use it if you want to:

Get the code with git clone https://github.com/adobe-research/fondue.git or use it in your Node.js project with npm install fondue.

How to trace some code

Call require('fondue').instrument(src, options). You pass it the JavaScript code you want to trace as a string, and it returns the same code, but modified so that if you run it, it will store trace data in a global object called __tracer.

Example: a script that inspects itself

Fondue comes with a script bin/fondue that calls instrument(src, { path: ... }) for you automatically. Try saving the following code as example01.js:

// example01.js

var nodesHandle = __tracer.trackNodes();
__tracer.newNodes(nodesHandle).forEach(printNode);

function printNode(node) {
	if (node.type === 'function') {
		console.log('found a function: ' + node.name + ' at ' + node.path + ':' + node.start.line + ' with id ' + node.id);
	}
}

Then execute the following commands:

  1. ./fondue/bin/fondue example01.js > example01.fondue.js
  2. node example01.fondue.js

It should print out something like this:

found a function: [log] at [built-in]:0 with id log
found a function: printNode at /Users/tom/src/example01.js:6 with id /Users/tom/src/example01.js-function-6-0-10-1

(log is a special function representing console.log. You can ignore it in your scripts if you want.)

If you add more functions to example01.js and re-run the commands, you should see that it prints out all the functions you add.

Example: a script that inspects another script

You can find basically the same example in the Fondue playground. Just hit "Run". You might find it easier to experiment there than on the command-line.

How to access the data (Fondue's API)

__tracer uses a polling data model. As you saw in the example, to get all nodes, first you call trackNodes() to get a handle object, then you call newNodes(handle) to get all the nodes that have been instrumented since the last call to newNodes.

As another example, trackHits() gives you a handle for tracking the hit counts of all functions. hitCountDeltas(handle) gives you an object containing the counts of all function calls that occurred since the last call to hitCountDeltas(handle) for that handle:

var hitsHandle = __tracer.trackHits();
var hits1 = __tracer.hitCountDeltas(hitsHandle); // { 'func-id-1' : 1, 'func-id-2' : 3 }
var hits2 = __tracer.hitCountDeltas(hitsHandle); // {}
var hits3 = __tracer.hitCountDeltas(hitsHandle); // {}
var hits4 = __tracer.hitCountDeltas(hitsHandle); // { 'func-id-2' : 1 }
var hits5 = __tracer.hitCountDeltas(hitsHandle); // {}
// ...

API Reference

I apologize for inconsistencies. The only consumer of this API until recently was my own project, Theseus.

Fondue's API

require('fondue').instrument(src, options)
fondue.instrument(src, options)

Use the first version in Node.js (after npm install fondue) and the second version if you include fondue.browser.js on a web page, which creates a global fondue object.

These functions take JavaScript source code in the src argument and return a string containing an instrumented version of that source code.

options is an object (or null) with any the following keys:

Key Default value Description
path "<anonymous>" The path of the source code being instrumented. It doesn't load the code from that page, only embeds that information as meta-data, so it can be whatever you want.
include_prefix true Whether to include the source code underlying the __tracer object. It's okay for this code to be included multiple times, so always leaving it on is okay. It's just kind of big.
tracer_name __tracer The name to use for the global object containing the trace information.
nodejs false If src will be executed in Node.js (not the browser), then set this to true to enable some Node.js-specific functionality.
maxInvocationsPerTick 4096 When more than this many trace points (nodes) are executed in a single tick of the JavaScript VM, fondue will stop storing trace information until the next tick. This prevents intense calculation from filling all your RAM.
throw_parse_errors false Whether to throw parse errors as exceptions, or just print them with console.error and attempt to continue. By default, when there's a parse error, the code is just passed through without instrumentation.

__tracer API

Fondue-instrumented code creates a global __tracer object that you can also access to get information about that code's execution.

__tracer.version() Returns a string like "0.6.1" with the version of Fondue being used.
handle = __tracer.trackNodes()
nodes = __tracer.newNodes(handle)

Get all known trace points in the program (functions, function call sites, etc).

id Unique string identifier for this node. It currently consists of the path, start, and end locations in the file, separated by dashes, but please don't rely on that.
type The node type as a string. Currently one of "toplevel", "function", or "callsite".
name A pretty name for the node. For named functions, it's the name. For anonymous functions, fondue tries to make something appropriate like "(timer handler (200ms))".
path Path of the file the node is in, or "<anonymous>".
start Start location in the file, like {"line": 1, "column": 0}.
end End location in the file, like {"line": 7, "column": 1}.
params

An array of the function's arguments (present only if type is function).

name Name of the argument
start Start location in the file, like {"line": 1, "column": 0}.
end End location in the file, like {"line": 7, "column": 1}.
handle = __tracer.trackHits()
hits = __tracer.hitCountDeltas(handle)

Get a map from node id to the number of times the node was executed since the last call to hitCountDeltas with the given handle. Only the ids of functions that actually executed will be present. So, if no code has been executed, hitCountDeltas will return an empty object.

Example return value of hitCountDeltas: { 'func-id-1' : 1, 'func-id-2' : 3 }.

handle = __tracer.trackExceptions()
hits = __tracer.newExceptions(handle)

Like trackHits and hitCountDeltas, except it only includes counts for nodes that threw an exception. The return value is also slightly different.

Example return value of newExceptions: { counts: { 'func-id-1' : 1, 'func-id-2' : 3 }}.

handle = __tracer.trackLogs(query)
invocations = __tracer.logDelta(handle, maxResults)

invocations will be a chronological array of invocations that match the given query. For example, if your query matches one function, then the results will be a list of all the times that function was called, including the values of the arguments. If your query matches multiple functions, you'll get a list of the times that either function was called.

query is an object describing which invocations you want to be returned. You will get a union of all matched invocations. query accepts the following keys:

ids (optional) An array of node IDs that you want the invocations of. These are strings that you might get from node.id on a node you get from trackNodes.
logs (optional) A boolean, true if you want all invocations of the special [log] function that represents console.log. With this you can recreate the terminal output of a program.
eventNames (optional) An array of strings, each one the name of an event whose invocations to include. See trackEpochs for a description of what an event is.
exceptions (optional) A boolean, true if all trace points that threw an exception should be included.

maxResults is the number of invocations to return. It's optional, and if you don't provide it, a default number is used.

Each object in the invocations array has the following keys:

timestamp The result of calling (new Date).getTime() at the time the node was invoked.
tick A number that goes up by one every time a trace point is reached. Primarily for use internally to enforce maxInvocationsPerTick. This shouldn't have been called "tick" because that word usually refers to one run of the JavaScript event loop, and that's how it's used everywhere else in this document.
invocationId A unique string identifier for this invocation.
parents

An array of objects representing synchronous or asynchronous parents in the call graph. Only parent invocations that are also matched by the query will be included. This is so that you can render these nodes as a graph without edges that go nowhere. Each parent object has the following keys:

invocationId The invocationId of the parent invocation
type

A string, either "call" (synchronous call chain) or "async" (asynchronous call chain).

An example of an "async" parent would be if you had this code:

function foo() {
	setTimeout(function bar() { /* ... */ });
}

Then invocations of foo() would be "async" parents of invocations to bar(). foo() doesn't call bar() directly, but bar()'s closure gets created inside of foo(), so foo() is considered a parent.

topLevelInvocationId Identifier of the invocation at the top of the stack when this invocation occurred. For example, when setTimeout(function foo() { }, 100) fires, function foo will be at the top of the stack, and every function that's called synchronously by foo will have the ID of that invocation of foo as its topLevelInvocationId.
nodeId Identifier of the node that was invoked (e.g. node.id of a node you got from trackNodes).
arguments

This key is only present if the node's type was function. It's an array of objects representing each argument value that was passed to the function, in order. Each object has these keys:

name (optional) The name of the argument according to the function definition. If you define function foo(a) { } and call it like foo(1, 2), then the object in this array for the first argument will have "a" as the name for the first argument, and no name for the second argument.
value

An object describing the value that was passed into the function. This isn't the object itself, but a JSON-friendly description and summary. Each object looks like this:

type

A string, the result of typeof object with some exceptions:

  • typeof document.all (and possibly other objects) claims to be undefined, but Fondue calls it an object.
  • The type of null is "null".
value (optional) If it's a simple, JSONifiable value like a number, boolean, or string, then value will simply be that number or string.
preview (optional) If present, this is a short string summary of the object. For example, for an array it might be "[Array:6]".
ownProperties

(optional) For objects, this is an object whose keys are all the keys from the original object for which hasOwnProperty is true. The values are objects just like this one, with type, value, preview, ownProperties, and truncated keys.

There's currently no way to see an object's prototype.

Fondue will not let this structure recurse indefinitely. See truncated.

truncated

(optional) Fondue will not let ownProperties recurse indefinitely. Once it detects that its representation of this object has reached a certain size, it stores only truncated versions of each object it comes across. If this key is present, then this object didn't make the cut.

The value of truncated is an object with these keys:

length (optional) If present, this is an object like { amount: 4 }, containing how many items from the end of the Buffer were omitted from the representation.
keys (optional) If present, this is an object like { amount: 4 }, containing how many keys from the object were omitted from the representation. Missing array values are also counted here.
handle = __tracer.trackEpochs()
something = __tracer.epochDelta(handle)
Insert documentation here. For now, try it out in the Fondue playground. E-mail me to bug me about writing this section.
handle = __tracer.trackFileCallGraph()
something = __tracer.fileCallGraphDelta(handle)
Insert documentation here. For now, try it out in the Fondue playground. E-mail me to bug me about writing this section.

Getting fancy (tracing whole programs, whole web sites, etc)

Instrumented code runs in Visualization code runs in Strategy
Browser Browser Pre-processed. Pre-process the code with bin/fondue (as in the example above) or with require('fondue').instrument(src, options) in Node.js, then execute the output on a web page with a <script> tag. That creates a global __tracer on which you can call the above methods directly from the same page. The "script that inspects itself" example on this page works this way.
Node.js Node.js Pre-processed. Same as with the Browser/Browser case above. eval() code that has been run through the instrument() function and check the global __tracer object.
Browser Browser On-the-fly, browser-side. Process the code with fondue.instrument(src, options) on a web page itself after including fondue.browser.js, the browser build of Fondue. Execute the output with eval(src). That also creates a global __tracer on which you can call the above methods directly. The Fondue playground works this way.
Browser Browser On-the-fly, server-side. Drop fondue-middleware into a Node.js server. When it detects that you're serving a JavaScript file, or an HTML file with embedded <script> tags, it will automatically process the scripts with Fondue. When that code executes on the web page, that creates a global __tracer object on which you can call the above methods directly.
Node.js Browser Remote Node.js instrumentation. Run the Node.js app with node-theseus instead of node (first, npm install -g node-theseus), which starts a WebSocket server that exposes the __tracer object via a simple RPC API. Here's an example project that displays information about a running Node.js process with d3.