JavaScript Profiler

posted on August 6, 2012

Every web developer who developed web applications or sites for mobile devices knows that there is no easy way to profile JavaScript execution times. Some developer tools are available for mobile debugging, such as iWebInspector and weinre (WEb INspector REmote). However, none of them can profile JavaScript execution times on remote devices as it can be done in Developer Tools in Chrome and Safari or Firebug in Firefox.

The following example shows the profile results in Chrome Developer Tools and Firebug of some simple code.

function MyClass() {
    this.foo();
}

MyClass.prototype.foo = function foo() {
    var i;
    for (i = 0; i < 100; i++) {
        document.createElement("div");
    }
    this.bar();
};

MyClass.prototype.bar = function bar() {
    var i;
    for (i = 0; i < 100; i++) {
        document.createElement("div");
    }
};

var obj = {
    funcA: function funcA() {
        var i;
        for (i = 0; i < 10; i++) {
            this.funcB();
        }
    },
    funcB: function funcB() {
        var i, c;
        for (i = 0; i < 10; i++) {
            c = new MyClass();
            c.bar();
        }
    }
};

function doSomething() {
    var i;
    for (i = 0; i < 10; i++) {
        obj.funcA();
    }
    return false;
}

I have invoked the doSomething() function while recording profile in Chrome Developer Tools and Firebug and got the following results:

Profiling JavaScript execution time in Chrome
Profiling JavaScript execution time in Chrome gives an in-depth overview of function call hierarchy with an average execution time of code within each function (the self “column”) and an average execution time of all functions called from within that function (the total “column”).
Profiling JavaScript execution time in Firefox
Profiling JavaScript execution time in Firebug gives an overview of executed functions. For each function the overview shows number of calls, own and total times, average time and other data. (View large version)

One thing to note regarding the Chrome profiling tool is that it doesn't show the total count of each executed function, as shown in Firebug. In addition, the times shown in this tool aren't accurate. The "self" time of the "MyClass" constructor is too high relative to the time of functions "foo" and "bar" invoked from within that constructor. At the same time, there is no code executed inside this constructor except the call for the "foo" function.

Unlike the Chrome profiling tool, Firebug doesn't show function call hierarchy. On the other hand, Firebug does show other valuable data.

In any case, none of the above tools can help us when we want to profile JavaScript on mobile devices. We have no Chrome Developer Tools or Firebug on mobile devices. Weinre, a tool I've mentioned before, at first glance, seems to allow profiling JavaScript on mobile devices remotely, just like Webkit's Web Inspector. But it doesn't have the "Profiles" tab in its panel, so it is no go. And iWebInspector allows profiling JavaScript executed from iOS simulator only.

So how are we going to fix that?

The idea is to create a JavaScript library that allows simple profiling of functions and class methods by collecting their execution times. Then, parsing collected data and displaying it in a remote console or even in a nice UI dialog. One downside of this tool is that all functions and classes intended for profiling must be registered with the profiler before profiling them.

Step 1: Registering functions and classes to be profiled

As I have already noted, all functions and classes intended for profiling must be registered with the profiler before starting the profiler. Before registering any functions we need to instantiate profiler object: var profiler = new JsProfiler();. Then we can start registering functions, classes, and objects on that profiler instance.

Registering functions:

To register single functions, invoke the following method of the profiler instance:

registerFunction(contextObject, functionName, contextObjectLabel);

The contextObject parameter is the object on which the registered function is defined. The functionName parameter is the string indicating the name of the registered function. In other words, the reference of the registered function can be obtained by using contextObject[functionName]. This string will also be used to identify the function in profiler results. If your function is defined in the global scope, pass the window as the contextObject. This makes sense because the reference to all global functions can be obtained using window[functionName]. The third parameter is the contetObjectLabel, a string used to prefix function name in profiler results.

Registering objects:

To register all functions defined on an object, including any getters and setters, invoke the following method of the profiler instance:

registerObject(object, objectLabel)

The object parameter is the object whose functions you want to register. And the objectLabel parameter is the string that will be used to prefix functions of this object in profiler results.

Registering classes:

To register classes, including their constructor function, methods defined on the prototype (instance), and the class (static), as well as any getters and setters, invoke the following method of the profiler instance:

registerClass(contextObject, className)

And just like registering functions, the first parameter is the object on which the registered class is defined. And the second parameter is the string indicating the name of the class. This string will be used to prefix class static functions and class instance methods in profiler results. Note that only functions from the first prototype in the prototype chain of the class will be registered.

All together

Continuing the first example, we can now use the following code to register the global scoped function doSomething(), the functions declared on object obj, and the class MyClass() including its constructor and instance methods:

var profiler = new JsProfiler();
profiler.registerFunction(window, "doSomething", "window");
profiler.registerClass(window, "MyClass");
profiler.registerObject(obj, "obj");

Step 2: Profiling javascript execution time

After registering all functions that we want to profile, we need to start the profiler to begin profiling the code and collecting the profile data. Invoking registered functions won't collect any profile data without starting the profiler. To start the profiler, we need to invoke its start method:

profiler.start();

Now we can start invoking registered functions and collect their execution times. Every time one of the registered functions is invoked, the profiler will record its execution time.

Let's execute the doSomething() function, which will call all other registered functions:

doSomething();

After our code has finished executing and collecting the profile data, we can stop the profiler. You guessed right! Stopping profiler instance is even simpler than starting it:

profiler.stop();

Step 3: Printing results

Once we have collected the profile data, we can decide how to present it. But first, we need to get the raw collected data by running:

var results = profiler.getResults();

The return value of the getResults method is an object representing the collected profile data. This object has a hierarchical tree structure matching the hierarchical invocation of registered functions. Every object in the tree represents an invoked function and contains the collected profile data of that function. The objects in the first level represent the functions invoked first in each call stack. And the objects nested inside the children property represent functions invoked by "parent" functions.

For example, assuming we have three registered functions defined in the global scope: rootFunction, innerFunction1, and innerFunction2, where rootFunction calls the two other functions. We would get the following result:

{
    "window.rootFunction()": {
        "count": 1,
        "total": 250,
        "totalAverage": 250,
        "totalChildrenTime": 200,
        "self": 50,
        "selfAverage": 50,
        "children": {
            "window.innerFunction1()": {
                "count": 1,
                "total": 100,
                "totalAverage": 100,
                "totalChildrenTime": 0,
                "self": 100,
                "selfAverage": 100,
                "children": {}
            },
            "window.innerFunction2()": {
                "count": 1,
                "total": 100,
                "totalAverage": 100,
                "totalChildrenTime": 0,
                "self": 100,
                "selfAverage": 100,
                "children": {}
            }
        }
    }
}

I have created two different output methods to display this data nicely. Of course, you can create other output formats and destinations. Now, after having the results object, we can pass it to one of the output methods:

JsProfiler.HtmlTableGenerator.showTable(results);

This method will show a dialog with an HTML table that lets us browse through call-stack hierarchy similar to Chrome Profiler. However, unlike Chrome, this table has more information, similar to FireBug:

JavaScript Profiler HTML Result Table
JavaScript Profiler HTML Result Table

Here is a demo of using the profiler on the code from the original example and the result table from JsFiddle:

The second method outputs the data to the browser’s console using the console.log method:

JsProfiler.ConsoleTableGenerator.printResults(results)
JavaScript Profiler Console Table
JavaScript Profiler Console Table

You can also use the raw data and send it elsewhere for later examination.

The JsProfiler is available at GitHub, just include the jsProfiler.js file in your code and start profiling your code to make the web better!