Saturday, July 13, 2013

Exporting Google Chrome's page load metrics

Google Chrome offers a rich functionality as it comes to a developer productivity. Pressing F12 opens the developer tools window with eight panels of tools, allowing you to inspect DOM elements and styles, various resources (DB, cookies, appcache, local storage, etc) and network statistics; debug JavaScript code; running commands in the shell console and many more.














Recently, I've been experimenting with several network configurations, and wanted to log page load in order to do a benchmark later on. At the very least, I wanted to log each page load - how much time in total it took to load the page. The Network panel in developer tools provides that information for a view only (can be found in the very bottom of the page).













But you can't tell Chrome to automatically log that information for each page load. There's an option to extract network metrics in a HTTP Archive (HAR) format, but again, you can't automate that process without extending the DevTools.

I browsed for extension that will meet my demands, and found that Page load time extension has the closest abilities to what I need. Except the fact it doesn't have persistence.
What are my choices?? Since I haven't wrote extensions before, it will be fascinating to create one and to get familiar with Chrome's APIs and tools.

So, as I previously wrote, at the very least I wanted to get total page load time for each opened tab/window. To accomplish that, there're two possibilities: getHAR method - which programmatically allows you to retrieve everything you see in the Network panel of development tools; or Navigation Timing API. While HTTP Archive (HAR) object divides each page load into a small pieces of network requests, I needed something simpler and more general. To be honest, even the Navigation Timing API provides a much richer set of information than I needed. But I will use it for my purpose, as it is the simplest way to obtain those metrics.

Let's look at what we have in this Timing object. It outputs many useful network info, like, redirect time, DNS lookup time, TCP handshake initialization time, request and response time, as well as other metrics. The timings are measured in milliseconds since January 1, 1970 in UTC in integer format.























At this stage, I knew exactly what I want to output. Each log record will contain a space-separated string with URL and each of the attributes from Navigation.Timing object. Now, let's look what required to create an extension in Chrome.

The extension in Chrome is just a bunch of html/js files and a mandatory JSON-formatted manifest file. The manifest file declares which permissions the extension requires, which files are being used, the extension metadata like name, description and author, and more. Mine manifest file eventually looked like this:

{
  "manifest_version": 2,
  "minimum_chrome_version": "22",

  "name": "Performance Exporter",
  "description": "This extension exports performance information.",
  "version": "0.2",

  "background": {
    "scripts": ["background.js"]
    },

  "content_scripts": [{
      "matches": ["*://*/*"],
      "js": ["timer.js"]
  }],
  
  "icons": { "16": "icon16.png",
             "48": "icon48.png",
             "128": "icon128.png"
       },

  "permissions": [
    "tabs",
    "http://*/*",
    "https://*/*",
    "unlimitedStorage"
  ]
}

It includes the name, description and version of my extension; the icons that are being used by the browser and Chrome WebStore. Rest of the attributes discussed later.
The main extension code resides within two JavaScript files: background.js and timer.js under background and content_scripts attributes respectively.

Background page (background.js in my implementation) starts to run on extension initialization, exists for the lifetime of the extension, and only one instance of it at a time is active. It is generally used for sharing a common state or performing a common task, and both of them required by my extension as well.

window.webkitRequestFileSystem(
  PERSISTENT,         // persistent vs. temporary storage
  5 * 1024 * 1024,    // size (bytes) of needed space
  initFs,             // success callback
  errorHandler        // opt. error callback, denial of access
);

The extension initialization is done by creating file system using window.webkitRequestFileSystem function call. The default quota for File System API's persistent storage is 0, and therefore the extension explicitly asks for unlimitedStorage in the manifest file. webkitRequestFileSystem parameters are PERSISTENT for persistent storage, storage size amount of 5 megabytes, success and failure callbacks. On failure, I just print the error to the console. Success callback is slightly more interesting: I'm creating and saving the FileWriter object in a logFileWriter global variable, which will be used later for printing the logs.

chrome.runtime.onMessage.addListener(
  function(t, sender, sendResponse) {

   // query is async, according to:
   chrome.tabs.query({active: true, lastFocusedWindow: true}, function(tabs) {
   var tab = tabs[0];
 var tabUrl = tab.url;

        if (t.loadEventEnd > 0) {
           var record = String("\n");
           record += tabUrl + " ";
           // .. concatenate all the events timings
           record += t.loadEventEnd;

        var blob = new Blob([record], {type: 'text/plain'});
        logFileWriter.seek(logFileWriter.length);
        logFileWriter.write(blob);
        }
   });
});

The 'common task' part of my background page is a listener for message passing mechanism. It's using Chrome's Runtime API that is available since version 22 (this is the reason why I added the minimum_chrome_version attribute in the extension manifest). The message listener is being added using the chrome.runtime.onMessage.addListener function call. This listener function will receive performance.timing objects, and will eventually print them into the log file.

As I previously mentioned, there's one more JavaScript file named timer.js. It is classified as content script, and therefore will run in the context of each web page. Here comes to help the matches attribute in the extension manifest, which doesn't restrict the script run to a particular domain.

(function() {
    (function waitForCompletion() {
        if(document.readyState == "complete")
            chrome.runtime.sendMessage(performance.timing);
        else
            setTimeout(waitForCompletion, 300);
    })();
})();

On each page load, the extension samples the page for document load completion each 300 milliseconds, and once it completes, uses the message passing mechanism to send a message with a performance.timing object to the background process.

The drawback of using the FileSystem APIs is that you can't specify a path to a log file. On my Windows 7 machine, it is located under %localappdata%\Google\Chrome\User Data\Default\File System\001\p\00\00000000, but it may differ from system to system.

Launching the www.google.com page creates the following record in my log file:
http://www.google.com/ 1373723797062 0 0 1373723797062 1373723797275 1373723797275 1373723797275 1373723797275 1373723797275 1373723797275 0 1373723797276 1373723797398 1373723797533 1373723797414 1373723797637 1373723797637 1373723797637 1373723799041 1373723799041 1373723799046

For a convenient viewing and analyzing, I wrote additional tool that visualizes the raw output and highlights anomalies on any stage of a page loading process.
The extension publishing process requires a one-time $5 fee, and is a pretty straightforward. A step-by-step publishing instructions can be found here.

The extension can be installed from this link: https://chrome.google.com/webstore/detail/performance-exporter/dapljbeoaecogflnbcacgadmdaifjhje
Full source code is available here: https://github.com/alipov/PerfTimingExporter

No comments:

Post a Comment