Optimizing JavaScript Windows 8 application: Using the Visual Studio performance analysis tool (profiler)

Performance is always an important point in any project. And you must be well equipped to find potential issues in your code in order to provide the best experience for your user regardless of the device he uses.

In my case, I had an issue with UrzaGatherer on low-end hardware: it took ages to open and display the first screen which is mainly a listview presenting the list of expansions:

On my computer (which is what we can call a high-end computer Sourire), I had no issue because the raw power of my system compensates the performance issue.

This is the first lesson of this article: ALWAYS TEST YOU APPLICATION ON LOW-END HARDWARE! Windows 8 (and Windows RT) is able to run on so many hardware that you cannot afford to not test on low-end devices.

When I found this issue, I decided to use a great tool of Visual Studio 2012: the integrated performance analysis tool which is available on the Debug (or Analyze) menu:

 

Using the Visual Studio performance analysis tool

To use this tool, you just have to launch it by using [Start Performance Analysis] if you want to integrate the start phase of your application in your analysis or by using the [Start Performance Analysis Paused] if you want to analyze a specific part of your application.

In my case, I needed to analyze why it took ages to start so I used the first menu.

The following screen is then displayed and your application is launched:

At any moment you can stop or pause the analysis.

And when the analysis is over, you can access the profiling report:

 

Please note the following to understand this report:

  • Inclusive time defines the time spent inside the function including children functions
  • Exclusive time defines the time spent inside the function WITHOUT children functions

 

Finding the problem

Using this report, you should be able to find some bottlenecks.

One obvious way to use this report is to analyze the hot path which shows you the main consuming path code.

In my case, we can see that something related to the UI (processElement —> marginLeft) is displayed. I you click on a line, you can see the associated code:

For this example, there is no code because the marginLeft property is inside the rendering engine (IE 10) and not inside the WinJS library. But if you click on calling functions, you will be able to see calling code:

Analyzing this part of code, I can see that it is related to WinJS UI code so for now, I will leave it unchanged Sourire

Back to the main report screen, we can also see that JSON.parse if the function with the most individual work associated (13,47% of the total time was spent inside this function). This is normal as I load a 20Mb json file which contains more than 20 000 cards information. I want to have all my cards in memory to be able to search through them quickly so I decided to also leave it unchanged.

In my quest to reduce the loading time of my application I then went to the functions page:

I found that a specific function inside ui.js was called more than 379 000 times! By clicking on the line, I discovered this code:

I added a breakpoint on it to understand that the listview used these function to determine the occupancyMap which is used to populate and draw the listview.

But 379 000 calls is a bit too much for only 40 entries in my screen:

And then I realized my error!! In order to be able to display the random card AND the expansions tiles, I decided to use a really small cell size for the grid (The items must have a size proportional with the cell size so with a really small size I was able to draw any virtual size):

ui.setOptions(listView, {
    oniteminvoked: this.itemInvoked,
    itemTemplate: this.itemRenderer,
    groupHeaderTemplate: this.headerRenderer,
    layout: new ui.GridLayout({
        groupHeaderPosition: "top",
        groupInfo: function () {
            return {
                enableCellSpanning: true,
                cellWidth: 28,
                cellHeight: 13
            };
        }
    })
});

But by doing that, I’m forcing the listview to go through a too important count of cells to build its layout!!

 

Fixing the problem

Fixing the problem is obvious. I just need to use bigger cells (so there will be less cells to go through):

ui.setOptions(listView, {
    oniteminvoked: this.itemInvoked,
    itemTemplate: this.itemRenderer,
    groupHeaderTemplate: this.headerRenderer,
    layout: new ui.GridLayout({
        groupHeaderPosition: "top",
        groupInfo: function () {
            return {
                enableCellSpanning: true,
                cellWidth: 280,
                cellHeight: 130
            };
        }
    })
});

I also need to update my CSS to accommodate the new layout and voila!

The new performance report is now the following:

The UI code is now called only 6 867 times which is clearly more logical.

With this optimization, UrzaGatherer launches twice as fast!

Another small thing…

Another thing really simple to fix is related to Math.floor which is called more than 130 000 times. I used it with the code for cropping cards to generate live tiles:

var pixels = imageData.data;
for (var index = 0; index < pixels.length; index += 4) {
    var r = pixels[index];
    var g = pixels[index + 1];
    var b = pixels[index + 2];

    var luminance = Math.floor(r * 0.3 + g * 0.59 + b * 0.11);

    luminance += (255 - luminance) * amplification;

    if (luminance > 255)
        luminance = 255;

    pixels[index] = luminance;
    pixels[index + 1] = luminance;
    pixels[index + 2] = luminance;
}

To fix this issue, just use a bitwise operator with 0 (old developer trick ^^):

var pixels = imageData.data;
for (var index = 0; index < pixels.length; index += 4) {
    var r = pixels[index];
    var g = pixels[index + 1];
    var b = pixels[index + 2];

    var luminance = (r * 0.3 + g * 0.59 + b * 0.11) >> 0;

    luminance += (255 - luminance) * amplification;

    if (luminance > 255)
        luminance = 255;

    pixels[index] = luminance;
    pixels[index + 1] = luminance;
    pixels[index + 2] = luminance;
}

And the result:

Far better no?

Obviously many other things can be optimized but I just wanted to write an article and not a book Clignement d'œil