Bug 795268 - Integrate SPS Profiler; r=rcampbell
authorAnton Kovalyov <anton>
Thu, 13 Dec 2012 17:17:00 -0500
changeset 116067 548d2c909b8175504c44c6f2b126a5fa804cae3c
parent 116060 2798371f16505dddc042e659a2c691f37317cc97
child 116068 bb2f453b7c0f6d6c0d461e223745544b24d86261
push id24042
push userrcampbell@mozilla.com
push dateSat, 15 Dec 2012 14:16:24 +0000
treeherdermozilla-central@bb2f453b7c0f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell
bugs795268
milestone20.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 795268 - Integrate SPS Profiler; r=rcampbell
browser/app/profile/firefox.js
browser/devtools/Makefile.in
browser/devtools/framework/ToolDefinitions.jsm
browser/devtools/jar.mn
browser/devtools/profiler/Makefile.in
browser/devtools/profiler/ProfilerController.jsm
browser/devtools/profiler/ProfilerPanel.jsm
browser/devtools/profiler/cleopatra/cleopatra.html
browser/devtools/profiler/cleopatra/css/devtools.css
browser/devtools/profiler/cleopatra/css/tree.css
browser/devtools/profiler/cleopatra/css/ui.css
browser/devtools/profiler/cleopatra/images/circlearrow.svg
browser/devtools/profiler/cleopatra/images/filter.png
browser/devtools/profiler/cleopatra/images/noise.png
browser/devtools/profiler/cleopatra/images/showall.png
browser/devtools/profiler/cleopatra/images/throbber.svg
browser/devtools/profiler/cleopatra/images/treetwisty.svg
browser/devtools/profiler/cleopatra/js/ProgressReporter.js
browser/devtools/profiler/cleopatra/js/devtools.js
browser/devtools/profiler/cleopatra/js/parser.js
browser/devtools/profiler/cleopatra/js/parserWorker.js
browser/devtools/profiler/cleopatra/js/tree.js
browser/devtools/profiler/cleopatra/js/ui.js
browser/devtools/profiler/profiler.css
browser/devtools/profiler/profiler.xul
browser/devtools/profiler/test/Makefile.in
browser/devtools/profiler/test/browser_profiler_controller.js
browser/devtools/profiler/test/browser_profiler_profiles.js
browser/devtools/profiler/test/browser_profiler_run.js
browser/devtools/profiler/test/head.js
browser/locales/en-US/chrome/browser/devtools/profiler.properties
browser/locales/jar.mn
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1042,16 +1042,19 @@ pref("devtools.debugger.ui.win-width", 9
 pref("devtools.debugger.ui.win-height", 400);
 pref("devtools.debugger.ui.stackframes-width", 200);
 pref("devtools.debugger.ui.variables-width", 300);
 pref("devtools.debugger.ui.panes-visible-on-startup", false);
 pref("devtools.debugger.ui.variables-sorting-enabled", true);
 pref("devtools.debugger.ui.variables-only-enum-visible", false);
 pref("devtools.debugger.ui.variables-searchbox-visible", false);
 
+// Enable the Profiler
+pref("devtools.profiler.enabled", true);
+
 // Enable the Tilt inspector
 pref("devtools.tilt.enabled", true);
 pref("devtools.tilt.intro_transition", true);
 pref("devtools.tilt.outro_transition", true);
 
 // Enable the Scratchpad tool.
 pref("devtools.scratchpad.enabled", true);
 
--- a/browser/devtools/Makefile.in
+++ b/browser/devtools/Makefile.in
@@ -22,11 +22,12 @@ DIRS = \
   styleinspector \
   tilt \
   scratchpad \
   debugger \
   layoutview \
   shared \
   responsivedesign \
   framework \
+  profiler \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/framework/ToolDefinitions.jsm
+++ b/browser/devtools/framework/ToolDefinitions.jsm
@@ -13,16 +13,17 @@ this.EXPORTED_SYMBOLS = [
                         ];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
 const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
 const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
 const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
+const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/EventEmitter.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "osString",
   function() Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
 
@@ -34,29 +35,35 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource:///modules/devtools/DebuggerPanel.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "StyleEditorPanel",
   "resource:///modules/devtools/StyleEditorPanel.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "InspectorPanel",
   "resource:///modules/devtools/InspectorPanel.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ProfilerPanel",
+  "resource:///modules/devtools/ProfilerPanel.jsm");
+
 // Strings
 XPCOMUtils.defineLazyGetter(this, "webConsoleStrings",
   function() Services.strings.createBundle(webConsoleProps));
 
 XPCOMUtils.defineLazyGetter(this, "debuggerStrings",
   function() Services.strings.createBundle(debuggerProps));
 
 XPCOMUtils.defineLazyGetter(this, "styleEditorStrings",
   function() Services.strings.createBundle(styleEditorProps));
 
 XPCOMUtils.defineLazyGetter(this, "inspectorStrings",
   function() Services.strings.createBundle(inspectorProps));
 
+XPCOMUtils.defineLazyGetter(this, "profilerStrings",
+  function() Services.strings.createBundle(profilerProps));
+
 // Definitions
 let webConsoleDefinition = {
   id: "webconsole",
   key: l10n("cmd.commandkey", webConsoleStrings),
   accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings),
   modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
   ordinal: 0,
   icon: "chrome://browser/skin/devtools/webconsole-tool-icon.png",
@@ -126,23 +133,49 @@ let styleEditorDefinition = {
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new StyleEditorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
+let profilerDefinition = {
+  id: "jsprofiler",
+  killswitch: "devtools.profiler.enabled",
+  icon: "chrome://browser/skin/devtools/tools-icons-small.png",
+  url: "chrome://browser/content/profiler.xul",
+  label: l10n("profiler.label", profilerStrings),
+
+  isTargetSupported: function (target) {
+    if (target.isRemote || target.isChrome) {
+      return false;
+    }
+
+    return true;
+  },
+
+  build: function (frame, target) {
+    let panel = new ProfilerPanel(frame, target);
+    return panel.open();
+  }
+};
+
+
 this.defaultTools = [
   styleEditorDefinition,
   webConsoleDefinition,
   debuggerDefinition,
   inspectorDefinition,
 ];
 
+if (Services.prefs.getBoolPref("devtools.profiler.enabled")) {
+  defaultTools.push(profilerDefinition);
+}
+
 /**
  * Lookup l10n string from a string bundle.
  *
  * @param {string} name
  *        The key to lookup.
  * @param {StringBundle} bundle
  *        The key to lookup.
  * @returns A localized version of the given key.
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -22,16 +22,34 @@ browser.jar:
     content/browser/orion.js                      (sourceeditor/orion/orion.js)
 *   content/browser/source-editor-overlay.xul     (sourceeditor/source-editor-overlay.xul)
     content/browser/debugger.xul                  (debugger/debugger.xul)
     content/browser/debugger.css                  (debugger/debugger.css)
     content/browser/debugger-controller.js        (debugger/debugger-controller.js)
     content/browser/debugger-view.js              (debugger/debugger-view.js)
     content/browser/debugger-toolbar.js           (debugger/debugger-toolbar.js)
     content/browser/debugger-panes.js             (debugger/debugger-panes.js)
+*   content/browser/profiler.xul                  (profiler/profiler.xul)
+    content/browser/profiler.css                  (profiler/profiler.css)
+    content/browser/devtools/cleopatra.html       (profiler/cleopatra/cleopatra.html)
+    content/browser/devtools/profiler/cleopatra/css/ui.css              (profiler/cleopatra/css/ui.css)
+    content/browser/devtools/profiler/cleopatra/css/tree.css            (profiler/cleopatra/css/tree.css)
+    content/browser/devtools/profiler/cleopatra/css/devtools.css        (profiler/cleopatra/css/devtools.css)
+    content/browser/devtools/profiler/cleopatra/js/parser.js            (profiler/cleopatra/js/parser.js)
+    content/browser/devtools/profiler/cleopatra/js/parserWorker.js      (profiler/cleopatra/js/parserWorker.js)
+    content/browser/devtools/profiler/cleopatra/js/tree.js              (profiler/cleopatra/js/tree.js)
+    content/browser/devtools/profiler/cleopatra/js/ui.js                (profiler/cleopatra/js/ui.js)
+    content/browser/devtools/profiler/cleopatra/js/ProgressReporter.js  (profiler/cleopatra/js/ProgressReporter.js)
+    content/browser/devtools/profiler/cleopatra/js/devtools.js          (profiler/cleopatra/js/devtools.js)
+    content/browser/devtools/profiler/cleopatra/images/circlearrow.svg  (profiler/cleopatra/images/circlearrow.svg)
+    content/browser/devtools/profiler/cleopatra/images/filter.png       (profiler/cleopatra/images/filter.png)
+    content/browser/devtools/profiler/cleopatra/images/noise.png        (profiler/cleopatra/images/noise.png)
+    content/browser/devtools/profiler/cleopatra/images/showall.png      (profiler/cleopatra/images/showall.png)
+    content/browser/devtools/profiler/cleopatra/images/throbber.svg     (profiler/cleopatra/images/throbber.svg)
+    content/browser/devtools/profiler/cleopatra/images/treetwisty.svg   (profiler/cleopatra/images/treetwisty.svg)
     content/browser/devtools/commandline.css      (commandline/commandline.css)
     content/browser/devtools/commandlineoutput.xhtml  (commandline/commandlineoutput.xhtml)
     content/browser/devtools/commandlinetooltip.xhtml  (commandline/commandlinetooltip.xhtml)
     content/browser/devtools/framework/toolbox-window.xul    (framework/toolbox-window.xul)
     content/browser/devtools/framework/toolbox.xul           (framework/toolbox.xul)
     content/browser/devtools/framework/toolbox.css           (framework/toolbox.css)
     content/browser/devtools/inspector/inspector.xul         (inspector/inspector.xul)
     content/browser/devtools/inspector/inspector.css         (inspector/inspector.css)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/Makefile.in
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH     = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+TEST_DIRS += test
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/ProfilerController.jsm
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+
+let EXPORTED_SYMBOLS = ["ProfilerController"];
+
+XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function () {
+  Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+  return DebuggerServer;
+});
+
+/**
+ * Object acting as a mediator between the ProfilerController and
+ * DebuggerServer.
+ */
+function ProfilerConnection() {
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init();
+    DebuggerServer.addBrowserActors();
+  }
+
+  let transport = DebuggerServer.connectPipe();
+  this.client = new DebuggerClient(transport);
+}
+
+ProfilerConnection.prototype = {
+  actor: null,
+
+  /**
+   * Connects to a debugee and executes a callback when ready.
+   *
+   * @param function aCallback
+   *        Function to be called once we're connected to the client.
+   */
+  connect: function PCn_connect(aCallback) {
+    let client = this.client;
+
+    client.connect(function (aType, aTraits) {
+      client.listTabs(function (aResponse) {
+        this.actor = aResponse.profilerActor;
+        aCallback();
+      }.bind(this));
+    }.bind(this));
+  },
+
+  /**
+   * Sends a message to check if the profiler is currently active.
+   *
+   * @param function aCallback
+   *        Function to be called once we have a response from
+   *        the client. It will be called with a single argument
+   *        containing a response object.
+   */
+  isActive: function PCn_isActive(aCallback) {
+    var message = { to: this.actor, type: "isActive" };
+    this.client.request(message, aCallback);
+  },
+
+  /**
+   * Sends a message to start a profiler.
+   *
+   * @param function aCallback
+   *        Function to be called once the profiler is running.
+   *        It will be called with a single argument containing
+   *        a response object.
+   */
+  startProfiler: function PCn_startProfiler(aCallback) {
+    var message = {
+      to: this.actor,
+      type: "startProfiler",
+      entries: 1000000,
+      interval: 1,
+      features: ["js"],
+    };
+    this.client.request(message, aCallback);
+  },
+
+  /**
+   * Sends a message to stop a profiler.
+   *
+   * @param function aCallback
+   *        Function to be called once the profiler is idle.
+   *        It will be called with a single argument containing
+   *        a response object.
+   */
+  stopProfiler: function PCn_stopProfiler(aCallback) {
+    var message = { to: this.actor, type: "stopProfiler" };
+    this.client.request(message, aCallback);
+  },
+
+  /**
+   * Sends a message to get the generated profile data.
+   *
+   * @param function aCallback
+   *        Function to be called once we have the data.
+   *        It will be called with a single argument containing
+   *        a response object.
+   */
+  getProfileData: function PCn_getProfileData(aCallback) {
+    var message = { to: this.actor, type: "getProfile" };
+    this.client.request(message, aCallback);
+  },
+
+  /**
+   * Cleanup.
+   */
+  destroy: function PCn_destroy() {
+    this.client.close(function () {
+      this.client = null;
+    }.bind(this));
+  }
+};
+
+/**
+ * Object defining the profiler controller components.
+ */
+function ProfilerController() {
+  this.profiler = new ProfilerConnection();
+  this._connected = false;
+}
+
+ProfilerController.prototype = {
+  /**
+   * Connects to the client unless we're already connected.
+   *
+   * @param function aCallback
+   *        Function to be called once we're connected. If
+   *        the controller is already connected, this function
+   *        will be called immediately (synchronously).
+   */
+  connect: function (aCallback) {
+    if (this._connected) {
+      aCallback();
+    }
+
+    this.profiler.connect(function onConnect() {
+      this._connected = true;
+      aCallback();
+    }.bind(this));
+  },
+
+  /**
+   * Checks whether the profiler is active.
+   *
+   * @param function aCallback
+   *        Function to be called with a response from the
+   *        client. It will be called with two arguments:
+   *        an error object (may be null) and a boolean
+   *        value indicating if the profiler is active or not.
+   */
+  isActive: function PC_isActive(aCallback) {
+    this.profiler.isActive(function onActive(aResponse) {
+      aCallback(aResponse.error, aResponse.isActive);
+    });
+  },
+
+  /**
+   * Starts the profiler.
+   *
+   * @param function aCallback
+   *        Function to be called once the profiler is started
+   *        or we get an error. It will be called with a single
+   *        argument: an error object (may be null).
+   */
+  start: function PC_start(aCallback) {
+    this.profiler.startProfiler(function onStart(aResponse) {
+      aCallback(aResponse.error);
+    });
+  },
+
+  /**
+   * Stops the profiler.
+   *
+   * @param function aCallback
+   *        Function to be called once the profiler is stopped
+   *        or we get an error. It will be called with a single
+   *        argument: an error object (may be null).
+   */
+  stop: function PC_stop(aCallback) {
+    this.profiler.getProfileData(function onData(aResponse) {
+      let data = aResponse.profile;
+      if (aResponse.error) {
+        Cu.reportError("Failed to fetch profile data before stopping the profiler.");
+      }
+
+      this.profiler.stopProfiler(function onStop(aResponse) {
+        aCallback(aResponse.error, data);
+      });
+    }.bind(this));
+  },
+
+  /**
+   * Cleanup.
+   */
+  destroy: function PC_destroy(aCallback) {
+    this.profiler.destroy();
+    this.profiler = null;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/ProfilerPanel.jsm
@@ -0,0 +1,393 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource:///modules/devtools/ProfilerController.jsm");
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/devtools/EventEmitter.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["ProfilerPanel"];
+
+XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function () {
+  Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+  return DebuggerServer;
+});
+
+/**
+ * An instance of a profile UI. Profile UI consists of
+ * an iframe with Cleopatra loaded in it and some
+ * surrounding meta-data (such as uids).
+ *
+ * Its main function is to talk to the Cleopatra instance
+ * inside of the iframe.
+ *
+ * ProfileUI is also an event emitter. Currently, it emits
+ * only one event, 'ready', when Cleopatra is done loading.
+ * You can also check 'isReady' property to see if a
+ * particular instance has been loaded yet.
+ *
+ * @param number uid
+ *   Unique ID for this profile.
+ * @param ProfilerPanel panel
+ *   A reference to the container panel.
+ */
+function ProfileUI(uid, panel) {
+  let doc = panel.document;
+  let win = panel.window;
+
+  new EventEmitter(this);
+
+  this.isReady = false;
+  this.panel = panel;
+  this.uid = uid;
+
+  this.iframe = doc.createElement("iframe");
+  this.iframe.setAttribute("flex", "1");
+  this.iframe.setAttribute("id", "profiler-cleo-" + uid);
+  this.iframe.setAttribute("src", "devtools/cleopatra.html?" + uid);
+  this.iframe.setAttribute("hidden", "true");
+
+  // Append our iframe and subscribe to postMessage events.
+  // They'll tell us when the underlying page is done loading
+  // or when user clicks on start/stop buttons.
+
+  doc.getElementById("profiler-report").appendChild(this.iframe);
+  win.addEventListener("message", function (event) {
+    if (parseInt(event.data.uid, 10) !== parseInt(this.uid, 10)) {
+      return;
+    }
+
+    switch (event.data.status) {
+      case "loaded":
+        this.isReady = true;
+        this.emit("ready");
+        break;
+      case "start":
+        // Start profiling and, once started, notify the
+        // underlying page so that it could update the UI.
+        this.panel.startProfiling(function onStart() {
+          var data = JSON.stringify({task: "onStarted"});
+          this.iframe.contentWindow.postMessage(data, "*");
+        }.bind(this));
+
+        break;
+      case "stop":
+        // Stop profiling and, once stopped, notify the
+        // underlying page so that it could update the UI.
+        this.panel.stopProfiling(function onStop() {
+          var data = JSON.stringify({task: "onStopped"});
+          this.iframe.contentWindow.postMessage(data, "*");
+        }.bind(this));
+    }
+  }.bind(this));
+}
+
+ProfileUI.prototype = {
+  show: function PUI_show() {
+    this.iframe.removeAttribute("hidden");
+  },
+
+  hide: function PUI_hide() {
+    this.iframe.setAttribute("hidden", true);
+  },
+
+  /**
+   * Send raw profiling data to Cleopatra for parsing.
+   *
+   * @param object data
+   *   Raw profiling data from the SPS Profiler.
+   * @param function onParsed
+   *   A callback to be called when Cleopatra finishes
+   *   parsing and displaying results.
+   *
+   */
+  parse: function PUI_parse(data, onParsed) {
+    if (!this.isReady) {
+      return;
+    }
+
+    let win = this.iframe.contentWindow;
+
+    win.postMessage(JSON.stringify({
+      task: "receiveProfileData",
+      rawProfile: data
+    }), "*");
+
+    let poll = function pollBreadcrumbs() {
+      let wait = this.panel.window.setTimeout.bind(null, poll, 100);
+      let trail = win.gBreadcrumbTrail;
+
+      if (!trail) {
+        return wait();
+      }
+
+      if (!trail._breadcrumbs || !trail._breadcrumbs.length) {
+        return wait();
+      }
+
+      onParsed();
+    }.bind(this);
+
+    poll();
+  },
+
+  /**
+   * Destroys the ProfileUI instance.
+   */
+  destroy: function PUI_destroy() {
+    this.isReady = null
+    this.panel = null;
+    this.uid = null;
+    this.iframe = null;
+  }
+};
+
+/**
+ * Profiler panel. It is responsible for creating and managing
+ * different profile instances (see ProfileUI).
+ *
+ * ProfilerPanel is an event emitter. It can emit the following
+ * events:
+ *
+ *   - ready:     after the panel is done loading everything,
+ *                including the default profile instance.
+ *   - started:   after the panel successfuly starts our SPS
+ *                profiler.
+ *   - stopped:   after the panel successfuly stops our SPS
+ *                profiler and is ready to hand over profiling
+ *                data
+ *   - parsed:    after Cleopatra finishes parsing profiling
+ *                data.
+ *   - destroyed: after the panel cleans up after itself and
+ *                is ready to be destroyed.
+ *
+ * The following events are used mainly by tests to prevent
+ * accidential oranges:
+ *
+ *   - profileCreated:  after a new profile is created.
+ *   - profileSwitched: after user switches to a different
+ *                      profile.
+ */
+function ProfilerPanel(frame, toolbox) {
+  this.isReady = false;
+  this.window = frame.window;
+  this.document = frame.document;
+  this.target = toolbox.target;
+  this.controller = new ProfilerController();
+
+  this.profiles = new Map();
+  this._uid = 0;
+
+  new EventEmitter(this);
+}
+
+ProfilerPanel.prototype = {
+  isReady:    null,
+  window:     null,
+  document:   null,
+  target:     null,
+  controller: null,
+  profiles:   null,
+
+  _uid:       null,
+  _activeUid: null,
+
+  get activeProfile() {
+    return this.profiles.get(this._activeUid);
+  },
+
+  set activeProfile(profile) {
+    this._activeUid = profile.uid;
+  },
+
+  /**
+   * Open a debug connection and, on success, switch to
+   * the newly created profile.
+   *
+   * @return Promise
+   */
+  open: function PP_open() {
+    let deferred = Promise.defer();
+
+    this.controller.connect(function onConnect() {
+      let create = this.document.getElementById("profiler-create");
+      create.addEventListener("click", this.createProfile.bind(this), false);
+      create.removeAttribute("disabled");
+
+      let profile = this.createProfile();
+      this.switchToProfile(profile, function () {
+        this.isReady = true;
+        this.emit("ready");
+
+        deferred.resolve(this);
+      }.bind(this))
+    }.bind(this));
+
+    return deferred.promise;
+  },
+
+  /**
+   * Creates a new profile instance (see ProfileUI) and
+   * adds an appropriate item to the sidebar. Note that
+   * this method doesn't automatically switch user to
+   * the newly created profile, they have do to switch
+   * explicitly.
+   *
+   * @return ProfilerPanel
+   */
+  createProfile: function PP_addProfile() {
+    let uid  = ++this._uid;
+    let list = this.document.getElementById("profiles-list");
+    let item = this.document.createElement("li");
+    let wrap = this.document.createElement("h1");
+
+    item.setAttribute("id", "profile-" + uid);
+    item.setAttribute("data-uid", uid);
+    item.addEventListener("click", function (ev) {
+      let uid = parseInt(ev.target.getAttribute("data-uid"), 10);
+      this.switchToProfile(this.profiles.get(uid));
+    }.bind(this), false);
+
+    wrap.className = "profile-name";
+    wrap.textContent = "Profile " + uid;
+
+    item.appendChild(wrap);
+    list.appendChild(item);
+
+    let profile = new ProfileUI(uid, this);
+    this.profiles.set(uid, profile);
+
+    this.emit("profileCreated", uid);
+    return profile;
+  },
+
+  /**
+   * Switches to a different profile by making its instance an
+   * active one.
+   *
+   * @param ProfileUI profile
+   *   A profile instance to switch to.
+   * @param function onLoad
+   *   A function to call when profile instance is ready.
+   *   If the instance is already loaded, onLoad will be
+   *   called synchronously.
+   */
+  switchToProfile: function PP_switchToProfile(profile, onLoad) {
+    let doc = this.document;
+
+    if (this.activeProfile) {
+      this.activeProfile.hide();
+    }
+
+    let active = doc.querySelector("#profiles-list > li.splitview-active");
+    if (active) {
+      active.className = "";
+    }
+
+    doc.getElementById("profile-" + profile.uid).className = "splitview-active";
+    profile.show();
+    this.activeProfile = profile;
+
+    if (profile.isReady) {
+      this.emit("profileSwitched", profile.uid);
+      onLoad();
+      return;
+    }
+
+    profile.once("ready", function () {
+      this.emit("profileSwitched", profile.uid);
+      onLoad();
+    }.bind(this));
+  },
+
+  /**
+   * Start collecting profile data.
+   *
+   * @param function onStart
+   *   A function to call once we get the message
+   *   that profiling had been successfuly started.
+   */
+  startProfiling: function PP_startProfiling(onStart) {
+    this.controller.start(function (err) {
+      if (err) {
+        Cu.reportError("ProfilerController.start: " + err.message);
+        return;
+      }
+
+      onStart();
+      this.emit("started");
+    }.bind(this));
+  },
+
+  /**
+   * Stop collecting profile data and send it to Cleopatra
+   * for parsing.
+   *
+   * @param function onStop
+   *   A function to call once we get the message
+   *   that profiling had been successfuly stopped.
+   */
+  stopProfiling: function PP_stopProfiling(onStop) {
+    this.controller.isActive(function (err, isActive) {
+      if (err) {
+        Cu.reportError("ProfilerController.isActive: " + err.message);
+        return;
+      }
+
+      if (!isActive) {
+        return;
+      }
+
+      this.controller.stop(function (err, data) {
+        if (err) {
+          Cu.reportError("ProfilerController.stop: " + err.message);
+          return;
+        }
+
+        this.activeProfile.parse(data, function onParsed() {
+          this.emit("parsed");
+        }.bind(this));
+
+        onStop();
+        this.emit("stopped");
+      }.bind(this));
+    }.bind(this));
+  },
+
+  /**
+   * Cleanup.
+   */
+  destroy: function PP_destroy() {
+    if (this.profiles) {
+      let uid = this._uid;
+
+      while (uid >= 0) {
+        if (this.profiles.has(uid)) {
+          this.profiles.get(uid).destroy();
+          this.profiles.delete(uid);
+        }
+        uid -= 1;
+      }
+    }
+
+    if (this.controller) {
+      this.controller.destroy();
+    }
+
+    this.isReady = null;
+    this.window = null;
+    this.document = null;
+    this.target = null;
+    this.controller = null;
+    this.profiles = null;
+    this._uid = null;
+    this._activeUid = null;
+
+    this.emit("destroyed");
+  }
+};
\ No newline at end of file
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/cleopatra.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+	 - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html>
+  <head>
+    <title>Firefox Profiler (SPS)</title>
+    <meta charset="utf-8">
+
+    <link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/ui.css">
+    <link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/tree.css">
+    <link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/devtools.css">
+
+    <script src="profiler/cleopatra/js/parser.js"></script>
+    <script src="profiler/cleopatra/js/tree.js"></script>
+    <script src="profiler/cleopatra/js/ui.js"></script>
+    <script src="profiler/cleopatra/js/ProgressReporter.js"></script>
+    <script src="profiler/cleopatra/js/devtools.js"></script>
+
+    <link rel="shortcut icon" href="favicon.png" />
+  </head>
+
+  <body onload="notifyParent('loaded');">
+    <script>
+      initUI();
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/css/devtools.css
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#mainarea > .controlPane {
+  font-size: 120%;
+  padding-top: 75px;
+  text-align: center;
+}
+
+#stopWrapper {
+  display: none;
+}
\ No newline at end of file
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/css/tree.css
@@ -0,0 +1,245 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.treeViewContainer {
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  user-select: none;
+  cursor: default;
+  line-height: 16px;
+  height: 100%;
+  outline: none; /* override the browser's focus styling */
+  position: relative;
+}
+
+.treeHeader {
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+  height: 16px;
+  margin: 0;
+  padding: 0;
+}
+
+.treeColumnHeader {
+  position: absolute;
+  display: block;
+  background: -moz-linear-gradient(#FFF 45%, #EEE 60%);
+  background: -webkit-linear-gradient(#FFF 45%, #EEE 60%);
+  background: linear-gradient(#FFF 45%, #EEE 60%);
+  margin: 0;
+  padding: 0;
+  top: 0;
+  height: 15px;
+  line-height: 15px;
+  border: 0 solid #CCC;
+  border-bottom-width: 1px;
+  text-indent: 5px;
+}
+
+.treeColumnHeader:not(:last-child) {
+  border-right-width: 1px;
+}
+
+.treeColumnHeader0 {
+  left: 0;
+  width: 86px;
+}
+
+.treeColumnHeader1 {
+  left: 99px;
+  width: 35px;
+}
+
+.treeColumnHeader0,
+.treeColumnHeader1 {
+  text-align: right;
+  padding-right: 12px;
+}
+
+.treeColumnHeader2 {
+  left: 147px;
+  right: 0;
+}
+
+.treeViewVerticalScrollbox {
+  position: absolute;
+  top: 16px;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow-y: scroll;
+  overflow-x: hidden;
+}
+
+.treeViewNode,
+.treeViewHorizontalScrollbox {
+  display: block;
+  margin: 0;
+  padding: 0;
+}
+
+.treeViewNode {
+  min-width: -moz-min-content;
+  white-space: nowrap;
+}
+
+.treeViewHorizontalScrollbox {
+  padding-left: 150px;
+  overflow: hidden;
+}
+
+.treeViewVerticalScrollbox,
+.treeViewHorizontalScrollbox {
+  background: -moz-linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
+  background: -webkit-linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
+  background: linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
+  background-size: 100px 32px;
+}
+
+.leftColumnBackground {
+  background: -moz-linear-gradient(left, transparent, transparent 98px, #CCC 98px, #CCC 99px, transparent 99px),
+    -moz-linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
+  background: -webkit-linear-gradient(left, transparent, transparent 98px, #CCC 98px, #CCC 99px, transparent 99px),
+    -webkit-linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
+  background: linear-gradient(left, transparent, transparent 98px, #CCC 98px, #CCC 99px, transparent 99px),
+    linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
+  background-size: auto, 100px 32px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 146px;
+  min-height: 100%;
+  border-right: 1px solid #CCC;
+}
+
+.sampleCount,
+.samplePercentage,
+.selfSampleCount {
+  position: absolute;
+  text-align: right;
+}
+
+.sampleCount {
+  left: 2px;
+  width: 50px;
+}
+
+.samplePercentage {
+  left: 55px;
+  width: 40px;
+}
+
+.selfSampleCount {
+  left: 98px;
+  width: 45px;
+  padding-right: 2px;
+  border: solid #CCC;
+  border-width: 0 1px;
+}
+
+.libraryName {
+  margin-left: 10px;
+  color: #999;
+}
+
+.treeViewNode > .treeViewNodeList {
+  margin-left: 1em;
+}
+
+.treeViewNode.collapsed > .treeViewNodeList {
+  display: none;
+}
+
+.treeLine {
+  /* extend the selection background almost infinitely to the left */
+  margin-left: -10000px;
+  padding-left: 10000px;
+}
+
+.treeLine.selected {
+  color: black;
+  background-color: -moz-dialog;
+}
+
+.treeLine.selected > .sampleCount {
+  background-color: inherit;
+  margin-left: -2px;
+  padding-left: 2px;
+  padding-right: 95px;
+  margin-right: -95px;
+}
+
+.treeViewContainer:focus .treeLine.selected {
+  color: highlighttext;
+  background-color: highlight;
+}
+
+.treeViewContainer:focus .treeLine.selected > .libraryName {
+  color: #CCC;
+}
+
+.expandCollapseButton,
+.focusCallstackButton {
+  background: none 0 0 no-repeat transparent;
+  margin: 0;
+  padding: 0;
+  border: 0;
+  width: 16px;
+  height: 16px;
+  overflow: hidden;
+  vertical-align: top;
+  color: transparent;
+  font-size: 0;
+}
+
+.expandCollapseButton {
+  background-image: url(../images/treetwisty.svg);
+}
+
+.focusCallstackButton {
+  background-image: url(../images/circlearrow.svg);
+  margin-left: 5px;
+  visibility: hidden;
+}
+
+.expandCollapseButton:active:hover,
+.focusCallstackButton:active:hover {
+  background-position: -16px 0;
+}
+
+.treeViewNode.collapsed > .treeLine > .expandCollapseButton {
+  background-position: 0 -16px;
+}
+
+.treeViewNode.collapsed > .treeLine > .expandCollapseButton:active:hover {
+  background-position: -16px -16px;
+}
+
+.treeViewContainer:focus .treeLine.selected > .expandCollapseButton,
+.treeViewContainer:focus .treeLine.selected > .focusCallstackButton {
+  background-position: -32px 0;
+}
+
+.treeViewContainer:focus .treeViewNode.collapsed > .treeLine.selected > .expandCollapseButton {
+  background-position: -32px -16px;
+}
+
+.treeViewContainer:focus .treeLine.selected > .expandCollapseButton:active:hover,
+.treeViewContainer:focus .treeLine.selected > .focusCallstackButton:active:hover {
+  background-position: -48px 0;
+}
+
+.treeViewContainer:focus .treeViewNode.collapsed > .treeLine.selected > .expandCollapseButton:active:hover {
+  background-position: -48px -16px;
+}
+
+.treeViewNode.leaf > * > .expandCollapseButton {
+  visibility: hidden;
+}
+
+.treeLine:hover > .focusCallstackButton {
+  visibility: visible;
+}
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/css/ui.css
@@ -0,0 +1,344 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+body {
+  margin: 0;
+  font-family: "Lucida Grande", sans-serif;
+  font-size: 11px;
+}
+#mainarea {
+  position: absolute;
+  top: 0;
+  left: 0px;
+  bottom: 0;
+  right: 0;
+}
+.finishedProfilePane,
+.finishedProfilePaneBackgroundCover,
+.profileEntryPane,
+.profileProgressPane {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+}
+.profileEntryPane {
+  overflow: auto;
+}
+.profileEntryPane,
+.profileProgressPane {
+  padding: 20px;
+  background-color: rgb(229,229,229);
+  background-image: url(../images/noise.png),
+                    -moz-linear-gradient(rgba(255,255,255,.5),rgba(255,255,255,.2));
+  background-image: url(../images/noise.png),
+                    -webkit-linear-gradient(rgba(255,255,255,.5),rgba(255,255,255,.2));
+  background-image: url(../images/noise.png),
+                    linear-gradient(rgba(255,255,255,.5),rgba(255,255,255,.2));
+  text-shadow: rgba(255, 255, 255, 0.4) 0 1px;
+}
+.profileEntryPane h1 {
+  margin-top: 0;
+  font-size: 13px;
+  font-weight: normal;
+}
+.profileEntryPane input[type="file"] {
+  margin-bottom: 1em;
+}
+.profileProgressPane a {
+  position: absolute;
+  top: 30%;
+  left: 30%;
+  width: 40%;
+  height: 16px;
+}
+.profileProgressPane progress {
+  position: absolute;
+  top: 40%;
+  left: 30%;
+  width: 40%;
+  height: 16px;
+}
+.finishedProfilePaneBackgroundCover {
+  -webkit-animation: darken 300ms cubic-bezier(0, 0, 1, 0);
+  -moz-animation: darken 300ms cubic-bezier(0, 0, 1, 0);
+  background-color: rgba(0, 0, 0, 0.5);
+}
+.finishedProfilePane {
+  -webkit-animation: appear 300ms ease-out;
+  -moz-animation: appear 300ms ease-out;
+}
+
+.breadcrumbTrail {
+  top: 0;
+  right: 0;
+  height: 29px;
+  left: 0;
+  background: -moz-linear-gradient(#FFF 50%, #F3F3F3 55%);
+  background: -webkit-linear-gradient(#FFF 50%, #F3F3F3 55%);
+  background: linear-gradient(#FFF 50%, #F3F3F3 55%);
+  border-bottom: 1px solid #CCC;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+}
+.breadcrumbTrailItem {
+  background: -moz-linear-gradient(#FFF 50%, #F3F3F3 55%);
+  background: -webkit-linear-gradient(#FFF 50%, #F3F3F3 55%);
+  background: linear-gradient(#FFF 50%, #F3F3F3 55%);
+  display: block;
+  margin: 0;
+  padding: 0;
+  float: left;
+  line-height: 29px;
+  padding: 0 10px;
+  font-size: 12px;
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  user-select: none;
+  cursor: default;
+  border-right: 1px solid #CCC;
+  max-width: 250px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  position: relative;
+}
+@-webkit-keyframes slide-out {
+  from {
+    margin-left: -270px;
+    opacity: 0;
+  }
+  to {
+    margin-left: 0;
+    opacity: 1;
+  }
+}
+@-moz-keyframes slide-out {
+  from {
+    margin-left: -270px;
+    opacity: 0;
+  }
+  to {
+    margin-left: 0;
+    opacity: 1;
+  }
+}
+.breadcrumbTrailItem:not(:first-child) {
+  -moz-animation: slide-out;
+  -moz-animation-duration: 400ms;
+  -moz-animation-timing-function: ease-out;
+  -webkit-animation: slide-out;
+  -webkit-animation-duration: 400ms;
+  -webkit-animation-timing-function: ease-out;
+}
+.breadcrumbTrailItem.selected {
+  background: linear-gradient(#E5E5E5 50%, #DADADA 55%);
+}
+.breadcrumbTrailItem:not(.selected):active:hover {
+  background: linear-gradient(#F2F2F2 50%, #E6E6E6 55%);
+}
+.breadcrumbTrailItem.deleted {
+  -moz-transition: 400ms ease-out;
+  -moz-transition-property: opacity, margin-left;
+  -webkit-transition: 400ms ease-out;
+  -webkit-transition-property: opacity, margin-left;
+  opacity: 0;
+  margin-left: -270px;
+}
+.treeContainer {
+  /*For asbolute position child*/
+  position: relative;
+}
+.tree {
+  height: 100%;
+}
+#sampleBar {
+  position: absolute;
+  float: right;
+  left: auto;
+  top: 0;
+  right: 0;
+  height: 100%;
+}
+#fileList {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 360px;
+  width: 199px;
+  overflow: auto;
+  padding: 0;
+  margin: 0;
+  background: #DBDFE7;
+  border-right: 1px solid #BBB;
+  cursor: pointer;
+}
+#infoBar dl {
+  margin: 0;
+}
+#infoBar dt,
+#infoBar dd {
+  display: inline;
+}
+#infoBar dt {
+  font-weight: bold;
+}
+#infoBar dt::after {
+  content: " ";
+  white-space: pre;
+}
+#infoBar dd {
+  margin-left: 0;
+}
+#infoBar dd::after {
+  content: "\a";
+  white-space:pre;
+}
+.sideBar {
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  width: 200px;
+  height: 480px;
+  overflow: auto;
+  padding: 3px;
+  background: #EEE;
+  border-top: 1px solid #BBB;
+  border-right: 1px solid #BBB;
+}
+.sideBar h2 {
+  font-size: 1em;
+  padding: 1px 3px;
+  margin: 3px -3px;
+  background: rgba(255, 255, 255, 0.6);
+  border: solid #CCC;
+  border-width: 1px 0;
+}
+.sideBar h2:first-child {
+  margin-top: -4px;
+}
+.sideBar ul {
+  margin: 2px 0;
+  padding-left: 18px;
+}
+.pluginview {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1;
+  background-color: white;
+}
+.pluginviewIFrame {
+  border-style: none;
+  width: 100%;
+  height: 100%;
+}
+.histogram {
+  position: relative;
+  height: 60px;
+  right: 0;
+  left: 0;
+  border-bottom: 1px solid #CCC;
+  background: -moz-linear-gradient(#EEE, #CCC);
+  background: -webkit-linear-gradient(#EEE, #CCC);
+  background: linear-gradient(#EEE, #CCC);
+}
+.histogramHilite {
+  position: absolute;
+  pointer-events: none;
+}
+.histogramHilite:not(.collapsed) {
+  background: rgba(150, 150, 150, 0.5);
+}
+.histogramMouseMarker {
+  position: absolute;
+  pointer-events: none;
+  top: 0;
+  width: 1px;
+  height: 100%;
+}
+.histogramMouseMarker:not(.collapsed) {
+    background: rgba(0, 0, 150, 0.7);
+}
+#iconbox {
+  display: none;
+}
+#filter, #showall {
+  cursor: pointer;
+}
+.markers {
+  display: none;
+}
+.hidden {
+  display: none !important;
+}
+.fileListItem {
+  display: block;
+  margin: 0;
+  padding: 0;
+  height: 40px;
+  text-indent: 8px;
+}
+.fileListItem.selected {
+  background: -moz-linear-gradient(#4B91D7 1px, #5FA9E4 1px, #5FA9E4 2px, #58A0DE 3px, #2B70C7 39px, #2763B4 39px);
+  background: -webkit-linear-gradient(#4B91D7 1px, #5FA9E4 1px, #5FA9E4 2px, #58A0DE 3px, #2B70C7 39px, #2763B4 39px);
+  background: linear-gradient(#4B91D7 1px, #5FA9E4 1px, #5FA9E4 2px, #58A0DE 3px, #2B70C7 39px, #2763B4 39px);
+  color: #FFF;
+  text-shadow: 0 1px rgba(0, 0, 0, 0.3);
+}
+.fileListItemTitle {
+  display: block;
+  padding-top: 6px;
+  font-size: 12px;
+}
+.fileListItemDescription {
+  display: block;
+  line-height: 15px;
+  font-size: 9px;
+}
+.busyCover {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  visibility: hidden;
+  opacity: 0;
+  pointer-events: none;
+  background: rgba(120, 120, 120, 0.2);
+  -moz-transition: 200ms ease-in-out;
+  -moz-transition-property: visibility, opacity;
+  -webkit-transition: 200ms ease-in-out;
+  -webkit-transition-property: visibility, opacity;
+}
+.busyCover.busy {
+  visibility: visible;
+  opacity: 1;
+}
+.busyCover::before {
+  content: url(../images/throbber.svg);
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  margin: -12px;
+}
+label {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+}
+.videoPane {
+  background-color: white;
+  width: 100%;
+}
+.video {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/images/circlearrow.svg
@@ -0,0 +1,27 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+	 - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg  xmlns="http://www.w3.org/2000/svg"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      width="64" height="16" viewBox="0 0 64 16">
+  <defs>
+    <mask id="arrowInCircle" maskContentUnits="userSpaceOnUse">
+      <circle cx="8" cy="8" r="6" fill="white"/>
+      <rect x="4.5" y="7" width="3.5" height="2" fill="black"/>
+      <polyline points="8 4 12 8 8 12" fill="black"/>
+    </mask>
+  </defs>
+  <g fill="#888">
+    <rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
+  </g>
+  <g fill="#444" transform="translate(16,0)">
+    <rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
+  </g>
+  <g fill="#FFF" transform="translate(32,0)">
+    <rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
+  </g>
+  <g fill="rgba(255, 255, 255, 0.7)" transform="translate(48,0)">
+    <rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
+  </g>
+</svg>
new file mode 100755
new file mode 100755
new file mode 100755
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/images/throbber.svg
@@ -0,0 +1,23 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+	 - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg  xmlns="http://www.w3.org/2000/svg"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      width="24" height="24" viewBox="0 0 64 64">
+  <g>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(0, 32, 32)" fill="#BBB"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(30, 32, 32)" fill="#AAA"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(60, 32, 32)" fill="#999"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(90, 32, 32)" fill="#888"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(120, 32, 32)" fill="#777"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(150, 32, 32)" fill="#666"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(180, 32, 32)" fill="#555"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(210, 32, 32)" fill="#444"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(240, 32, 32)" fill="#333"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(270, 32, 32)" fill="#222"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(300, 32, 32)" fill="#111"/>
+    <rect x="30" y="4" width="4" height="15" transform="rotate(330, 32, 32)" fill="#000"/>
+    <animateTransform attributeName="transform" type="rotate" calcMode="discrete" values="0 32 32;30 32 32;60 32 32;90 32 32;120 32 32;150 32 32;180 32 32;210 32 32;240 32 32;270 32 32;300 32 32;330 32 32" dur="0.8s" repeatCount="indefinite"/>
+  </g>
+</svg>
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/images/treetwisty.svg
@@ -0,0 +1,32 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+	 - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg  xmlns="http://www.w3.org/2000/svg"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      width="64" height="32" viewBox="0 0 64 32">
+  <g fill="#888">
+    <polyline points="3 4 12 4 7.5 12"/>
+    <g transform="translate(0,16)">
+      <polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
+    </g>
+  </g>
+  <g fill="#444" transform="translate(16,0)">
+    <polyline points="3 4 12 4 7.5 12"/>
+    <g transform="translate(0,16)">
+      <polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
+    </g>
+  </g>
+  <g fill="#FFF" transform="translate(32,0)">
+    <polyline points="3 4 12 4 7.5 12"/>
+    <g transform="translate(0,16)">
+      <polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
+    </g>
+  </g>
+  <g fill="rgba(255, 255, 255, 0.7)" transform="translate(48,0)">
+    <polyline points="3 4 12 4 7.5 12"/>
+    <g transform="translate(0,16)">
+      <polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
+    </g>
+  </g>
+</svg>
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/ProgressReporter.js
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * ProgressReporter
+ *
+ * This class is used by long-winded tasks to report progress to observers.
+ * If a task has subtasks that want to report their own progress, these
+ * subtasks can have their own progress reporters which are hooked up to the
+ * parent progress reporter, resulting in a tree structure. A parent progress
+ * reporter will calculate its progress value as a weighted sum of its
+ * subreporters' progress values.
+ *
+ * A progress reporter has a state, an action, and a progress value.
+ *
+ *  - state is one of STATE_WAITING, STATE_DOING and STATE_FINISHED.
+ *  - action is a string that describes the current task.
+ *  - progress is the progress value as a number between 0 and 1, or NaN if
+ *    indeterminate.
+ *
+ * A progress reporter starts out in the WAITING state. The DOING state is
+ * entered with the begin method which also sets the action. While the task is
+ * executing, the progress value can be updated with the setProgress method.
+ * When a task has finished, it can call the finish method which is just a
+ * shorthand for setProgress(1); this will set the state to FINISHED.
+ *
+ * Progress observers can be added with the addListener method which takes a
+ * function callback. Whenever the progress value or state change, all
+ * listener callbacks will be called with the progress reporter object. The
+ * observer can get state, progress value and action by calling the getter
+ * methods getState(), getProgress() and getAction().
+ *
+ * Creating child progress reporters for subtasks can be done with the
+ * addSubreporter(s) methods. If a progress reporter has subreporters, normal
+ * progress report functions (setProgress and finish) can no longer be called.
+ * Instead, the parent reporter will listen to progress changes on its
+ * subreporters and update its state automatically, and then notify its own
+ * listeners.
+ * When adding a subreporter, you are expected to provide an estimated
+ * duration for the subtask. This value will be used as a weight when
+ * calculating the progress of the parent reporter.
+ */
+
+const gDebugExpectedDurations = false;
+
+function ProgressReporter() {
+  this._observers = [];
+  this._subreporters = [];
+  this._subreporterExpectedDurationsSum = 0;
+  this._progress = 0;
+  this._state = ProgressReporter.STATE_WAITING;
+  this._action = "";
+}
+
+ProgressReporter.STATE_WAITING = 0;
+ProgressReporter.STATE_DOING = 1;
+ProgressReporter.STATE_FINISHED = 2;
+
+ProgressReporter.prototype = {
+  getProgress: function () {
+    return this._progress;
+  },
+  getState: function () {
+    return this._state;
+  },
+  setAction: function (action) {
+    this._action = action;
+    this._reportProgress();
+  },
+  getAction: function () {
+    switch (this._state) {
+      case ProgressReporter.STATE_WAITING:
+        return "Waiting for preceding tasks to finish...";
+      case ProgressReporter.STATE_DOING:
+        return this._action;
+      case ProgressReporter.STATE_FINISHED:
+        return "Finished.";
+      default:
+        throw "Broken state";
+    }
+  },
+  addListener: function (callback) {
+    this._observers.push(callback);
+  },
+  addSubreporter: function (expectedDuration) {
+    this._subreporterExpectedDurationsSum += expectedDuration;
+    var subreporter = new ProgressReporter();
+    var self = this;
+    subreporter.addListener(function (progress) {
+      self._recalculateProgressFromSubreporters();
+      self._recalculateStateAndActionFromSubreporters();
+      self._reportProgress();
+    });
+    this._subreporters.push({ expectedDuration: expectedDuration, reporter: subreporter });
+    return subreporter;
+  },
+  addSubreporters: function (expectedDurations) {
+    var reporters = {};
+    for (var key in expectedDurations) {
+      reporters[key] = this.addSubreporter(expectedDurations[key]);
+    }
+    return reporters;
+  },
+  begin: function (action) {
+    this._startTime = Date.now();
+    this._state = ProgressReporter.STATE_DOING;
+    this._action = action;
+    this._reportProgress();
+  },
+  setProgress: function (progress) {
+    if (this._subreporters.length > 0)
+      throw "Can't call setProgress on a progress reporter with subreporters";
+    if (progress != this._progress &&
+        (progress == 1 ||
+         (isNaN(progress) != isNaN(this._progress)) ||
+         (progress - this._progress >= 0.01))) {
+      this._progress = progress;
+      if (progress == 1)
+        this._transitionToFinished();
+      this._reportProgress();
+    }
+  },
+  finish: function () {
+    this.setProgress(1);
+  },
+  _recalculateProgressFromSubreporters: function () {
+    if (this._subreporters.length == 0)
+      throw "Can't _recalculateProgressFromSubreporters on a progress reporter without any subreporters";
+    this._progress = 0;
+    for (var i = 0; i < this._subreporters.length; i++) {
+      var expectedDuration = this._subreporters[i].expectedDuration;
+      var reporter = this._subreporters[i].reporter;
+      this._progress += reporter.getProgress() * expectedDuration / this._subreporterExpectedDurationsSum;
+    }
+  },
+  _recalculateStateAndActionFromSubreporters: function () {
+    if (this._subreporters.length == 0)
+      throw "Can't _recalculateStateAndActionFromSubreporters on a progress reporter without any subreporters";
+    var actions = [];
+    var allWaiting = true;
+    var allFinished = true;
+    for (var i = 0; i < this._subreporters.length; i++) {
+      var expectedDuration = this._subreporters[i].expectedDuration;
+      var reporter = this._subreporters[i].reporter;
+      var state = reporter.getState();
+      if (state != ProgressReporter.STATE_WAITING)
+        allWaiting = false;
+      if (state != ProgressReporter.STATE_FINISHED)
+        allFinished = false;
+      if (state == ProgressReporter.STATE_DOING)
+        actions.push(reporter.getAction());
+    }
+    if (allFinished) {
+      this._transitionToFinished();
+    } else if (!allWaiting) {
+      this._state = ProgressReporter.STATE_DOING;
+      if (actions.length == 0) {
+        this._action = "About to start next task..."
+      } else {
+        this._action = actions.join("\n");
+      }
+    }
+  },
+  _transitionToFinished: function () {
+    this._state = ProgressReporter.STATE_FINISHED;
+
+    if (gDebugExpectedDurations) {
+      this._realDuration = Date.now() - this._startTime;
+      if (this._subreporters.length) {
+        for (var i = 0; i < this._subreporters.length; i++) {
+          var expectedDuration = this._subreporters[i].expectedDuration;
+          var reporter = this._subreporters[i].reporter;
+          var realDuration = reporter._realDuration;
+          dump("For reporter with expectedDuration " + expectedDuration + ", real duration was " + realDuration + "\n");
+        }
+      }
+    }
+  },
+  _reportProgress: function () {
+    for (var i = 0; i < this._observers.length; i++) {
+      this._observers[i](this);
+    }
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/devtools.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var gInstanceUID;
+
+/**
+ * Sends a message to the parent window with a status
+ * update.
+ *
+ * @param string status
+ *   Status to send to the parent page:
+ *    - loaded, when page is loaded.
+ *    - start, when user wants to start profiling.
+ *    - stop, when user wants to stop profiling.
+ */
+function notifyParent(status) {
+  if (!gInstanceUID) {
+    gInstanceUID = window.location.search.substr(1);
+  }
+
+  window.parent.postMessage({
+    uid: gInstanceUID,
+    status: status
+  }, "*");
+}
+
+/**
+ * A listener for incoming messages from the parent
+ * page. All incoming messages must be stringified
+ * JSON objects to be compatible with Cleopatra's
+ * format:
+ *
+ * {
+ *   task: string,
+ *   ...
+ * }
+ *
+ * This listener recognizes two tasks: onStarted and
+ * onStopped.
+ *
+ * @param object event
+ *   PostMessage event object.
+ */
+function onParentMessage(event) {
+  var start = document.getElementById("startWrapper");
+  var stop = document.getElementById("stopWrapper");
+  var msg = JSON.parse(event.data);
+
+  if (msg.task === "onStarted") {
+    start.style.display = "none";
+    start.querySelector("button").removeAttribute("disabled");
+    stop.style.display = "inline";
+  } else if (msg.task === "onStopped") {
+    stop.style.display = "none";
+    stop.querySelector("button").removeAttribute("disabled");
+    start.style.display = "inline";
+  }
+}
+
+window.addEventListener("message", onParentMessage);
+
+/**
+ * Main entry point. This function initializes Cleopatra
+ * in the light mode and creates all the UI we need.
+ */
+function initUI() {
+  gLightMode = true;
+  gJavaScriptOnly = true;
+
+  var container = document.createElement("div");
+  container.id = "ui";
+
+  gMainArea = document.createElement("div");
+  gMainArea.id = "mainarea";
+
+  container.appendChild(gMainArea);
+  document.body.appendChild(container);
+
+  var startButton = document.createElement("button");
+  startButton.innerHTML = "Start";
+  startButton.addEventListener("click", function (event) {
+    event.target.setAttribute("disabled", true);
+    notifyParent("start");
+  }, false);
+
+  var stopButton = document.createElement("button");
+  stopButton.innerHTML = "Stop";
+  stopButton.addEventListener("click", function (event) {
+    event.target.setAttribute("disabled", true);
+    notifyParent("stop");
+  }, false);
+
+  var controlPane = document.createElement("div");
+  controlPane.className = "controlPane";
+  controlPane.innerHTML =
+    "<p id='startWrapper'>Click <span class='btn'></span> to start profiling.</p>" +
+    "<p id='stopWrapper'>Click <span class='btn'></span> to stop profiling.</p>";
+
+  controlPane.querySelector("#startWrapper > span.btn").appendChild(startButton);
+  controlPane.querySelector("#stopWrapper > span.btn").appendChild(stopButton);
+
+  gMainArea.appendChild(controlPane);
+}
\ No newline at end of file
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/parser.js
@@ -0,0 +1,275 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Array.prototype.clone = function() { return this.slice(0); }
+
+function makeSample(frames, extraInfo, lines) {
+  return {
+    frames: frames,
+    extraInfo: extraInfo,
+    lines: lines
+  };
+}
+
+function cloneSample(sample) {
+  return makeSample(sample.frames.clone(), sample.extraInfo, sample.lines.clone());
+}
+
+function bucketsBySplittingArray(array, maxItemsPerBucket) {
+  var buckets = [];
+  while (buckets.length * maxItemsPerBucket < array.length) {
+    buckets.push(array.slice(buckets.length * maxItemsPerBucket,
+                             (buckets.length + 1) * maxItemsPerBucket));
+  }
+  return buckets;
+}
+
+var gParserWorker = new Worker("profiler/cleopatra/js/parserWorker.js");
+gParserWorker.nextRequestID = 0;
+
+function WorkerRequest(worker) {
+  var self = this;
+  this._eventListeners = {};
+  var requestID = worker.nextRequestID++;
+  this._requestID = requestID;
+  this._worker = worker;
+  this._totalReporter = new ProgressReporter();
+  this._totalReporter.addListener(function (reporter) {
+    self._fireEvent("progress", reporter.getProgress(), reporter.getAction());
+  })
+  this._sendChunkReporter = this._totalReporter.addSubreporter(500);
+  this._executeReporter = this._totalReporter.addSubreporter(3000);
+  this._receiveChunkReporter = this._totalReporter.addSubreporter(100);
+  this._totalReporter.begin("Processing task in worker...");
+  var partialResult = null;
+  function onMessageFromWorker(msg) {
+    pendingMessages.push(msg);
+    scheduleMessageProcessing();
+  }
+  function processMessage(msg) {
+    var startTime = Date.now();
+    var data = msg.data;
+    var readTime = Date.now() - startTime;
+    if (readTime > 10)
+      console.log("reading data from worker message: " + readTime + "ms");
+    if (data.requestID == requestID || !data.requestID) {
+      switch(data.type) {
+        case "error":
+          self._sendChunkReporter.setAction("Error in worker: " + data.error);
+          self._executeReporter.setAction("Error in worker: " + data.error);
+          self._receiveChunkReporter.setAction("Error in worker: " + data.error);
+          self._totalReporter.setAction("Error in worker: " + data.error);
+          PROFILERERROR("Error in worker: " + data.error);
+          self._fireEvent("error", data.error);
+          break;
+        case "progress":
+          self._executeReporter.setProgress(data.progress);
+          break;
+        case "finished":
+          self._executeReporter.finish();
+          self._receiveChunkReporter.begin("Receiving data from worker...");
+          self._receiveChunkReporter.finish();
+          self._fireEvent("finished", data.result);
+          worker.removeEventListener("message", onMessageFromWorker);
+          break;
+        case "finishedStart":
+          partialResult = null;
+          self._totalReceiveChunks = data.numChunks;
+          self._gotReceiveChunks = 0;
+          self._executeReporter.finish();
+          self._receiveChunkReporter.begin("Receiving data from worker...");
+          break;
+        case "finishedChunk":
+          partialResult = partialResult ? partialResult.concat(data.chunk) : data.chunk;
+          var chunkIndex = self._gotReceiveChunks++;
+          self._receiveChunkReporter.setProgress((chunkIndex + 1) / self._totalReceiveChunks);
+          break;
+        case "finishedEnd":
+          self._receiveChunkReporter.finish();
+          self._fireEvent("finished", partialResult);
+          worker.removeEventListener("message", onMessageFromWorker);
+          break;
+      }
+      // dump log if present
+      if (data.log) {
+        for (var line in data.log) {
+          PROFILERLOG(line);
+        }
+      }
+    }
+  }
+  var pendingMessages = [];
+  var messageProcessingTimer = 0;
+  function processMessages() {
+    messageProcessingTimer = 0;
+    processMessage(pendingMessages.shift());
+    if (pendingMessages.length)
+      scheduleMessageProcessing();
+  }
+  function scheduleMessageProcessing() {
+    if (messageProcessingTimer)
+      return;
+    messageProcessingTimer = setTimeout(processMessages, 10);
+  }
+  worker.addEventListener("message", onMessageFromWorker);
+}
+
+WorkerRequest.prototype = {
+  send: function WorkerRequest_send(task, taskData) {
+    this._sendChunkReporter.begin("Sending data to worker...");
+    var startTime = Date.now();
+    this._worker.postMessage({
+      requestID: this._requestID,
+      task: task,
+      taskData: taskData
+    });
+    var postTime = Date.now() - startTime;
+    if (true || postTime > 10)
+      console.log("posting message to worker: " + postTime + "ms");
+    this._sendChunkReporter.finish();
+    this._executeReporter.begin("Processing worker request...");
+  },
+  sendInChunks: function WorkerRequest_sendInChunks(task, taskData, params, maxChunkSize) {
+    this._sendChunkReporter.begin("Sending data to worker...");
+    var self = this;
+    var chunks = bucketsBySplittingArray(taskData, maxChunkSize);
+    var pendingMessages = [
+      {
+        requestID: this._requestID,
+        task: "chunkedStart",
+        numChunks: chunks.length
+      }
+    ].concat(chunks.map(function (chunk) {
+      return {
+        requestID: self._requestID,
+        task: "chunkedChunk",
+        chunk: chunk
+      };
+    })).concat([
+      {
+        requestID: this._requestID,
+        task: "chunkedEnd"
+      },
+      {
+        requestID: this._requestID,
+        params: params,
+        task: task
+      },
+    ]);
+    var totalMessages = pendingMessages.length;
+    var numSentMessages = 0;
+    function postMessage(msg) {
+      var msgIndex = numSentMessages++;
+      var startTime = Date.now();
+      self._worker.postMessage(msg);
+      var postTime = Date.now() - startTime;
+      if (postTime > 10)
+        console.log("posting message to worker: " + postTime + "ms");
+      self._sendChunkReporter.setProgress((msgIndex + 1) / totalMessages);
+    }
+    var messagePostingTimer = 0;
+    function postMessages() {
+      messagePostingTimer = 0;
+      postMessage(pendingMessages.shift());
+      if (pendingMessages.length) {
+        scheduleMessagePosting();
+      } else {
+        self._sendChunkReporter.finish();
+        self._executeReporter.begin("Processing worker request...");
+      }
+    }
+    function scheduleMessagePosting() {
+      if (messagePostingTimer)
+        return;
+      messagePostingTimer = setTimeout(postMessages, 10);
+    }
+    scheduleMessagePosting();
+  },
+
+  // TODO: share code with TreeView
+  addEventListener: function WorkerRequest_addEventListener(eventName, callbackFunction) {
+    if (!(eventName in this._eventListeners))
+      this._eventListeners[eventName] = [];
+    if (this._eventListeners[eventName].indexOf(callbackFunction) != -1)
+      return;
+    this._eventListeners[eventName].push(callbackFunction);
+  },
+  removeEventListener: function WorkerRequest_removeEventListener(eventName, callbackFunction) {
+    if (!(eventName in this._eventListeners))
+      return;
+    var index = this._eventListeners[eventName].indexOf(callbackFunction);
+    if (index == -1)
+      return;
+    this._eventListeners[eventName].splice(index, 1);
+  },
+  _fireEvent: function WorkerRequest__fireEvent(eventName, eventObject, p1) {
+    if (!(eventName in this._eventListeners))
+      return;
+    this._eventListeners[eventName].forEach(function (callbackFunction) {
+      callbackFunction(eventObject, p1);
+    });
+  },
+}
+
+var Parser = {
+  parse: function Parser_parse(data, params) {
+    console.log("profile num chars: " + data.length);
+    var request = new WorkerRequest(gParserWorker);
+    request.sendInChunks("parseRawProfile", data, params, 3000000);
+    return request;
+  },
+
+  updateFilters: function Parser_updateFilters(filters) {
+    var request = new WorkerRequest(gParserWorker);
+    request.send("updateFilters", {
+      filters: filters,
+      profileID: 0
+    });
+    return request;
+  },
+
+  updateViewOptions: function Parser_updateViewOptions(options) {
+    var request = new WorkerRequest(gParserWorker);
+    request.send("updateViewOptions", {
+      options: options,
+      profileID: 0
+    });
+    return request;
+  },
+
+  getSerializedProfile: function Parser_getSerializedProfile(complete, callback) {
+    var request = new WorkerRequest(gParserWorker);
+    request.send("getSerializedProfile", {
+      profileID: 0,
+      complete: complete
+    });
+    request.addEventListener("finished", callback);
+  },
+
+  calculateHistogramData: function Parser_calculateHistogramData() {
+    var request = new WorkerRequest(gParserWorker);
+    request.send("calculateHistogramData", {
+      profileID: 0
+    });
+    return request;
+  },
+
+  calculateDiagnosticItems: function Parser_calculateDiagnosticItems(meta) {
+    var request = new WorkerRequest(gParserWorker);
+    request.send("calculateDiagnosticItems", {
+      profileID: 0,
+      meta: meta
+    });
+    return request;
+  },
+
+  updateLogSetting: function Parser_updateLogSetting() {
+    var request = new WorkerRequest(gParserWorker);
+    request.send("initWorker", {
+      debugLog: gDebugLog,
+      debugTrace: gDebugTrace,
+    });
+    return request;
+  },
+};
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/parserWorker.js
@@ -0,0 +1,1499 @@
+/* -*- Mode: js2; indent-tabs-mode: nil; js2-basic-offset: 2; -*- */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+importScripts("ProgressReporter.js");
+
+var gProfiles = [];
+
+var partialTaskData = {};
+
+var gNextProfileID = 0;
+
+var gLogLines = [];
+
+var gDebugLog = false;
+var gDebugTrace = false;
+// Use for verbose tracing, otherwise use log
+function PROFILDERTRACE(msg) {
+  if (gDebugTrace)
+    PROFILERLOG(msg);
+}
+function PROFILERLOG(msg) {
+  if (gDebugLog) {
+    msg = "Cleo: " + msg;
+    //if (window.dump)
+    //  window.dump(msg + "\n");
+  }
+}
+function PROFILERERROR(msg) {
+  msg = "Cleo: " + msg;
+  //if (window.dump)
+  //  window.dump(msg + "\n");
+}
+
+// http://stackoverflow.com/a/2548133
+function endsWith(str, suffix) {
+      return str.indexOf(suffix, this.length - suffix.length) !== -1;
+};
+
+// https://bugzilla.mozilla.org/show_bug.cgi?id=728780
+if (!String.prototype.startsWith) {
+  String.prototype.startsWith =
+    function(s) { return this.lastIndexOf(s, 0) === 0; }
+}
+
+// functions for which lr is unconditionally valid.  These are
+// largely going to be atomics and other similar functions
+// that don't touch lr.  This is currently populated with
+// some functions from bionic, largely via manual inspection
+// of the assembly in e.g.
+// http://androidxref.com/source/xref/bionic/libc/arch-arm/syscalls/
+var sARMFunctionsWithValidLR = [
+  "__atomic_dec",
+  "__atomic_inc",
+  "__atomic_cmpxchg",
+  "__atomic_swap",
+  "__atomic_dec",
+  "__atomic_inc",
+  "__atomic_cmpxchg",
+  "__atomic_swap",
+  "__futex_syscall3",
+  "__futex_wait",
+  "__futex_wake",
+  "__futex_syscall3",
+  "__futex_wait",
+  "__futex_wake",
+  "__futex_syscall4",
+  "__ioctl",
+  "__brk",
+  "__wait4",
+  "epoll_wait",
+  "fsync",
+  "futex",
+  "nanosleep",
+  "pause",
+  "sched_yield",
+  "syscall"
+];
+
+function log() {
+  var z = [];
+  for (var i = 0; i < arguments.length; ++i)
+    z.push(arguments[i]);
+  gLogLines.push(z.join(" "));
+}
+
+self.onmessage = function (msg) {
+  try {
+    var requestID = msg.data.requestID;
+    var task = msg.data.task;
+    var taskData = msg.data.taskData;
+    if (!taskData &&
+        (["chunkedStart", "chunkedChunk", "chunkedEnd"].indexOf(task) == -1)) {
+      taskData = partialTaskData[requestID];
+      delete partialTaskData[requestID];
+    }
+    PROFILERLOG("Start task: " + task);
+
+    gLogLines = [];
+
+    switch (task) {
+      case "initWorker":
+        gDebugLog = taskData.debugLog;
+        gDebugTrace = taskData.debugTrace;
+        PROFILERLOG("Init logging in parserWorker");
+        return;
+      case "chunkedStart":
+        partialTaskData[requestID] = null;
+        break;
+      case "chunkedChunk":
+        if (partialTaskData[requestID] === null)
+          partialTaskData[requestID] = msg.data.chunk;
+        else
+          partialTaskData[requestID] = partialTaskData[requestID].concat(msg.data.chunk);
+        break;
+      case "chunkedEnd":
+        break;
+      case "parseRawProfile":
+        parseRawProfile(requestID, msg.data.params, taskData);
+        break;
+      case "updateFilters":
+        updateFilters(requestID, taskData.profileID, taskData.filters);
+        break;
+      case "updateViewOptions":
+        updateViewOptions(requestID, taskData.profileID, taskData.options);
+        break;
+      case "getSerializedProfile":
+        getSerializedProfile(requestID, taskData.profileID, taskData.complete);
+        break;
+      case "calculateHistogramData":
+        calculateHistogramData(requestID, taskData.profileID);
+        break;
+      case "calculateDiagnosticItems":
+        calculateDiagnosticItems(requestID, taskData.profileID, taskData.meta);
+        break;
+      default:
+        sendError(requestID, "Unknown task " + task);
+        break;
+    }
+    PROFILERLOG("Complete task: " + task);
+  } catch (e) {
+    PROFILERERROR("Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")");
+    sendError(requestID, "Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")");
+  }
+}
+
+function sendError(requestID, error) {
+  // support sendError(msg)
+  if (error == null) {
+    error = requestID;
+    requestID = null;
+  }
+
+  self.postMessage({
+    requestID: requestID,
+    type: "error",
+    error: error,
+    log: gLogLines
+  });
+}
+
+function sendProgress(requestID, progress) {
+  self.postMessage({
+    requestID: requestID,
+    type: "progress",
+    progress: progress
+  });
+}
+
+function sendFinished(requestID, result) {
+  self.postMessage({
+    requestID: requestID,
+    type: "finished",
+    result: result,
+    log: gLogLines
+  });
+}
+
+function bucketsBySplittingArray(array, maxCostPerBucket, costOfElementCallback) {
+  var buckets = [];
+  var currentBucket = [];
+  var currentBucketCost = 0;
+  for (var i = 0; i < array.length; i++) {
+    var element = array[i];
+    var costOfCurrentElement = costOfElementCallback ? costOfElementCallback(element) : 1;
+    if (currentBucketCost + costOfCurrentElement > maxCostPerBucket) {
+      buckets.push(currentBucket);
+      currentBucket = [];
+      currentBucketCost = 0;
+    }
+    currentBucket.push(element);
+    currentBucketCost += costOfCurrentElement;
+  }
+  buckets.push(currentBucket);
+  return buckets;
+}
+
+function sendFinishedInChunks(requestID, result, maxChunkCost, costOfElementCallback) {
+  if (result.length === undefined || result.slice === undefined)
+    throw new Error("Can't slice result into chunks");
+  self.postMessage({
+    requestID: requestID,
+    type: "finishedStart"
+  });
+  var chunks = bucketsBySplittingArray(result, maxChunkCost, costOfElementCallback);
+  for (var i = 0; i < chunks.length; i++) {
+    self.postMessage({
+      requestID: requestID,
+      type: "finishedChunk",
+      chunk: chunks[i]
+    });
+  }
+  self.postMessage({
+    requestID: requestID,
+    type: "finishedEnd",
+    log: gLogLines
+  });
+}
+
+function makeSample(frames, extraInfo) {
+  return {
+    frames: frames,
+    extraInfo: extraInfo
+  };
+}
+
+function cloneSample(sample) {
+  return makeSample(sample.frames.slice(0), sample.extraInfo);
+}
+function parseRawProfile(requestID, params, rawProfile) {
+  var progressReporter = new ProgressReporter();
+  progressReporter.addListener(function (r) {
+    sendProgress(requestID, r.getProgress());
+  });
+  progressReporter.begin("Parsing...");
+
+  var symbolicationTable = {};
+  var symbols = [];
+  var symbolIndices = {};
+  var functions = [];
+  var functionIndices = {};
+  var samples = [];
+  var meta = {};
+  var armIncludePCIndex = {};
+
+  if (typeof rawProfile == "string" && rawProfile[0] == "{") {
+    // rawProfile is a JSON string.
+    rawProfile = JSON.parse(rawProfile);
+  }
+
+  if (rawProfile.profileJSON && !rawProfile.profileJSON.meta && rawProfile.meta) {
+    rawProfile.profileJSON.meta = rawProfile.meta;
+  }
+
+  if (typeof rawProfile == "object") {
+    switch (rawProfile.format) {
+      case "profileStringWithSymbolicationTable,1":
+        symbolicationTable = rawProfile.symbolicationTable;
+        parseProfileString(rawProfile.profileString);
+        break;
+      case "profileJSONWithSymbolicationTable,1":
+        symbolicationTable = rawProfile.symbolicationTable;
+        parseProfileJSON(rawProfile.profileJSON);
+        break;
+      default:
+        parseProfileJSON(rawProfile);
+    }
+  } else {
+    parseProfileString(rawProfile);
+  }
+
+  function cleanFunctionName(functionName) {
+    var ignoredPrefix = "non-virtual thunk to ";
+    if (functionName.substr(0, ignoredPrefix.length) == ignoredPrefix)
+      return functionName.substr(ignoredPrefix.length);
+    return functionName;
+  }
+
+  function resourceNameForAddon(addonID) {
+    for (var i in meta.addons) {
+      var addon = meta.addons[i];
+      if (addon.id.toLowerCase() == addonID.toLowerCase()) {
+        var iconHTML = "";
+        if (addon.iconURL)
+          iconHTML = "<img src=\"" + addon.iconURL + "\" style='width:12px; height:12px;'> "
+        return iconHTML + " " + (/@jetpack$/.exec(addonID) ? "Jetpack: " : "") + addon.name;
+      }
+    }
+    return "";
+  }
+
+  function parseResourceName(url) {
+    if (!url) {
+      return "No URL";
+    }
+    if (url.startsWith("resource:///")) {
+      // Take the last URL from a chained list of URLs.
+      var urls = url.split(" -> ");
+      url = urls[urls.length - 1];
+    }
+
+    // TODO Fix me, this certainly doesn't handle all URLs formats
+    var match = /^.*:\/\/(.*?)\/.*$/.exec(url);
+
+    if (!match)
+      return url;
+
+    var host = match[1];
+
+    if (meta && meta.addons) {
+      if (url.startsWith("resource:") && endsWith(host, "-at-jetpack")) {
+        // Assume this is a jetpack url
+        var jetpackID = host.substring(0, host.length - 11) + "@jetpack";
+        var resName = resourceNameForAddon(jetpackID);
+        if (resName)
+          return resName;
+      }
+      if (url.startsWith("file:///") && url.indexOf("/extensions/") != -1) {
+        var unpackedAddonNameMatch = /\/extensions\/(.*?)\//.exec(url);
+        if (unpackedAddonNameMatch) {
+          var resName = resourceNameForAddon(decodeURIComponent(unpackedAddonNameMatch[1]));
+          if (resName)
+            return resName;
+        }
+      }
+      if (url.startsWith("jar:file:///") && url.indexOf("/extensions/") != -1) {
+        var packedAddonNameMatch = /\/extensions\/(.*?).xpi/.exec(url);
+        if (packedAddonNameMatch) {
+          var resName = resourceNameForAddon(decodeURIComponent(packedAddonNameMatch[1]));
+          if (resName)
+            return resName;
+        }
+      }
+    }
+
+    var iconHTML = "";
+    if (url.indexOf("http://") == 0) {
+      iconHTML = "<img src=\"http://" + host + "/favicon.ico\" style='width:12px; height:12px;'> ";
+    } else if (url.indexOf("https://") == 0) {
+      iconHTML = "<img src=\"https://" + host + "/favicon.ico\" style='width:12px; height:12px;'> ";
+    }
+    return iconHTML + host;
+  }
+
+  function parseScriptFile(url) {
+     // TODO Fix me, this certainly doesn't handle all URLs formats
+     var match = /^.*\/(.*)\.js$/.exec(url);
+
+     if (!match)
+       return url;
+
+     return match[1] + ".js";
+  }
+
+  function parseScriptURI(url) {
+    if (url) {
+      var urlTokens = url.split(" ");
+      url = urlTokens[urlTokens.length-1];
+    }
+    return url;
+  }
+
+  function getFunctionInfo(fullName) {
+    var isJSFrame = false;
+    var match =
+      /^(.*) \(in ([^\)]*)\) (\+ [0-9]+)$/.exec(fullName) ||
+      /^(.*) \(in ([^\)]*)\) (\(.*:.*\))$/.exec(fullName) ||
+      /^(.*) \(in ([^\)]*)\)$/.exec(fullName);
+      // Try to parse a JS frame
+    var scriptLocation = null;
+    var jsMatch1 = match ||
+      /^(.*) \((.*):([0-9]+)\)$/.exec(fullName);
+    if (!match && jsMatch1) {
+      scriptLocation = {
+        scriptURI: parseScriptURI(jsMatch1[2]),
+        lineInformation: jsMatch1[3]
+      };
+      match = [0, jsMatch1[1]+"() @ "+parseScriptFile(jsMatch1[2]) + ":" + jsMatch1[3], parseResourceName(jsMatch1[2]), ""];
+      isJSFrame = true;
+    }
+    var jsMatch2 = match ||
+      /^(.*):([0-9]+)$/.exec(fullName);
+    if (!match && jsMatch2) {
+      scriptLocation = {
+        scriptURI: parseScriptURI(jsMatch2[1]),
+        lineInformation: jsMatch2[2]
+      };
+      match = [0, "<Anonymous> @ "+parseScriptFile(jsMatch2[1]) + ":" + jsMatch2[2], parseResourceName(jsMatch2[1]), ""];
+      isJSFrame = true;
+    }
+    if (!match) {
+      match = [fullName, fullName];
+    }
+    return {
+      functionName: cleanFunctionName(match[1]),
+      libraryName: match[2] || "",
+      lineInformation: match[3] || "",
+      isJSFrame: isJSFrame,
+      scriptLocation: scriptLocation
+    };
+  }
+
+  function indexForFunction(symbol, functionName, libraryName, isJSFrame, scriptLocation) {
+    var resolve = functionName+"_LIBNAME_"+libraryName;
+    if (resolve in functionIndices)
+      return functionIndices[resolve];
+    var newIndex = functions.length;
+    functions[newIndex] = {
+      symbol: symbol,
+      functionName: functionName,
+      libraryName: libraryName,
+      isJSFrame: isJSFrame,
+      scriptLocation: scriptLocation
+    };
+    functionIndices[resolve] = newIndex;
+    return newIndex;
+  }
+
+  function parseSymbol(symbol) {
+    var info = getFunctionInfo(symbol);
+    //dump("Parse symbol: " + symbol + "\n");
+    return {
+      symbolName: symbol,
+      functionName: info.functionName,
+      functionIndex: indexForFunction(symbol, info.functionName, info.libraryName, info.isJSFrame, info.scriptLocation),
+      lineInformation: info.lineInformation,
+      isJSFrame: info.isJSFrame,
+      scriptLocation: info.scriptLocation
+    };
+  }
+
+  function translatedSymbol(symbol) {
+    return symbolicationTable[symbol] || symbol;
+  }
+
+  function indexForSymbol(symbol) {
+    if (symbol in symbolIndices)
+      return symbolIndices[symbol];
+    var newIndex = symbols.length;
+    symbols[newIndex] = parseSymbol(translatedSymbol(symbol));
+    symbolIndices[symbol] = newIndex;
+    return newIndex;
+  }
+
+  function clearRegExpLastMatch() {
+    /./.exec(" ");
+  }
+
+  function shouldIncludeARMLRForPC(pcIndex) {
+    if (pcIndex in armIncludePCIndex)
+      return armIncludePCIndex[pcIndex];
+
+    var pcName = symbols[pcIndex].functionName;
+    var include = sARMFunctionsWithValidLR.indexOf(pcName) != -1;
+    armIncludePCIndex[pcIndex] = include;
+    return include;
+  }
+
+  function parseProfileString(data) {
+    var extraInfo = {};
+    var lines = data.split("\n");
+    var sample = null;
+    for (var i = 0; i < lines.length; ++i) {
+      var line = lines[i];
+      if (line.length < 2 || line[1] != '-') {
+        // invalid line, ignore it
+        continue;
+      }
+      var info = line.substring(2);
+      switch (line[0]) {
+      //case 'l':
+      //  // leaf name
+      //  if ("leafName" in extraInfo) {
+      //    extraInfo.leafName += ":" + info;
+      //  } else {
+      //    extraInfo.leafName = info;
+      //  }
+      //  break;
+      case 'm':
+        // marker
+        if (!("marker" in extraInfo)) {
+          extraInfo.marker = [];
+        }
+        extraInfo.marker.push(info);
+        break;
+      case 's':
+        // sample
+        var sampleName = info;
+        sample = makeSample([indexForSymbol(sampleName)], extraInfo);
+        samples.push(sample);
+        extraInfo = {}; // reset the extra info for future rounds
+        break;
+      case 'c':
+      case 'l':
+        // continue sample
+        if (sample) { // ignore the case where we see a 'c' before an 's'
+          sample.frames.push(indexForSymbol(info));
+        }
+        break;
+      case 'L':
+        // continue sample; this is an ARM LR record.  Stick it before the
+        // PC if it's one of the functions where we know LR is good.
+        if (sample && sample.frames.length > 1) {
+          var pcIndex = sample.frames[sample.frames.length - 1];
+          if (shouldIncludeARMLRForPC(pcIndex)) {
+            sample.frames.splice(-1, 0, indexForSymbol(info));
+          }
+        }
+        break;
+      case 't':
+        // time
+        if (sample) {
+          sample.extraInfo["time"] = parseFloat(info);
+        }
+        break;
+      case 'r':
+        // responsiveness
+        if (sample) {
+          sample.extraInfo["responsiveness"] = parseFloat(info);
+        }
+        break;
+      }
+      progressReporter.setProgress((i + 1) / lines.length);
+    }
+  }
+
+  function parseProfileJSON(profile) {
+    // Thread 0 will always be the main thread of interest
+    // TODO support all the thread in the profile
+    var profileSamples = null;
+    meta = profile.meta || {};
+    if (params.appendVideoCapture) {
+      meta.videoCapture = {
+        src: params.appendVideoCapture,
+      };
+    }
+    // Support older format that aren't thread aware
+    if (profile.threads != null) {
+      profileSamples = profile.threads[0].samples;
+    } else {
+      profileSamples = profile;
+    }
+    var rootSymbol = null;
+    var insertCommonRoot = false;
+    var frameStart = {};
+    meta.frameStart = frameStart;
+    for (var j = 0; j < profileSamples.length; j++) {
+      var sample = profileSamples[j];
+      var indicedFrames = [];
+      if (!sample) {
+        // This sample was filtered before saving
+        samples.push(null);
+        progressReporter.setProgress((j + 1) / profileSamples.length);
+        continue;
+      }
+      for (var k = 0; sample.frames && k < sample.frames.length; k++) {
+        var frame = sample.frames[k];
+        var pcIndex;
+        if (frame.location !== undefined) {
+          pcIndex = indexForSymbol(frame.location);
+        } else {
+          pcIndex = indexForSymbol(frame);
+        }
+
+        if (frame.lr !== undefined && shouldIncludeARMLRForPC(pcIndex)) {
+          indicedFrames.push(indexForSymbol(frame.lr));
+        }
+
+        indicedFrames.push(pcIndex);
+      }
+      if (indicedFrames.length >= 1) {
+        if (rootSymbol && rootSymbol != indicedFrames[0]) {
+          insertCommonRoot = true;
+        }
+        rootSymbol = rootSymbol || indicedFrames[0];
+      }
+      if (sample.extraInfo == null) {
+        sample.extraInfo = {};
+      }
+      if (sample.responsiveness) {
+        sample.extraInfo["responsiveness"] = sample.responsiveness;
+      }
+      if (sample.time) {
+        sample.extraInfo["time"] = sample.time;
+      }
+      if (sample.frameNumber) {
+        sample.extraInfo["frameNumber"] = sample.frameNumber;
+        //dump("Got frame number: " + sample.frameNumber + "\n");
+        frameStart[sample.frameNumber] = samples.length;
+      }
+      samples.push(makeSample(indicedFrames, sample.extraInfo));
+      progressReporter.setProgress((j + 1) / profileSamples.length);
+    }
+    if (insertCommonRoot) {
+      var rootIndex = indexForSymbol("(root)");
+      for (var i = 0; i < samples.length; i++) {
+        var sample = samples[i];
+        if (!sample) continue;
+        // If length == 0 then the sample was filtered when saving the profile
+        if (sample.frames.length >= 1 && sample.frames[0] != rootIndex)
+          sample.frames.splice(0, 0, rootIndex)
+      }
+    }
+  }
+
+  progressReporter.finish();
+  var profileID = gNextProfileID++;
+  gProfiles[profileID] = JSON.parse(JSON.stringify({
+    meta: meta,
+    symbols: symbols,
+    functions: functions,
+    allSamples: samples
+  }));
+  clearRegExpLastMatch();
+  sendFinished(requestID, {
+    meta: meta,
+    numSamples: samples.length,
+    profileID: profileID,
+    symbols: symbols,
+    functions: functions
+  });
+}
+
+function getSerializedProfile(requestID, profileID, complete) {
+  var profile = gProfiles[profileID];
+  var symbolicationTable = {};
+  if (complete || !profile.filterSettings.mergeFunctions) {
+    for (var symbolIndex in profile.symbols) {
+      symbolicationTable[symbolIndex] = profile.symbols[symbolIndex].symbolName;
+    }
+  } else {
+    for (var functionIndex in profile.functions) {
+      var f = profile.functions[functionIndex];
+      symbolicationTable[functionIndex] = f.symbol;
+    }
+  }
+  var serializedProfile = JSON.stringify({
+    format: "profileJSONWithSymbolicationTable,1",
+    meta: profile.meta,
+    profileJSON: complete ? profile.allSamples : profile.filteredSamples,
+    symbolicationTable: symbolicationTable
+  });
+  sendFinished(requestID, serializedProfile);
+}
+
+function TreeNode(name, parent, startCount) {
+  this.name = name;
+  this.children = [];
+  this.counter = startCount;
+  this.parent = parent;
+}
+TreeNode.prototype.getDepth = function TreeNode__getDepth() {
+  if (this.parent)
+    return this.parent.getDepth() + 1;
+  return 0;
+};
+TreeNode.prototype.findChild = function TreeNode_findChild(name) {
+  for (var i = 0; i < this.children.length; i++) {
+    var child = this.children[i];
+    if (child.name == name)
+      return child;
+  }
+  return null;
+}
+// path is an array of strings which is matched to our nodes' names.
+// Try to walk path in our own tree and return the last matching node. The
+// length of the match can be calculated by the caller by comparing the
+// returned node's depth with the depth of the path's start node.
+TreeNode.prototype.followPath = function TreeNode_followPath(path) {
+  if (path.length == 0)
+    return this;
+
+  var matchingChild = this.findChild(path[0]);
+  if (!matchingChild)
+    return this;
+
+  return matchingChild.followPath(path.slice(1));
+};
+TreeNode.prototype.incrementCountersInParentChain = function TreeNode_incrementCountersInParentChain() {
+  this.counter++;
+  if (this.parent)
+    this.parent.incrementCountersInParentChain();
+};
+
+function convertToCallTree(samples, isReverse) {
+  function areSamplesMultiroot(samples) {
+    var previousRoot;
+    for (var i = 0; i < samples.length; ++i) {
+      if (!samples[i].frames) continue;
+      if (!previousRoot) {
+        previousRoot = samples[i].frames[0];
+        continue;
+      }
+      if (previousRoot != samples[i].frames[0]) {
+        return true;
+      }
+    }
+    return false;
+  }
+  samples = samples.filter(function noNullSamples(sample) {
+    return sample != null;
+  });
+  if (samples.length == 0)
+    return new TreeNode("(empty)", null, 0);
+  var firstRoot = null;
+  for (var i = 0; i < samples.length; ++i) {
+    if (!samples[i].frames) continue;
+    sendError(null, "got root: " + samples[i].frames[0]);
+    firstRoot = samples[i].frames[0];
+    break;
+  }
+  if (firstRoot == null) {
+    return new TreeNode("(all filtered)", null, 0);
+  }
+  var multiRoot = areSamplesMultiroot(samples);
+  var treeRoot = new TreeNode((isReverse || multiRoot) ? "(total)" : firstRoot, null, 0);
+  for (var i = 0; i < samples.length; ++i) {
+    var sample = samples[i];
+    if (!sample.frames) {
+      continue;
+    }
+    var callstack = sample.frames.slice(0);
+    callstack.shift();
+    if (isReverse)
+      callstack.reverse();
+    var deepestExistingNode = treeRoot.followPath(callstack);
+    var remainingCallstack = callstack.slice(deepestExistingNode.getDepth());
+    deepestExistingNode.incrementCountersInParentChain();
+    var node = deepestExistingNode;
+    for (var j = 0; j < remainingCallstack.length; ++j) {
+      var frame = remainingCallstack[j];
+      var child = new TreeNode(frame, node, 1);
+      node.children.push(child);
+      node = child;
+    }
+  }
+  return treeRoot;
+}
+
+function filterByJank(samples, filterThreshold) {
+  return samples.map(function nullNonJank(sample) {
+    if (!sample ||
+        !("responsiveness" in sample.extraInfo) ||
+        sample.extraInfo["responsiveness"] < filterThreshold)
+      return null;
+    return sample;
+  });
+}
+
+function filterBySymbol(samples, symbolOrFunctionIndex) {
+  return samples.map(function filterSample(origSample) {
+    if (!origSample)
+      return null;
+    var sample = cloneSample(origSample);
+    for (var i = 0; i < sample.frames.length; i++) {
+      if (symbolOrFunctionIndex == sample.frames[i]) {
+        sample.frames = sample.frames.slice(i);
+        return sample;
+      }
+    }
+    return null; // no frame matched; filter out complete sample
+  });
+}
+
+function filterByCallstackPrefix(samples, callstack) {
+  return samples.map(function filterSample(origSample) {
+    if (!origSample)
+      return null;
+    if (origSample.frames.length < callstack.length)
+      return null;
+    var sample = cloneSample(origSample);
+    for (var i = 0; i < callstack.length; i++) {
+      if (sample.frames[i] != callstack[i])
+        return null;
+    }
+    sample.frames = sample.frames.slice(callstack.length - 1);
+    return sample;
+  });
+}
+
+function filterByCallstackPostfix(samples, callstack) {
+  return samples.map(function filterSample(origSample) {
+    if (!origSample)
+      return null;
+    if (origSample.frames.length < callstack.length)
+      return null;
+    var sample = cloneSample(origSample);
+    for (var i = 0; i < callstack.length; i++) {
+      if (sample.frames[sample.frames.length - i - 1] != callstack[i])
+        return null;
+    }
+    sample.frames = sample.frames.slice(0, sample.frames.length - callstack.length + 1);
+    return sample;
+  });
+}
+
+function chargeNonJSToCallers(samples, symbols, functions, useFunctions) {
+  function isJSFrame(index, useFunction) {
+    if (useFunctions) {
+      if (!(index in functions))
+        return "";
+      return functions[index].isJSFrame;
+    }
+    if (!(index in symbols))
+      return "";
+    return symbols[index].isJSFrame;
+  }
+  samples = samples.slice(0);
+  for (var i = 0; i < samples.length; ++i) {
+    var sample = samples[i];
+    if (!sample)
+      continue;
+    var callstack = sample.frames;
+    var newFrames = [];
+    for (var j = 0; j < callstack.length; ++j) {
+      if (isJSFrame(callstack[j], useFunctions)) {
+        // Record Javascript frames
+        newFrames.push(callstack[j]);
+      }
+    }
+    if (!newFrames.length) {
+      newFrames = null;
+    } else {
+      newFrames.splice(0, 0, "(total)");
+    }
+    samples[i].frames = newFrames;
+  }
+  return samples;
+}
+
+function filterByName(samples, symbols, functions, filterName, useFunctions) {
+  function getSymbolOrFunctionName(index, useFunctions) {
+    if (useFunctions) {
+      if (!(index in functions))
+        return "";
+      return functions[index].functionName;
+    }
+    if (!(index in symbols))
+      return "";
+    return symbols[index].symbolName;
+  }
+  function getLibraryName(index, useFunctions) {
+    if (useFunctions) {
+      if (!(index in functions))
+        return "";
+      return functions[index].libraryName;
+    }
+    if (!(index in symbols))
+      return "";
+    return symbols[index].libraryName;
+  }
+  samples = samples.slice(0);
+  filterName = filterName.toLowerCase();
+  calltrace_it: for (var i = 0; i < samples.length; ++i) {
+    var sample = samples[i];
+    if (!sample)
+      continue;
+    var callstack = sample.frames;
+    for (var j = 0; j < callstack.length; ++j) { 
+      var symbolOrFunctionName = getSymbolOrFunctionName(callstack[j], useFunctions);
+      var libraryName = getLibraryName(callstack[j], useFunctions);
+      if (symbolOrFunctionName.toLowerCase().indexOf(filterName) != -1 || 
+          libraryName.toLowerCase().indexOf(filterName) != -1) {
+        continue calltrace_it;
+      }
+    }
+    samples[i] = null;
+  }
+  return samples;
+}
+
+function discardLineLevelInformation(samples, symbols, functions) {
+  var data = samples;
+  var filteredData = [];
+  for (var i = 0; i < data.length; i++) {
+    if (!data[i]) {
+      filteredData.push(null);
+      continue;
+    }
+    filteredData.push(cloneSample(data[i]));
+    var frames = filteredData[i].frames;
+    for (var j = 0; j < frames.length; j++) {
+      if (!(frames[j] in symbols))
+        continue;
+      frames[j] = symbols[frames[j]].functionIndex;
+    }
+  }
+  return filteredData;
+}
+
+function mergeUnbranchedCallPaths(root) {
+  var mergedNames = [root.name];
+  var node = root;
+  while (node.children.length == 1 && node.counter == node.children[0].counter) {
+    node = node.children[0];
+    mergedNames.push(node.name);
+  }
+  if (node != root) {
+    // Merge path from root to node into root.
+    root.children = node.children;
+    root.mergedNames = mergedNames;
+    //root.name = clipText(root.name, 50) + " to " + this._clipText(node.name, 50);
+  }
+  for (var i = 0; i < root.children.length; i++) {
+    mergeUnbranchedCallPaths(root.children[i]);
+  }
+}
+
+function FocusedFrameSampleFilter(focusedSymbol) {
+  this._focusedSymbol = focusedSymbol;
+}
+FocusedFrameSampleFilter.prototype = {
+  filter: function FocusedFrameSampleFilter_filter(samples, symbols, functions) {
+    return filterBySymbol(samples, this._focusedSymbol);
+  }
+};
+
+function FocusedCallstackPrefixSampleFilter(focusedCallstack) {
+  this._focusedCallstackPrefix = focusedCallstack;
+}
+FocusedCallstackPrefixSampleFilter.prototype = {
+  filter: function FocusedCallstackPrefixSampleFilter_filter(samples, symbols, functions) {
+    return filterByCallstackPrefix(samples, this._focusedCallstackPrefix);
+  }
+};
+
+function FocusedCallstackPostfixSampleFilter(focusedCallstack) {
+  this._focusedCallstackPostfix = focusedCallstack;
+}
+FocusedCallstackPostfixSampleFilter.prototype = {
+  filter: function FocusedCallstackPostfixSampleFilter_filter(samples, symbols, functions) {
+    return filterByCallstackPostfix(samples, this._focusedCallstackPostfix);
+  }
+};
+
+function RangeSampleFilter(start, end) {
+  this._start = start;
+  this._end = end;
+}
+RangeSampleFilter.prototype = {
+  filter: function RangeSampleFilter_filter(samples, symbols, functions) {
+    return samples.slice(this._start, this._end);
+  }
+}
+
+function unserializeSampleFilters(filters) {
+  return filters.map(function (filter) {
+    switch (filter.type) {
+      case "FocusedFrameSampleFilter":
+        return new FocusedFrameSampleFilter(filter.focusedSymbol);
+      case "FocusedCallstackPrefixSampleFilter":
+        return new FocusedCallstackPrefixSampleFilter(filter.focusedCallstack);
+      case "FocusedCallstackPostfixSampleFilter":
+        return new FocusedCallstackPostfixSampleFilter(filter.focusedCallstack);
+      case "RangeSampleFilter":
+        return new RangeSampleFilter(filter.start, filter.end);
+      case "PluginView":
+        return null;
+      default:
+        throw new Error("Unknown filter");
+    }
+  })
+}
+
+var gJankThreshold = 50 /* ms */;
+
+function updateFilters(requestID, profileID, filters) {
+  var profile = gProfiles[profileID];
+  var samples = profile.allSamples;
+  var symbols = profile.symbols;
+  var functions = profile.functions;
+
+  if (filters.mergeFunctions) {
+    samples = discardLineLevelInformation(samples, symbols, functions);
+  }
+  if (filters.javascriptOnly) {
+    try {
+      //samples = filterByName(samples, symbols, functions, "runScript", filters.mergeFunctions);
+      samples = chargeNonJSToCallers(samples, symbols, functions, filters.mergeFunctions);
+    } catch (e) {
+      dump("Could not filer by javascript: " + e + "\n");
+    }
+  }
+  if (filters.nameFilter) {
+    try {
+      samples = filterByName(samples, symbols, functions, filters.nameFilter, filters.mergeFunctions);
+    } catch (e) {
+      dump("Could not filer by name: " + e + "\n");
+    }
+  }
+  samples = unserializeSampleFilters(filters.sampleFilters).reduce(function (filteredSamples, currentFilter) {
+    if (currentFilter===null) return filteredSamples;
+    return currentFilter.filter(filteredSamples, symbols, functions);
+  }, samples);
+  if (filters.jankOnly) {
+    samples = filterByJank(samples, gJankThreshold);
+  }
+
+  gProfiles[profileID].filterSettings = filters;
+  gProfiles[profileID].filteredSamples = samples;
+  sendFinishedInChunks(requestID, samples, 40000,
+                       function (sample) { return (sample && sample.frames) ? sample.frames.length : 1; });
+}
+
+function updateViewOptions(requestID, profileID, options) {
+  var profile = gProfiles[profileID];
+  var samples = profile.filteredSamples;
+  var symbols = profile.symbols;
+  var functions = profile.functions;
+
+  var treeData = convertToCallTree(samples, options.invertCallstack);
+  if (options.mergeUnbranched)
+    mergeUnbranchedCallPaths(treeData);
+  sendFinished(requestID, treeData);
+}
+
+// The responsiveness threshold (in ms) after which the sample shuold become
+// completely red in the histogram.
+var kDelayUntilWorstResponsiveness = 1000;
+
+function calculateHistogramData(requestID, profileID) {
+
+  function getStepColor(step) {
+    if (step.extraInfo && "responsiveness" in step.extraInfo) {
+      var res = step.extraInfo.responsiveness;
+      var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness));
+      return "rgb(" + redComponent + ",0,0)";
+    }
+
+    return "rgb(0,0,0)";
+  }
+
+  var profile = gProfiles[profileID];
+  var data = profile.filteredSamples;
+  var histogramData = [];
+  var maxHeight = 0;
+  for (var i = 0; i < data.length; ++i) {
+    if (!data[i])
+      continue;
+    var value = data[i].frames ? data[i].frames.length : 0;
+    if (maxHeight < value)
+      maxHeight = value;
+  }
+  maxHeight += 1;
+  var nextX = 0;
+  // The number of data items per histogramData rects.
+  // Except when seperated by a marker.
+  // This is used to cut down the number of rects, since
+  // there's no point in having more rects then pixels
+  var samplesPerStep = Math.max(1, Math.floor(data.length / 2000));
+  var frameStart = {};
+  for (var i = 0; i < data.length; i++) {
+    var step = data[i];
+    if (!step || !step.frames) {
+      // Add a gap for the sample that was filtered out.
+      nextX += 1 / samplesPerStep;
+      continue;
+    }
+    nextX = Math.ceil(nextX);
+    var value = step.frames.length / maxHeight;
+    var frames = step.frames;
+    var currHistogramData = histogramData[histogramData.length-1];
+    if (step.extraInfo && "marker" in step.extraInfo) {
+      // A new marker boundary has been discovered.
+      histogramData.push({
+        frames: "marker",
+        x: nextX,
+        width: 2,
+        value: 1,
+        marker: step.extraInfo.marker,
+        color: "fuchsia"
+      });
+      nextX += 2;
+      histogramData.push({
+        frames: [step.frames],
+        x: nextX,
+        width: 1,
+        value: value,
+        color: getStepColor(step),
+      });
+      nextX += 1;
+    } else if (currHistogramData != null &&
+      currHistogramData.frames.length < samplesPerStep &&
+      !(step.extraInfo && "frameNumber" in step.extraInfo)) {
+      currHistogramData.frames.push(step.frames);
+      // When merging data items take the average:
+      currHistogramData.value =
+        (currHistogramData.value * (currHistogramData.frames.length - 1) + value) /
+        currHistogramData.frames.length;
+      // Merge the colors? For now we keep the first color set.
+    } else {
+      // A new name boundary has been discovered.
+      currHistogramData = {
+        frames: [step.frames],
+        x: nextX,
+        width: 1,
+        value: value,
+        color: getStepColor(step),
+      };
+      if (step.extraInfo && "frameNumber" in step.extraInfo) {
+        currHistogramData.frameNumber = step.extraInfo.frameNumber;
+        frameStart[step.extraInfo.frameNumber] = histogramData.length;
+      }
+      histogramData.push(currHistogramData);
+      nextX += 1;
+    }
+  }
+  sendFinished(requestID, { histogramData: histogramData, frameStart: frameStart, widthSum: Math.ceil(nextX) });
+}
+
+var diagnosticList = [
+  // *************** Known bugs first (highest priority)
+  {
+    image: "io.png",
+    title: "Main Thread IO - Bug 765135 - TISCreateInputSourceList",
+    check: function(frames, symbols, meta) {
+
+      if (!stepContains('TISCreateInputSourceList', frames, symbols))
+        return false;
+
+      return stepContains('__getdirentries64', frames, symbols) 
+          || stepContains('__read', frames, symbols) 
+          || stepContains('__open', frames, symbols) 
+          || stepContains('stat$INODE64', frames, symbols)
+          ;
+    },
+  },
+
+  {
+    image: "js.png",
+    title: "Bug 772916 - Gradients are slow on mobile",
+    bugNumber: "772916",
+    check: function(frames, symbols, meta) {
+
+      return stepContains('PaintGradient', frames, symbols)
+          && stepContains('BasicTiledLayerBuffer::PaintThebesSingleBufferDraw', frames, symbols)
+          ;
+    },
+  },
+  {
+    image: "js.png",
+    title: "Bug 789193 - AMI_startup() takes 200ms on startup",
+    bugNumber: "789193",
+    check: function(frames, symbols, meta) {
+
+      return stepContains('AMI_startup()', frames, symbols)
+          ;
+    },
+  },
+  {
+    image: "js.png",
+    title: "Bug 789185 - LoginManagerStorage_mozStorage.init() takes 300ms on startup ",
+    bugNumber: "789185",
+    check: function(frames, symbols, meta) {
+
+      return stepContains('LoginManagerStorage_mozStorage.prototype.init()', frames, symbols)
+          ;
+    },
+  },
+
+  {
+    image: "js.png",
+    title: "JS - Bug 767070 - Text selection performance is bad on android",
+    bugNumber: "767070",
+    check: function(frames, symbols, meta) {
+
+      if (!stepContains('FlushPendingNotifications', frames, symbols))
+        return false;
+
+      return stepContains('sh_', frames, symbols)
+          && stepContains('browser.js', frames, symbols)
+          ;
+    },
+  },
+
+  {
+    image: "js.png",
+    title: "JS - Bug 765930 - Reader Mode: Optimize readability check",
+    bugNumber: "765930",
+    check: function(frames, symbols, meta) {
+
+      return stepContains('Readability.js', frames, symbols)
+          ;
+    },
+  },
+
+  // **************** General issues
+  {
+    image: "js.png",
+    title: "JS is triggering a sync reflow",
+    check: function(frames, symbols, meta) {
+      return symbolSequence(['js::RunScript','layout::DoReflow'], frames, symbols) ||
+             symbolSequence(['js::RunScript','layout::Flush'], frames, symbols)
+          ;
+    },
+  },
+
+  {
+    image: "gc.png",
+    title: "Garbage Collection Slice",
+    canMergeWithGC: false,
+    check: function(frames, symbols, meta, step) {
+      var slice = findGCSlice(frames, symbols, meta, step);
+
+      if (slice) {
+        var gcEvent = findGCEvent(frames, symbols, meta, step);
+        //dump("found event matching diagnostic\n");
+        //dump(JSON.stringify(gcEvent) + "\n");
+        return true;
+      }
+      return false;
+    },
+    details: function(frames, symbols, meta, step) {
+      var slice = findGCSlice(frames, symbols, meta, step);
+      if (slice) {
+        return "" +
+          "Reason: " + slice.reason + "\n" +
+          "Slice: " + slice.slice + "\n" +
+          "Pause: " + slice.pause + " ms";
+      }
+      return null;
+    },
+    onclickDetails: function(frames, symbols, meta, step) {
+      var gcEvent = findGCEvent(frames, symbols, meta, step);
+      if (gcEvent) {
+        return JSON.stringify(gcEvent);
+      } else {
+        return null;
+      }
+    },
+  },
+  {
+    image: "cc.png",
+    title: "Cycle Collect",
+    check: function(frames, symbols, meta, step) {
+      var ccEvent = findCCEvent(frames, symbols, meta, step);
+
+      if (ccEvent) {
+        dump("Found\n");
+        return true;
+      }
+      return false;
+    },
+    details: function(frames, symbols, meta, step) {
+      var ccEvent = findCCEvent(frames, symbols, meta, step);
+      if (ccEvent) {
+        return "" +
+          "Duration: " + ccEvent.duration + " ms\n" +
+          "Suspected: " + ccEvent.suspected;
+      }
+      return null;
+    },
+    onclickDetails: function(frames, symbols, meta, step) {
+      var ccEvent = findCCEvent(frames, symbols, meta, step);
+      if (ccEvent) {
+        return JSON.stringify(ccEvent);
+      } else {
+        return null;
+      }
+    },
+  },
+  {
+    image: "gc.png",
+    title: "Garbage Collection",
+    canMergeWithGC: false,
+    check: function(frames, symbols, meta) {
+      return stepContainsRegEx(/.*Collect.*Runtime.*Invocation.*/, frames, symbols)
+          || stepContains('GarbageCollectNow', frames, symbols) // Label
+          || stepContains('CycleCollect__', frames, symbols) // Label
+          ;
+    },
+  },
+  {
+    image: "plugin.png",
+    title: "Sync Plugin Constructor",
+    check: function(frames, symbols, meta) {
+      return stepContains('CallPPluginInstanceConstructor', frames, symbols) 
+          || stepContains('CallPCrashReporterConstructor', frames, symbols) 
+          || stepContains('PPluginModuleParent::CallNP_Initialize', frames, symbols)
+          || stepContains('GeckoChildProcessHost::SyncLaunch', frames, symbols)
+          ;
+    },
+  },
+  {
+    image: "text.png",
+    title: "Font Loading",
+    check: function(frames, symbols, meta) {
+      return stepContains('gfxFontGroup::BuildFontList', frames, symbols);
+    },
+  },
+  {
+    image: "io.png",
+    title: "Main Thread IO!",
+    check: function(frames, symbols, meta) {
+      return stepContains('__getdirentries64', frames, symbols) 
+          || stepContains('__open', frames, symbols) 
+          || stepContains('storage:::Statement::ExecuteStep', frames, symbols) 
+          || stepContains('__unlink', frames, symbols) 
+          || stepContains('fsync', frames, symbols) 
+          || stepContains('stat$INODE64', frames, symbols)
+          ;
+    },
+  },
+];
+
+function hasJSFrame(frames, symbols) {
+  for (var i = 0; i < frames.length; i++) {
+    if (symbols[frames[i]].isJSFrame === true) {
+      return true;
+    }
+  }
+  return false;
+}
+function findCCEvent(frames, symbols, meta, step) {
+  if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
+    return null;
+
+  var time = step.extraInfo.time;
+
+  for (var i = 0; i < meta.gcStats.ccEvents.length; i++) {
+    var ccEvent = meta.gcStats.ccEvents[i];
+    if (ccEvent.start_timestamp <= time && ccEvent.end_timestamp >= time) {
+      //dump("JSON: " + js_beautify(JSON.stringify(ccEvent)) + "\n");
+      return ccEvent;
+    }
+  }
+
+  return null;
+}
+function findGCEvent(frames, symbols, meta, step) {
+  if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
+    return null;
+
+  var time = step.extraInfo.time;
+
+  for (var i = 0; i < meta.gcStats.gcEvents.length; i++) {
+    var gcEvent = meta.gcStats.gcEvents[i];
+    if (!gcEvent.slices)
+      continue;
+    for (var j = 0; j < gcEvent.slices.length; j++) {
+      var slice = gcEvent.slices[j];
+      if (slice.start_timestamp <= time && slice.end_timestamp >= time) {
+        return gcEvent;
+      }
+    }
+  }
+
+  return null;
+}
+function findGCSlice(frames, symbols, meta, step) {
+  if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats)
+    return null;
+
+  var time = step.extraInfo.time;
+
+  for (var i = 0; i < meta.gcStats.gcEvents.length; i++) {
+    var gcEvent = meta.gcStats.gcEvents[i];
+    if (!gcEvent.slices)
+      continue;
+    for (var j = 0; j < gcEvent.slices.length; j++) {
+      var slice = gcEvent.slices[j];
+      if (slice.start_timestamp <= time && slice.end_timestamp >= time) {
+        return slice;
+      }
+    }
+  }
+
+  return null;
+}
+function stepContains(substring, frames, symbols) {
+  for (var i = 0; frames && i < frames.length; i++) {
+    var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
+    if (frameSym.indexOf(substring) != -1) {
+      return true;
+    }
+  }
+  return false;
+}
+function stepContainsRegEx(regex, frames, symbols) {
+  for (var i = 0; frames && i < frames.length; i++) {
+    var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
+    if (regex.exec(frameSym)) {
+      return true;
+    }
+  }
+  return false;
+}
+function symbolSequence(symbolsOrder, frames, symbols) {
+  var symbolIndex = 0;
+  for (var i = 0; frames && i < frames.length; i++) {
+    var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName;
+    var substring = symbolsOrder[symbolIndex];
+    if (frameSym.indexOf(substring) != -1) {
+      symbolIndex++;
+      if (symbolIndex == symbolsOrder.length) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+function firstMatch(array, matchFunction) {
+  for (var i = 0; i < array.length; i++) {
+    if (matchFunction(array[i]))
+      return array[i];
+  }
+  return undefined;
+}
+
+function calculateDiagnosticItems(requestID, profileID, meta) {
+  /*
+  if (!histogramData || histogramData.length < 1) {
+    sendFinished(requestID, []);
+    return;
+  }*/
+
+  var profile = gProfiles[profileID];
+  //var symbols = profile.symbols;
+  var symbols = profile.functions;
+  var data = profile.filteredSamples;
+
+  var lastStep = data[data.length-1];
+  var widthSum = data.length;
+  var pendingDiagnosticInfo = null;
+
+  var diagnosticItems = [];
+
+  function finishPendingDiagnostic(endX) {
+    if (!pendingDiagnosticInfo)
+      return;
+
+    var diagnostic = pendingDiagnosticInfo.diagnostic;
+    var currDiagnostic = {
+      x: pendingDiagnosticInfo.x / widthSum,
+      width: (endX - pendingDiagnosticInfo.x) / widthSum,
+      imageFile: pendingDiagnosticInfo.diagnostic.image,
+      title: pendingDiagnosticInfo.diagnostic.title,
+      details: pendingDiagnosticInfo.details,
+      onclickDetails: pendingDiagnosticInfo.onclickDetails
+    };
+
+    if (!currDiagnostic.onclickDetails && diagnostic.bugNumber) {
+      currDiagnostic.onclickDetails = "bug " + diagnostic.bugNumber;
+    }
+
+    diagnosticItems.push(currDiagnostic);
+
+    pendingDiagnosticInfo = null;
+  }
+
+/*
+  dump("meta: " + meta.gcStats + "\n");
+  if (meta && meta.gcStats) {
+    dump("GC Stats: " + JSON.stringify(meta.gcStats) + "\n");
+  }
+*/
+
+  data.forEach(function diagnoseStep(step, x) {
+    if (!step)
+      return;
+
+    var frames = step.frames;
+
+    var diagnostic = firstMatch(diagnosticList, function (diagnostic) {
+      return diagnostic.check(frames, symbols, meta, step);
+    });
+
+    if (!diagnostic) {
+      finishPendingDiagnostic(x);
+      return;
+    }
+
+    var details = diagnostic.details ? diagnostic.details(frames, symbols, meta, step) : null;
+
+    if (pendingDiagnosticInfo) {
+      // We're already inside a diagnostic range.
+      if (diagnostic == pendingDiagnosticInfo.diagnostic && pendingDiagnosticInfo.details == details) {
+        // We're still inside the same diagnostic.
+        return;
+      }
+
+      // We have left the old diagnostic and found a new one. Finish the old one.
+      finishPendingDiagnostic(x);
+    }
+
+    pendingDiagnosticInfo = {
+      diagnostic: diagnostic,
+      x: x,
+      details: details,
+      onclickDetails: diagnostic.onclickDetails ? diagnostic.onclickDetails(frames, symbols, meta, step) : null
+    };
+  });
+  if (pendingDiagnosticInfo)
+    finishPendingDiagnostic(data.length);
+
+  sendFinished(requestID, diagnosticItems);
+}
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/tree.js
@@ -0,0 +1,641 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var kMaxChunkDuration = 30; // ms
+
+var escape = document.createElement('textarea');
+
+function escapeHTML(html) {
+  escape.innerHTML = html;
+  return escape.innerHTML;
+}
+
+function unescapeHTML(html) {
+  escape.innerHTML = html;
+  return escape.value;
+}
+
+RegExp.escape = function(text) {
+    return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+}
+
+var requestAnimationFrame_timeout = null;
+var requestAnimationFrame = window.webkitRequestAnimationFrame ||
+                            window.mozRequestAnimationFrame ||
+                            window.oRequestAnimationFrame ||
+                            window.msRequestAnimationFrame ||
+                            function(callback, element) {
+                              window.setTimeout(callback, 1000 / 60);
+                            };
+
+var cancelAnimationFrame = window.webkitCancelAnimationFrame ||
+                           window.mozCancelAnimationFrame ||
+                           window.oCancelAnimationFrame ||
+                           window.msCancelAnimationFrame ||
+                           function(callback, element) {
+                             if (requestAnimationFrame_timeout) {
+                               window.clearTimeout(requestAnimationFrame_timeout);
+                               requestAnimationFrame_timeout = null;
+                             }
+                           };
+
+function TreeView() {
+  this._eventListeners = {};
+  this._pendingActions = [];
+  this._pendingActionsProcessingCallback = null;
+
+  this._container = document.createElement("div");
+  this._container.className = "treeViewContainer";
+  this._container.setAttribute("tabindex", "0"); // make it focusable
+
+  this._header = document.createElement("ul");
+  this._header.className = "treeHeader";
+  this._container.appendChild(this._header);
+
+  this._verticalScrollbox = document.createElement("div");
+  this._verticalScrollbox.className = "treeViewVerticalScrollbox";
+  this._container.appendChild(this._verticalScrollbox);
+
+  this._leftColumnBackground = document.createElement("div");
+  this._leftColumnBackground.className = "leftColumnBackground";
+  this._verticalScrollbox.appendChild(this._leftColumnBackground);
+
+  this._horizontalScrollbox = document.createElement("div");
+  this._horizontalScrollbox.className = "treeViewHorizontalScrollbox";
+  this._verticalScrollbox.appendChild(this._horizontalScrollbox);
+
+  this._contextMenu = document.createElement("menu");
+  this._contextMenu.setAttribute("type", "context");
+  this._contextMenu.id = "contextMenuForTreeView" + TreeView.instanceCounter++;
+  this._container.appendChild(this._contextMenu);
+
+  this._busyCover = document.createElement("div");
+  this._busyCover.className = "busyCover";
+  this._container.appendChild(this._busyCover);
+  this._abortToggleAll = false;
+
+  var self = this;
+  this._container.onkeydown = function (e) {
+    self._onkeypress(e);
+  };
+  this._container.onclick = function (e) {
+    self._onclick(e);
+  };
+  this._verticalScrollbox.addEventListener("contextmenu", function(event) {
+    self._populateContextMenu(event);
+  }, true);
+  this._setUpScrolling();
+};
+TreeView.instanceCounter = 0;
+
+TreeView.prototype = {
+  getContainer: function TreeView_getContainer() {
+    return this._container;
+  },
+  setColumns: function TreeView_setColumns(columns) {
+    this._header.innerHTML = "";
+    for (var i = 0; i < columns.length; i++) {
+      var li = document.createElement("li");
+      li.className = "treeColumnHeader treeColumnHeader" + i;
+      li.id = columns[i].name + "Header";
+      li.textContent = columns[i].title;
+      this._header.appendChild(li);
+    }
+  },
+  dataIsOutdated: function TreeView_dataIsOutdated() {
+    this._busyCover.classList.add("busy");
+  },
+  display: function TreeView_display(data, filterByName) {
+    this._busyCover.classList.remove("busy");
+    this._filterByName = filterByName;
+    this._filterByNameReg = null; // lazy init
+    if (this._filterByName === "")
+      this._filterByName = null;
+    this._horizontalScrollbox.innerHTML = "";
+    this._horizontalScrollbox.data = data[0].getData();
+    if (this._pendingActionsProcessingCallback) {
+      cancelAnimationFrame(this._pendingActionsProcessingCallback);
+      this._pendingActionsProcessingCallback = 0;
+    }
+    this._pendingActions = [];
+
+    this._pendingActions.push({
+      parentElement: this._horizontalScrollbox,
+      parentNode: null,
+      data: data[0].getData()
+    });
+    this._processPendingActionsChunk();
+    this._select(this._horizontalScrollbox.firstChild);
+    this._toggle(this._horizontalScrollbox.firstChild);
+    this._container.focus();
+  },
+  // Provide a snapshot of the reverse selection to restore with 'invert callback'
+  getReverseSelectionSnapshot: function TreeView__getReverseSelectionSnapshot(isJavascriptOnly) {
+    if (!this._selectedNode)
+      return;
+    var snapshot = [];
+    var curr = this._selectedNode.data;
+
+    while(curr) {
+      if (isJavascriptOnly && curr.isJSFrame || !isJavascriptOnly) {
+        snapshot.push(curr.name);
+        //dump(JSON.stringify(curr.name) + "\n");
+      }
+      if (curr.children && curr.children.length >= 1) {
+        curr = curr.children[0].getData();
+      } else {
+        break;
+      }
+    }
+
+    return snapshot.reverse();
+  },
+  // Provide a snapshot of the current selection to restore
+  getSelectionSnapshot: function TreeView__getSelectionSnapshot(isJavascriptOnly) {
+    var snapshot = [];
+    var curr = this._selectedNode;
+
+    while(curr) {
+      if (isJavascriptOnly && curr.data.isJSFrame || !isJavascriptOnly) {
+        snapshot.push(curr.data.name);
+        //dump(JSON.stringify(curr.data.name) + "\n");
+      }
+      curr = curr.treeParent;
+    }
+
+    return snapshot.reverse();
+  },
+  setSelection: function TreeView_setSelection(frames) {
+    this.restoreSelectionSnapshot(frames, false);
+  },
+  // Take a selection snapshot and restore the selection
+  restoreSelectionSnapshot: function TreeView_restoreSelectionSnapshot(snapshot, allowNonContigious) {
+    var currNode = this._horizontalScrollbox.firstChild;
+    if (currNode.data.name == snapshot[0] || snapshot[0] == "(total)") {
+      snapshot.shift();
+    }
+    //dump("len: " + snapshot.length + "\n");
+    next_level: while (currNode && snapshot.length > 0) {
+      this._toggle(currNode, false, true);
+      this._syncProcessPendingActionProcessing();
+      for (var i = 0; i < currNode.treeChildren.length; i++) {
+        if (currNode.treeChildren[i].data.name == snapshot[0]) {
+          //dump("Found: " + currNode.treeChildren[i].data.name + "\n");
+          snapshot.shift();
+          this._toggle(currNode, false, true);
+          currNode = currNode.treeChildren[i];
+          continue next_level;
+        }
+      }
+      if (allowNonContigious === true) {
+        // We need to do a Breadth-first search to find a match
+        var pendingSearch = [currNode.data];
+        while (pendingSearch.length > 0) {
+          var node = pendingSearch.shift();
+          //dump("searching: " + node.name + " for: " + snapshot[0] + "\n");
+          if (!node.children)
+            continue;
+          for (var i = 0; i < node.children.length; i++) {
+            var childNode = node.children[i].getData();
+            if (childNode.name == snapshot[0]) {
+              //dump("found: " + childNode.name + "\n");
+              snapshot.shift();
+              var nodesToToggle = [childNode];
+              while (nodesToToggle[0].name != currNode.data.name) {
+                nodesToToggle.splice(0, 0, nodesToToggle[0].parent);
+              }
+              var lastToggle = currNode;
+              for (var j = 0; j < nodesToToggle.length; j++) {
+                for (var k = 0; k < lastToggle.treeChildren.length; k++) {
+                  if (lastToggle.treeChildren[k].data.name == nodesToToggle[j].name) {
+                    //dump("Expend: " + nodesToToggle[j].name + "\n");
+                    this._toggle(lastToggle.treeChildren[k], false, true);
+                    lastToggle = lastToggle.treeChildren[k];
+                    this._syncProcessPendingActionProcessing();
+                  }
+                }
+              }
+              currNode = lastToggle;
+              continue next_level;
+            }
+            //dump("pending: " + childNode.name + "\n");
+            pendingSearch.push(childNode);
+          }
+        }
+      }
+      break; // Didn't find child node matching
+    }
+
+    if (currNode == this._horizontalScrollbox) {
+      PROFILERERROR("Failed to restore selection, could not find root.\n");
+      return;
+    }
+
+    this._toggle(currNode, true, true);
+    this._select(currNode);
+  },
+  _processPendingActionsChunk: function TreeView__processPendingActionsChunk(isSync) {
+    this._pendingActionsProcessingCallback = 0;
+
+    var startTime = Date.now();
+    var endTime = startTime + kMaxChunkDuration;
+    while ((isSync == true || Date.now() < endTime) && this._pendingActions.length > 0) {
+      this._processOneAction(this._pendingActions.shift());
+    }
+    this._scrollHeightChanged();
+
+    this._schedulePendingActionProcessing();
+  },
+  _schedulePendingActionProcessing: function TreeView__schedulePendingActionProcessing() {
+    if (!this._pendingActionsProcessingCallback && this._pendingActions.length > 0) {
+      var self = this;
+      this._pendingActionsProcessingCallback = requestAnimationFrame(function () {
+        self._processPendingActionsChunk();
+      });
+    }
+  },
+  _syncProcessPendingActionProcessing: function TreeView__syncProcessPendingActionProcessing() {
+    this._processPendingActionsChunk(true);
+  },
+  _processOneAction: function TreeView__processOneAction(action) {
+    var li = this._createTree(action.parentElement, action.parentNode, action.data);
+    if ("allChildrenCollapsedValue" in action) {
+      if (this._abortToggleAll)
+        return;
+      this._toggleAll(li, action.allChildrenCollapsedValue, true);
+    }
+  },
+  addEventListener: function TreeView_addEventListener(eventName, callbackFunction) {
+    if (!(eventName in this._eventListeners))
+      this._eventListeners[eventName] = [];
+    if (this._eventListeners[eventName].indexOf(callbackFunction) != -1)
+      return;
+    this._eventListeners[eventName].push(callbackFunction);
+  },
+  removeEventListener: function TreeView_removeEventListener(eventName, callbackFunction) {
+    if (!(eventName in this._eventListeners))
+      return;
+    var index = this._eventListeners[eventName].indexOf(callbackFunction);
+    if (index == -1)
+      return;
+    this._eventListeners[eventName].splice(index, 1);
+  },
+  _fireEvent: function TreeView__fireEvent(eventName, eventObject) {
+    if (!(eventName in this._eventListeners))
+      return;
+    this._eventListeners[eventName].forEach(function (callbackFunction) {
+      callbackFunction(eventObject);
+    });
+  },
+  _setUpScrolling: function TreeView__setUpScrolling() {
+    var waitingForPaint = false;
+    var accumulatedDeltaX = 0;
+    var accumulatedDeltaY = 0;
+    var self = this;
+    function scrollListener(e) {
+      if (!waitingForPaint) {
+        requestAnimationFrame(function () {
+          self._horizontalScrollbox.scrollLeft += accumulatedDeltaX;
+          self._verticalScrollbox.scrollTop += accumulatedDeltaY;
+          accumulatedDeltaX = 0;
+          accumulatedDeltaY = 0;
+          waitingForPaint = false;
+        });
+        waitingForPaint = true;
+      }
+      if (e.axis == e.HORIZONTAL_AXIS) {
+        accumulatedDeltaX += e.detail;
+      } else {
+        accumulatedDeltaY += e.detail;
+      }
+      e.preventDefault();
+    }
+    this._verticalScrollbox.addEventListener("MozMousePixelScroll", scrollListener, false);
+    this._verticalScrollbox.cleanUp = function () {
+      self._verticalScrollbox.removeEventListener("MozMousePixelScroll", scrollListener, false);
+    };
+  },
+  _scrollHeightChanged: function TreeView__scrollHeightChanged() {
+    this._leftColumnBackground.style.height = this._horizontalScrollbox.getBoundingClientRect().height + 'px';
+  },
+  _createTree: function TreeView__createTree(parentElement, parentNode, data) {
+    var div = document.createElement("div");
+    div.className = "treeViewNode collapsed";
+    var hasChildren = ("children" in data) && (data.children.length > 0);
+    if (!hasChildren)
+      div.classList.add("leaf");
+    var treeLine = document.createElement("div");
+    treeLine.className = "treeLine";
+    treeLine.innerHTML = this._HTMLForFunction(data);
+    // When this item is toggled we will expand its children
+    div.pendingExpand = [];
+    div.treeLine = treeLine;
+    div.data = data;
+    div.appendChild(treeLine);
+    div.treeChildren = [];
+    div.treeParent = parentNode;
+    if (hasChildren) {
+      var parent = document.createElement("div");
+      parent.className = "treeViewNodeList";
+      for (var i = 0; i < data.children.length; ++i) {
+        div.pendingExpand.push({parentElement: parent, parentNode: div, data: data.children[i].getData() });
+      }
+      div.appendChild(parent);
+    }
+    if (parentNode) {
+      parentNode.treeChildren.push(div);
+    }
+    parentElement.appendChild(div);
+    return div;
+  },
+  _populateContextMenu: function TreeView__populateContextMenu(event) {
+    this._verticalScrollbox.setAttribute("contextmenu", "");
+
+    var target = event.target;
+    if (target.classList.contains("expandCollapseButton") ||
+        target.classList.contains("focusCallstackButton"))
+      return;
+
+    var li = this._getParentTreeViewNode(target);
+    if (!li)
+      return;
+
+    this._select(li);
+
+    this._contextMenu.innerHTML = "";
+
+    var self = this;
+    this._contextMenuForFunction(li.data).forEach(function (menuItem) {
+      var menuItemNode = document.createElement("menuitem");
+      menuItemNode.onclick = (function (menuItem) {
+        return function() {
+          self._contextMenuClick(li.data, menuItem);
+        };
+      })(menuItem);
+      menuItemNode.label = menuItem;
+      self._contextMenu.appendChild(menuItemNode);
+    });
+
+    this._verticalScrollbox.setAttribute("contextmenu", this._contextMenu.id);
+  },
+  _contextMenuClick: function TreeView__contextMenuClick(node, menuItem) {
+    this._fireEvent("contextMenuClick", { node: node, menuItem: menuItem });
+  },
+  _contextMenuForFunction: function TreeView__contextMenuForFunction(node) {
+    // TODO move me outside tree.js
+    var menu = [];
+    if (node.library != null && (
+      node.library.toLowerCase() == "xul" ||
+      node.library.toLowerCase() == "xul.dll"
+      )) {
+      menu.push("View Source");
+    }
+    if (node.isJSFrame && node.scriptLocation) {
+      menu.push("View JS Source");
+    }
+    menu.push("Focus Frame");
+    menu.push("Focus Callstack");
+    menu.push("Google Search");
+    menu.push("Plugin View: Pie");
+    menu.push("Plugin View: Tree");
+    return menu;
+  },
+  _HTMLForFunction: function TreeView__HTMLForFunction(node) {
+    var nodeName = escapeHTML(node.name);
+    var libName = node.library;
+    if (this._filterByName) {
+      if (!this._filterByNameReg) {
+        this._filterByName = RegExp.escape(this._filterByName);
+        this._filterByNameReg = new RegExp("(" + this._filterByName + ")","gi");
+      }
+      nodeName = nodeName.replace(this._filterByNameReg, "<a style='color:red;'>$1</a>");
+      libName = libName.replace(this._filterByNameReg, "<a style='color:red;'>$1</a>");
+    }
+    var samplePercentage;
+    if (isNaN(node.ratio)) {
+      samplePercentage = "";
+    } else {
+      samplePercentage = (100 * node.ratio).toFixed(1) + "%";
+    }
+    return '<input type="button" value="Expand / Collapse" class="expandCollapseButton" tabindex="-1"> ' +
+      '<span class="sampleCount">' + node.counter + '</span> ' +
+      '<span class="samplePercentage">' + samplePercentage + '</span> ' +
+      '<span class="selfSampleCount">' + node.selfCounter + '</span> ' +
+      '<span class="functionName">' + nodeName + '</span>' +
+      '<span class="libraryName">' + libName + '</span>' +
+      '<input type="button" value="Focus Callstack" title="Focus Callstack" class="focusCallstackButton" tabindex="-1">';
+  },
+  _resolveChildren: function TreeView__resolveChildren(div, childrenCollapsedValue) {
+    while (div.pendingExpand != null && div.pendingExpand.length > 0) {
+      var pendingExpand = div.pendingExpand.shift();
+      pendingExpand.allChildrenCollapsedValue = childrenCollapsedValue;
+      this._pendingActions.push(pendingExpand);
+      this._schedulePendingActionProcessing();
+    }
+  },
+  _toggle: function TreeView__toggle(div, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) {
+    var currentCollapsedValue = this._isCollapsed(div);
+    if (newCollapsedValue === undefined)
+      newCollapsedValue = !currentCollapsedValue;
+    if (newCollapsedValue) {
+      div.classList.add("collapsed");
+    } else {
+      this._resolveChildren(div, true);
+      div.classList.remove("collapsed");
+    }
+    if (!suppressScrollHeightNotification)
+      this._scrollHeightChanged();
+  },
+  _toggleAll: function TreeView__toggleAll(subtreeRoot, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) {
+
+    // Reset abort
+    this._abortToggleAll = false;
+
+    // Expands / collapses all child nodes, too.
+
+    if (newCollapsedValue === undefined)
+      newCollapsedValue = !this._isCollapsed(subtreeRoot);
+    if (!newCollapsedValue) {
+      // expanding
+      this._resolveChildren(subtreeRoot, newCollapsedValue);
+    }
+    this._toggle(subtreeRoot, newCollapsedValue, true);
+    for (var i = 0; i < subtreeRoot.treeChildren.length; ++i) {
+      this._toggleAll(subtreeRoot.treeChildren[i], newCollapsedValue, true);
+    }
+    if (!suppressScrollHeightNotification)
+      this._scrollHeightChanged();
+  },
+  _getParent: function TreeView__getParent(div) {
+    return div.treeParent;
+  },
+  _getFirstChild: function TreeView__getFirstChild(div) {
+    if (this._isCollapsed(div))
+      return null;
+    var child = div.treeChildren[0];
+    return child;
+  },
+  _getLastChild: function TreeView__getLastChild(div) {
+    if (this._isCollapsed(div))
+      return div;
+    var lastChild = div.treeChildren[div.treeChildren.length-1];
+    if (lastChild == null)
+      return div;
+    return this._getLastChild(lastChild);
+  },
+  _getPrevSib: function TreeView__getPevSib(div) {
+    if (div.treeParent == null)
+      return null;
+    var nodeIndex = div.treeParent.treeChildren.indexOf(div);
+    if (nodeIndex == 0)
+      return null;
+    return div.treeParent.treeChildren[nodeIndex-1];
+  },
+  _getNextSib: function TreeView__getNextSib(div) {
+    if (div.treeParent == null)
+      return null;
+    var nodeIndex = div.treeParent.treeChildren.indexOf(div);
+    if (nodeIndex == div.treeParent.treeChildren.length - 1)
+      return this._getNextSib(div.treeParent);
+    return div.treeParent.treeChildren[nodeIndex+1];
+  },
+  _scrollIntoView: function TreeView__scrollIntoView(element, maxImportantWidth) {
+    // Make sure that element is inside the visible part of our scrollbox by
+    // adjusting the scroll positions. If element is wider or
+    // higher than the scroll port, the left and top edges are prioritized over
+    // the right and bottom edges.
+    // If maxImportantWidth is set, parts of the beyond this widths are
+    // considered as not important; they'll not be moved into view.
+
+    if (maxImportantWidth === undefined)
+      maxImportantWidth = Infinity;
+
+    var visibleRect = {
+      left: this._horizontalScrollbox.getBoundingClientRect().left + 150, // TODO: un-hardcode 150
+      top: this._verticalScrollbox.getBoundingClientRect().top,
+      right: this._horizontalScrollbox.getBoundingClientRect().right,
+      bottom: this._verticalScrollbox.getBoundingClientRect().bottom
+    }
+    var r = element.getBoundingClientRect();
+    var right = Math.min(r.right, r.left + maxImportantWidth);
+    var leftCutoff = visibleRect.left - r.left;
+    var rightCutoff = right - visibleRect.right;
+    var topCutoff = visibleRect.top - r.top;
+    var bottomCutoff = r.bottom - visibleRect.bottom;
+    if (leftCutoff > 0)
+      this._horizontalScrollbox.scrollLeft -= leftCutoff;
+    else if (rightCutoff > 0)
+      this._horizontalScrollbox.scrollLeft += Math.min(rightCutoff, -leftCutoff);
+    if (topCutoff > 0)
+      this._verticalScrollbox.scrollTop -= topCutoff;
+    else if (bottomCutoff > 0)
+      this._verticalScrollbox.scrollTop += Math.min(bottomCutoff, -topCutoff);
+  },
+  _select: function TreeView__select(li) {
+    if (this._selectedNode != null) {
+      this._selectedNode.treeLine.classList.remove("selected");
+      this._selectedNode = null;
+    }
+    if (li) {
+      li.treeLine.classList.add("selected");
+      this._selectedNode = li;
+      var functionName = li.treeLine.querySelector(".functionName");
+      this._scrollIntoView(functionName, 400);
+      this._fireEvent("select", li.data);
+    }
+  },
+  _isCollapsed: function TreeView__isCollapsed(div) {
+    return div.classList.contains("collapsed");
+  },
+  _getParentTreeViewNode: function TreeView__getParentTreeViewNode(node) {
+    while (node) {
+      if (node.nodeType != node.ELEMENT_NODE)
+        break;
+      if (node.classList.contains("treeViewNode"))
+        return node;
+      node = node.parentNode;
+    }
+    return null;
+  },
+  _onclick: function TreeView__onclick(event) {
+    var target = event.target;
+    var node = this._getParentTreeViewNode(target);
+    if (!node)
+      return;
+    if (target.classList.contains("expandCollapseButton")) {
+      if (event.altKey)
+        this._toggleAll(node);
+      else
+        this._toggle(node);
+    } else if (target.classList.contains("focusCallstackButton")) {
+      this._fireEvent("focusCallstackButtonClicked", node.data);
+    } else {
+      this._select(node);
+      if (event.detail == 2) // dblclick
+        this._toggle(node);
+    }
+  },
+  _onkeypress: function TreeView__onkeypress(event) {
+    if (event.ctrlKey || event.altKey || event.metaKey)
+      return;
+
+    this._abortToggleAll = true;
+
+    var selected = this._selectedNode;
+    if (event.keyCode < 37 || event.keyCode > 40) {
+      if (event.keyCode != 0 ||
+          String.fromCharCode(event.charCode) != '*') {
+        return;
+      }
+    }
+    event.stopPropagation();
+    event.preventDefault();
+    if (!selected)
+      return;
+    if (event.keyCode == 37) { // KEY_LEFT
+      var isCollapsed = this._isCollapsed(selected);
+      if (!isCollapsed) {
+        this._toggle(selected);
+      } else {
+        var parent = this._getParent(selected); 
+        if (parent != null) {
+          this._select(parent);
+        }
+      }
+    } else if (event.keyCode == 38) { // KEY_UP
+      var prevSib = this._getPrevSib(selected);
+      var parent = this._getParent(selected); 
+      if (prevSib != null) {
+        this._select(this._getLastChild(prevSib));
+      } else if (parent != null) {
+        this._select(parent);
+      }
+    } else if (event.keyCode == 39) { // KEY_RIGHT
+      var isCollapsed = this._isCollapsed(selected);
+      if (isCollapsed) {
+        this._toggle(selected);
+      } else {
+        // Do KEY_DOWN
+        var nextSib = this._getNextSib(selected);
+        var child = this._getFirstChild(selected); 
+        if (child != null) {
+          this._select(child);
+        } else if (nextSib) {
+          this._select(nextSib);
+        }
+      }
+    } else if (event.keyCode == 40) { // KEY_DOWN
+      var nextSib = this._getNextSib(selected);
+      var child = this._getFirstChild(selected); 
+      if (child != null) {
+        this._select(child);
+      } else if (nextSib) {
+        this._select(nextSib);
+      }
+    } else if (String.fromCharCode(event.charCode) == '*') {
+      this._toggleAll(selected);
+    }
+  },
+};
+
new file mode 100755
--- /dev/null
+++ b/browser/devtools/profiler/cleopatra/js/ui.js
@@ -0,0 +1,1683 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EIDETICKER_BASE_URL = "http://eideticker.wrla.ch/";
+
+var gDebugLog = false;
+var gDebugTrace = false;
+var gLocation = window.location + "";
+if (gLocation.indexOf("file:") == 0) {
+  gDebugLog = true;
+  gDebugTrace = true;
+  PROFILERLOG("Turning on logging+tracing since cleopatra is served from the file protocol");
+}
+// Use for verbose tracing, otherwise use log
+function PROFILERTRACE(msg) {
+  if (gDebugTrace)
+    PROFILERLOG(msg);
+}
+function PROFILERLOG(msg) {
+  if (gDebugLog) {
+    msg = "Cleo: " + msg;
+    console.log(msg);
+    if (window.dump)
+      window.dump(msg + "\n");
+  }
+}
+function PROFILERERROR(msg) {
+  msg = "Cleo: " + msg;
+  console.log(msg);
+  if (window.dump)
+    window.dump(msg + "\n");
+}
+function enableProfilerTracing() {
+  gDebugLog = true;
+  gDebugTrace = true;
+  Parser.updateLogSetting();
+}
+function enableProfilerLogging() {
+  gDebugLog = true;
+  Parser.updateLogSetting();
+}
+
+function removeAllChildren(element) {
+  while (element.firstChild) {
+    element.removeChild(element.firstChild);
+  }
+}
+
+function FileList() {
+  this._container = document.createElement("ul");
+  this._container.id = "fileList";
+  this._selectedFileItem = null;
+  this._fileItemList = [];
+}
+
+FileList.prototype = {
+  getContainer: function FileList_getContainer() {
+    return this._container;
+  },
+
+  loadProfileListFromLocalStorage: function FileList_loadProfileListFromLocalStorage() {
+    var self = this;
+    gLocalStorage.getProfileList(function(profileList) {
+      for (var i = 0; i < profileList.length; i++) {
+        (function closure() {
+          // This only carries info about the profile and the access key to retrieve it.
+          var profileInfo = profileList[i];
+          //PROFILERTRACE("Profile list from local storage: " + JSON.stringify(profileInfo));
+          var fileEntry = self.addFile(profileInfo.profileKey, "local storage", function fileEntryClick() {
+            loadLocalStorageProfile(profileInfo.profileKey);
+          });
+        })();
+      }
+    });
+  },
+
+  addFile: function FileList_addFile(fileName, description, onselect) {
+    var li = document.createElement("li");
+
+    li.fileName = fileName || "New Profile";
+    li.description = description || "(empty)";
+
+    li.className = "fileListItem";
+    if (!this._selectedFileItem) {
+      li.classList.add("selected");
+      this._selectedFileItem = li;
+    }
+
+    li.onselect = onselect;
+    var self = this;
+    li.onclick = function() {
+      self.setSelection(li);
+    }
+
+    var fileListItemTitleSpan = document.createElement("span");
+    fileListItemTitleSpan.className = "fileListItemTitle";
+    fileListItemTitleSpan.textContent = li.fileName;
+    li.appendChild(fileListItemTitleSpan);
+
+    var fileListItemDescriptionSpan = document.createElement("span");
+    fileListItemDescriptionSpan.className = "fileListItemDescription";
+    fileListItemDescriptionSpan.textContent = li.description;
+    li.appendChild(fileListItemDescriptionSpan);
+
+    this._container.appendChild(li);
+
+    this._fileItemList.push(li);
+
+    return li;
+  },
+
+  setSelection: function FileList_setSelection(fileEntry) {
+    if (this._selectedFileItem) {
+      this._selectedFileItem.classList.remove("selected");
+    }
+    this._selectedFileItem = fileEntry;
+    fileEntry.classList.add("selected");
+    if (this._selectedFileItem.onselect)
+      this._selectedFileItem.onselect();
+  },
+
+  profileParsingFinished: function FileList_profileParsingFinished() {
+    this._container.querySelector(".fileListItemTitle").textContent = "Current Profile";
+    this._container.querySelector(".fileListItemDescription").textContent = gNumSamples + " Samples";
+  }
+}
+
+function treeObjSort(a, b) {
+  return b.counter - a.counter;
+}
+
+function ProfileTreeManager() {
+  this.treeView = new TreeView();
+  this.treeView.setColumns([
+    { name: "sampleCount", title: "Running time" },
+    { name: "selfSampleCount", title: "Self" },
+    { name: "symbolName", title: "Symbol Name"},
+  ]);
+  var self = this;
+  this.treeView.addEventListener("select", function (frameData) {
+    self.highlightFrame(frameData);
+  });
+  this.treeView.addEventListener("contextMenuClick", function (e) {
+    self._onContextMenuClick(e);
+  });
+  this.treeView.addEventListener("focusCallstackButtonClicked", function (frameData) {
+    var focusedCallstack = self._getCallstackUpTo(frameData);
+    focusOnCallstack(focusedCallstack, frameData.name);
+  });
+  this._container = document.createElement("div");
+  this._container.className = "tree";
+  this._container.appendChild(this.treeView.getContainer());
+
+  // If this is set when the tree changes the snapshot is immediately restored.
+  this._savedSnapshot = null;
+}
+ProfileTreeManager.prototype = {
+  getContainer: function ProfileTreeManager_getContainer() {
+    return this._container;
+  },
+  highlightFrame: function Treedisplay_highlightFrame(frameData) {
+    setHighlightedCallstack(this._getCallstackUpTo(frameData), this._getHeaviestCallstack(frameData));
+  },
+  dataIsOutdated: function ProfileTreeManager_dataIsOutdated() {
+    this.treeView.dataIsOutdated();
+  },
+  saveSelectionSnapshot: function ProfileTreeManager_getSelectionSnapshot(isJavascriptOnly) {
+    this._savedSnapshot = this.treeView.getSelectionSnapshot(isJavascriptOnly);
+  },
+  saveReverseSelectionSnapshot: function ProfileTreeManager_getReverseSelectionSnapshot(isJavascriptOnly) {
+    this._savedSnapshot = this.treeView.getReverseSelectionSnapshot(isJavascriptOnly);
+  },
+  _restoreSelectionSnapshot: function ProfileTreeManager__restoreSelectionSnapshot(snapshot, allowNonContigous) {
+    return this.treeView.restoreSelectionSnapshot(snapshot, allowNonContigous);
+  },
+  setSelection: function ProfileTreeManager_setSelection(frames) {
+    return this.treeView.setSelection(frames);
+  },
+  _getCallstackUpTo: function ProfileTreeManager__getCallstackUpTo(frame) {
+    var callstack = [];
+    var curr = frame;
+    while (curr != null) {
+      if (curr.name != null) {
+        var subCallstack = curr.fullFrameNamesAsInSample.clone();
+        subCallstack.reverse();
+        callstack = callstack.concat(subCallstack);
+      }
+      curr = curr.parent;
+    }
+    callstack.reverse();
+    if (gInvertCallstack)
+      callstack.shift(); // remove (total)
+    return callstack;
+  },
+  _getHeaviestCallstack: function ProfileTreeManager__getHeaviestCallstack(frame) {
+    // FIXME: This gets the first leaf which is not the heaviest leaf.
+    while(frame.children && frame.children.length > 0) {
+      var nextFrame = frame.children[0].getData();
+      if (!nextFrame)
+        break;
+      frame = nextFrame;
+    }
+    return this._getCallstackUpTo(frame);
+  },
+  _onContextMenuClick: function ProfileTreeManager__onContextMenuClick(e) {
+    var node = e.node;
+    var menuItem = e.menuItem;
+
+    if (menuItem == "View Source") {
+      // Remove anything after ( since MXR doesn't handle search with the arguments.
+      var symbol = node.name.split("(")[0];
+      window.open("http://mxr.mozilla.org/mozilla-central/search?string=" + symbol, "View Source");
+    } else if (menuItem == "View JS Source") {
+      viewJSSource(node);
+    } else if (menuItem == "Plugin View: Pie") {
+      focusOnPluginView("protovis", {type:"pie"});
+    } else if (menuItem == "Plugin View: Tree") {
+      focusOnPluginView("protovis", {type:"tree"});
+    } else if (menuItem == "Google Search") {
+      var symbol = node.name;
+      window.open("https://www.google.ca/search?q=" + symbol, "View Source");
+    } else if (menuItem == "Focus Frame") {
+      var symbol = node.fullFrameNamesAsInSample[0]; // TODO: we only function one symbol when callpath merging is on, fix that
+      focusOnSymbol(symbol, node.name);
+    } else if (menuItem == "Focus Callstack") {
+      var focusedCallstack = this._getCallstackUpTo(node);
+      focusOnCallstack(focusedCallstack, node.name);
+    }
+  },
+  setAllowNonContigous: function ProfileTreeManager_setAllowNonContigous() {
+    this._allowNonContigous = true;
+  },
+  display: function ProfileTreeManager_display(tree, symbols, functions, useFunctions, filterByName) {
+    this.treeView.display(this.convertToJSTreeData(tree, symbols, functions, useFunctions), filterByName);
+    if (this._savedSnapshot) {
+      this._restoreSelectionSnapshot(this._savedSnapshot, this._allowNonContigous);
+      this._savedSnapshot = null;
+      this._allowNonContigous = false;
+    }
+  },
+  convertToJSTreeData: function ProfileTreeManager__convertToJSTreeData(rootNode, symbols, functions, useFunctions) {
+    var totalSamples = rootNode.counter;
+    function createTreeViewNode(node, parent) {
+      var curObj = {};
+      curObj.parent = parent;
+      curObj.counter = node.counter;
+      var selfCounter = node.counter;
+      for (var i = 0; i < node.children.length; ++i) {
+        selfCounter -= node.children[i].counter;
+      }
+      curObj.selfCounter = selfCounter;
+      curObj.ratio = node.counter / totalSamples;
+      curObj.fullFrameNamesAsInSample = node.mergedNames ? node.mergedNames : [node.name];
+      if (useFunctions ? !(node.name in functions) : !(node.name in symbols)) {
+        curObj.name = node.name;
+        curObj.library = "";
+      } else {
+        var functionObj = useFunctions ? functions[node.name] : functions[symbols[node.name].functionIndex];
+        var info = {
+          functionName: functionObj.functionName,
+          libraryName: functionObj.libraryName,
+          lineInformation: useFunctions ? "" : symbols[node.name].lineInformation
+        };
+        curObj.name = (info.functionName + " " + info.lineInformation).trim();
+        curObj.library = info.libraryName;
+        curObj.isJSFrame = functionObj.isJSFrame;
+        if (functionObj.scriptLocation) {
+          curObj.scriptLocation = functionObj.scriptLocation;
+        }
+      }
+      if (node.children.length) {
+        curObj.children = getChildrenObjects(node.children, curObj);
+      }
+      return curObj;
+    }
+    function getChildrenObjects(children, parent) {
+      var sortedChildren = children.slice(0).sort(treeObjSort);
+      return sortedChildren.map(function (child) {
+        var createdNode = null;
+        return {
+          getData: function () {
+            if (!createdNode) {
+              createdNode = createTreeViewNode(child, parent);
+            }
+            return createdNode;
+          }
+        };
+      });
+    }
+    return getChildrenObjects([rootNode], null);
+  },
+};
+
+function SampleBar() {
+  this._container = document.createElement("div");
+  this._container.id = "sampleBar";
+  this._container.className = "sideBar";
+
+  this._header = document.createElement("h2");
+  this._header.innerHTML = "Selection - Most time spent in:";
+  this._header.alt = "This shows the heaviest leaf of the selected sample. Use this to get a quick glimpse of where the selection is spending most of its time.";
+  this._container.appendChild(this._header);
+
+  this._text = document.createElement("span");
+  this._text.style.whiteSpace = "pre";
+  this._text.innerHTML = "Sample text";
+  this._container.appendChild(this._text);
+}
+
+SampleBar.prototype = {
+  getContainer: function SampleBar_getContainer() {
+    return this._container;
+  },
+  setSample: function SampleBar_setSample(sample) {
+    var str = "";
+    var list = [];
+
+    this._text.innerHTML = "";
+
+    for (var i = 0; i < sample.length; i++) {
+      var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex];
+      if (!functionObj)
+        continue;
+      var functionLink = document.createElement("a");
+      functionLink.textContent = "- " + functionObj.functionName;
+      functionLink.href = "#";
+      this._text.appendChild(functionLink);
+      this._text.appendChild(document.createElement("br"));
+      list.push(functionObj.functionName);
+      functionLink.selectIndex = i;
+      functionLink.onclick = function() {
+        var selectedFrames = [];
+        if (gInvertCallstack) {
+          for (var i = 0; i <= this.selectIndex; i++) {
+            var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex];
+            selectedFrames.push(functionObj.functionName);
+          }
+        } else {
+          for (var i = sample.length - 1; i >= this.selectIndex; i--) {
+            var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex];
+            selectedFrames.push(functionObj.functionName);
+          }
+        }
+        gTreeManager.setSelection(selectedFrames);
+        return false;
+      }
+    }
+    return list;
+  },
+}
+
+
+function PluginView() {
+  this._container = document.createElement("div");
+  this._container.className = "pluginview";
+  this._container.style.visibility = 'hidden';
+  this._iframe = document.createElement("iframe");
+  this._iframe.className = "pluginviewIFrame";
+  this._container.appendChild(this._iframe);
+  this._container.style.top = "";
+}
+PluginView.prototype = {
+  getContainer: function PluginView_getContainer() {
+    return this._container;
+  },
+  hide: function() {
+    // get rid of the scrollbars
+    this._container.style.top = "";
+    this._container.style.visibility = 'hidden';
+  },
+  show: function() {
+    // This creates extra scrollbar so only do it when needed
+    this._container.style.top = "0px";
+    this._container.style.visibility = '';
+  },
+  display: function(pluginName, param, data) {
+    this._iframe.src = "js/plugins/" + pluginName + "/index.html";
+    var self = this;
+    this._iframe.onload = function() {
+      console.log("Pluginview '" + pluginName + " iframe onload");
+      self._iframe.contentWindow.initCleopatraPlugin(data, param, gSymbols);
+    }
+    this.show();
+    //console.log(gSymbols);
+  },
+}
+
+function HistogramView() {
+  this._container = document.createElement("div");
+  this._container.className = "histogram";
+
+  this._canvas = this._createCanvas();
+  this._container.appendChild(this._canvas);
+
+  this._rangeSelector = new RangeSelector(this._canvas, this);
+  this._rangeSelector.enableRangeSelectionOnHistogram();
+  this._container.appendChild(this._rangeSelector.getContainer());
+
+  this._busyCover = document.createElement("div");
+  this._busyCover.className = "busyCover";
+  this._container.appendChild(this._busyCover);
+
+  this._histogramData = [];
+}
+HistogramView.prototype = {
+  dataIsOutdated: function HistogramView_dataIsOutdated() {
+    this._busyCover.classList.add("busy");
+  },
+  _createCanvas: function HistogramView__createCanvas() {
+    var canvas = document.createElement("canvas");
+    canvas.height = 60;
+    canvas.style.width = "100%";
+    canvas.style.height = "100%";
+    return canvas;
+  },
+  getContainer: function HistogramView_getContainer() {
+    return this._container;
+  },
+  showVideoFramePosition: function HistogramView_showVideoFramePosition(frame) {
+    if (!this._frameStart || !this._frameStart[frame])
+      return;
+    var frameStart = this._frameStart[frame];
+    // Now we look for the frame end. Because we can swap frame we don't present we have to look ahead
+    // in the stream if frame+1 doesn't exist.
+    var frameEnd = this._frameStart[frame+1];
+    for (var i = 0; i < 10 && !frameEnd; i++) {
+      frameEnd = this._frameStart[frame+1+i];
+    }
+    this._rangeSelector.showVideoRange(frameStart, frameEnd);
+  },
+  showVideoPosition: function HistogramView_showVideoPosition(position) {
+    // position in 0..1
+    this._rangeSelector.showVideoPosition(position);
+  },
+  _gatherMarkersList: function HistogramView__gatherMarkersList(histogramData) {
+    var markers = [];
+    for (var i = 0; i < histogramData.length; ++i) {
+      var step = histogramData[i];
+      if ("marker" in step) {
+        markers.push({
+          index: i,
+          name: step.marker
+        });
+      }
+    }
+    return markers;
+  },
+  _calculateWidthMultiplier: function () {
+    var minWidth = 2000;
+    return Math.ceil(minWidth / this._widthSum);
+  },
+  histogramClick: function HistogramView_histogramClick(index) {
+    var sample = this._histogramData[index];
+    var frames = sample.frames;
+    if (gSampleBar) {
+      var list = gSampleBar.setSample(frames[0]);
+      gTreeManager.setSelection(list);
+      setHighlightedCallstack(frames[0], frames[0]);
+    }
+  },
+  display: function HistogramView_display(histogramData, frameStart, widthSum, highlightedCallstack) {
+    this._histogramData = histogramData;
+    PROFILERTRACE("FRAME START: " + frameStart + "\n");
+    this._frameStart = frameStart;
+    this._widthSum = widthSum;
+    this._widthMultiplier = this._calculateWidthMultiplier();
+    this._canvas.width = this._widthMultiplier * this._widthSum;
+    this._render(highlightedCallstack);
+    this._busyCover.classList.remove("busy");
+  },
+  _render: function HistogramView__render(highlightedCallstack) {
+    var ctx = this._canvas.getContext("2d");
+    var height = this._canvas.height;
+    ctx.setTransform(this._widthMultiplier, 0, 0, 1, 0, 0);
+    ctx.clearRect(0, 0, this._widthSum, height);
+
+    var self = this;
+    for (var i = 0; i < this._histogramData.length; i++) {
+      var step = this._histogramData[i];
+      var isSelected = self._isStepSelected(step, highlightedCallstack);
+      var isInRangeSelector = self._isInRangeSelector(i);
+      if (isSelected) {
+        ctx.fillStyle = "green";
+      } else if (isInRangeSelector) {
+        ctx.fillStyle = "blue";
+      } else {
+        ctx.fillStyle = step.color;
+      }
+      var roundedHeight = Math.round(step.value * height);
+      ctx.fillRect(step.x, height - roundedHeight, step.width, roundedHeight);
+    }
+
+    this._finishedRendering = true;
+  },
+  highlightedCallstackChanged: function HistogramView_highlightedCallstackChanged(highlightedCallstack) {
+    this._render(highlightedCallstack);
+  },
+  _isInRangeSelector: function HistogramView_isInRangeSelector(index) {
+    return false;
+  },
+  _isStepSelected: function HistogramView__isStepSelected(step, highlightedCallstack) {
+    if ("marker" in step)
+      return false;
+    return step.frames.some(function isCallstackSelected(frames) {
+      if (frames.length < highlightedCallstack.length ||
+          highlightedCallstack.length <= (gInvertCallstack ? 0 : 1))
+        return false;
+
+      var compareFrames = frames;
+      if (gInvertCallstack) {
+        for (var j = 0; j < highlightedCallstack.length; j++) {
+          var compareFrameIndex = compareFrames.length - 1 - j;
+          if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) {
+            return false;
+          }
+        }
+      } else {
+        for (var j = 0; j < highlightedCallstack.length; j++) {
+          var compareFrameIndex = j;
+          if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) {
+            return false;
+          }
+        }
+      }
+      return true;
+    });
+  },
+  getHistogramData: function HistogramView__getHistogramData() {
+    return this._histogramData;
+  },
+  _getStepColor: function HistogramView__getStepColor(step) {
+      if ("responsiveness" in step.extraInfo) {
+        var res = step.extraInfo.responsiveness;
+        var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness));
+        return "rgb(" + redComponent + ",0,0)";
+      }
+
+      return "rgb(0,0,0)";
+  },
+};
+
+function RangeSelector(graph, histogram) {
+  this._histogram = histogram;
+  this.container = document.createElement("div");
+  this.container.className = "rangeSelectorContainer";
+  this._graph = graph;
+  this._selectedRange = { startX: 0, endX: 0 };
+  this._selectedSampleRange = { start: 0, end: 0 };
+
+  this._highlighter = document.createElement("div");
+  this._highlighter.className = "histogramHilite collapsed";
+  this.container.appendChild(this._highlighter);
+
+  this._mouseMarker = document.createElement("div");
+  this._mouseMarker.className = "histogramMouseMarker";
+  this.container.appendChild(this._mouseMarker);
+}
+RangeSelector.prototype = {
+  getContainer: function RangeSelector_getContainer() {
+    return this.container;
+  },
+  // echo the location off the mouse on the histogram
+  drawMouseMarker: function RangeSelector_drawMouseMarker(x) {
+    console.log("Draw");
+    var mouseMarker = this._mouseMarker;
+    mouseMarker.style.left = x + "px";
+  },
+  showVideoPosition: function RangeSelector_showVideoPosition(position) {
+    this.drawMouseMarker(position * (this._graph.parentNode.clientWidth-1));
+    PROFILERLOG("Show video position: " + position);
+  },
+  drawHiliteRectangle: function RangeSelector_drawHiliteRectangle(x, y, width, height) {
+    var hilite = this._highlighter;
+    hilite.style.left = x + "px";
+    hilite.style.top = "0";
+    hilite.style.width = width + "px";
+    hilite.style.height = height + "px";
+  },
+  clearCurrentRangeSelection: function RangeSelector_clearCurrentRangeSelection() {
+    try {
+      this.changeEventSuppressed = true;
+      var children = this.selector.childNodes;
+      for (var i = 0; i < children.length; ++i) {
+        children[i].selected = false;
+      }
+    } finally {
+      this.changeEventSuppressed = false;
+    }
+  },
+  showVideoRange: function RangeSelector_showVideoRange(startIndex, endIndex) {
+    if (!endIndex || endIndex < 0)
+      endIndex = gCurrentlyShownSampleData.length;
+
+    var len = this._graph.parentNode.getBoundingClientRect().right - this._graph.parentNode.getBoundingClientRect().left;
+    this._selectedRange.startX = startIndex * len / this._histogram._histogramData.length;
+    this._selectedRange.endX = endIndex * len / this._histogram._histogramData.length;
+    var width = this._selectedRange.endX - this._selectedRange.startX;
+    var height = this._graph.parentNode.clientHeight;
+    this._highlighter.classList.remove("collapsed");
+    this.drawHiliteRectangle(this._selectedRange.startX, 0, width, height);
+    //this._finishSelection(startIndex, endIndex);
+  },
+  enableRangeSelectionOnHistogram: function RangeSelector_enableRangeSelectionOnHistogram() {
+    var graph = this._graph;
+    var isDrawingRectangle = false;
+    var origX, origY;
+    var self = this;
+    function histogramClick(clickX, clickY) {
+      clickX = Math.min(clickX, graph.parentNode.getBoundingClientRect().right);
+      clickX = clickX - graph.parentNode.getBoundingClientRect().left;
+      var index = self._histogramIndexFromPoint(clickX);
+      self._histogram.histogramClick(index);
+    }
+    function updateHiliteRectangle(newX, newY) {
+      newX = Math.min(newX, graph.parentNode.getBoundingClientRect().right);
+      var startX = Math.min(newX, origX) - graph.parentNode.getBoundingClientRect().left;
+      var startY = 0;
+      var width = Math.abs(newX - origX);
+      var height = graph.parentNode.clientHeight;
+      if (startX < 0) {
+        width += startX;
+        startX = 0;
+      }
+      self._selectedRange.startX = startX;
+      self._selectedRange.endX = startX + width;
+      self.drawHiliteRectangle(startX, startY, width, height);
+    }
+    function updateMouseMarker(newX) {
+      self.drawMouseMarker(newX - graph.parentNode.getBoundingClientRect().left);
+    }
+    graph.addEventListener("mousedown", function(e) {
+      if (e.button != 0)
+        return;
+      graph.style.cursor = "col-resize";
+      isDrawingRectangle = true;
+      self.beginHistogramSelection();
+      origX = e.pageX;
+      origY = e.pageY;
+      if (this.setCapture)
+        this.setCapture();
+      // Reset the highlight rectangle
+      updateHiliteRectangle(e.pageX, e.pageY);
+      e.preventDefault();
+      this._movedDuringClick = false;
+    }, false);
+    graph.addEventListener("mouseup", function(e) {
+      graph.style.cursor = "default";
+      if (!this._movedDuringClick) {
+        isDrawingRectangle = false;
+        // Handle as a click on the histogram. Select the sample:
+        histogramClick(e.pageX, e.pageY);
+      } else if (isDrawingRectangle) {
+        isDrawingRectangle = false;
+        updateHiliteRectangle(e.pageX, e.pageY);
+        self.finishHistogramSelection(e.pageX != origX);
+        if (e.pageX == origX) {
+          // Simple click in the histogram
+          var index = self._sampleIndexFromPoint(e.pageX - graph.parentNode.getBoundingClientRect().left);
+          // TODO Select this sample in the tree view
+          var sample = gCurrentlyShownSampleData[index];
+          console.log("Should select: " + sample);
+        }
+      }
+    }, false);
+    graph.addEventListener("mousemove", function(e) {
+      this._movedDuringClick = true;
+      if (isDrawingRectangle) {
+        console.log(e.pageX);
+        updateMouseMarker(-1); // Clear
+        updateHiliteRectangle(e.pageX, e.pageY);
+      } else {
+        updateMouseMarker(e.pageX);
+      }
+    }, false);
+    graph.addEventListener("mouseout", function(e) {
+      updateMouseMarker(-1); // Clear
+    }, false);
+  },
+  beginHistogramSelection: function RangeSelector_beginHistgramSelection() {
+    var hilite = this._highlighter;
+    hilite.classList.remove("finished");
+    hilite.classList.add("selecting");
+    hilite.classList.remove("collapsed");
+    if (this._transientRestrictionEnteringAffordance) {
+      this._transientRestrictionEnteringAffordance.discard();
+    }
+  },
+  _finishSelection: function RangeSelector__finishSelection(start, end) {
+    var newFilterChain = gSampleFilters.concat({ type: "RangeSampleFilter", start: start, end: end });
+    var self = this;
+    self._transientRestrictionEnteringAffordance = gBreadcrumbTrail.add({
+      title: "Sample Range [" + start + ", " + (end + 1) + "]",
+      enterCallback: function () {
+        gSampleFilters = newFilterChain;
+        self.collapseHistogramSelection();
+        filtersChanged();
+      }
+    });
+  },
+  finishHistogramSelection: function RangeSelector_finishHistgramSelection(isSomethingSelected) {
+    var self = this;
+    var hilite = this._highlighter;
+    hilite.classList.remove("selecting");
+    if (isSomethingSelected) {
+      hilite.classList.add("finished");
+      var start = this._sampleIndexFromPoint(this._selectedRange.startX);
+      var end = this._sampleIndexFromPoint(this._selectedRange.endX);
+      self._finishSelection(start, end);
+    } else {
+      hilite.classList.add("collapsed");
+    }
+  },
+  collapseHistogramSelection: function RangeSelector_collapseHistogramSelection() {
+    var hilite = this._highlighter;
+    hilite.classList.add("collapsed");
+  },
+  _sampleIndexFromPoint: function RangeSelector__sampleIndexFromPoint(x) {
+    // XXX this is completely wrong, fix please
+    var totalSamples = parseFloat(gCurrentlyShownSampleData.length);
+    var width = parseFloat(this._graph.parentNode.clientWidth);
+    var factor = totalSamples / width;
+    return parseInt(parseFloat(x) * factor);
+  },
+  _histogramIndexFromPoint: function RangeSelector__histogramIndexFromPoint(x) {
+    // XXX this is completely wrong, fix please
+    var totalSamples = parseFloat(this._histogram._histogramData.length);
+    var width = parseFloat(this._graph.parentNode.clientWidth);
+    var factor = totalSamples / width;
+    return parseInt(parseFloat(x) * factor);
+  },
+};
+
+function videoPaneTimeChange(video) {
+  if (!gMeta || !gMeta.frameStart)
+    return;
+
+  var frame = gVideoPane.getCurrentFrameNumber();
+  //var frameStart = gMeta.frameStart[frame];
+  //var frameEnd = gMeta.frameStart[frame+1]; // If we don't have a frameEnd assume the end of the profile
+
+  gHistogramView.showVideoFramePosition(frame);
+}
+
+
+window.onpopstate = function(ev) {
+  if (!gBreadcrumbTrail)
+    return;
+  console.log("pop: " + JSON.stringify(ev.state));
+  gBreadcrumbTrail.pop();
+  if (ev.state) {
+    console.log("state");
+    if (ev.state.action === "popbreadcrumb") {
+      console.log("bread");
+      //gBreadcrumbTrail.pop();
+    }
+  }
+}
+
+function BreadcrumbTrail() {
+  this._breadcrumbs = [];
+  this._selectedBreadcrumbIndex = -1;
+
+  this._containerElement = document.createElement("div");
+  this._containerElement.className = "breadcrumbTrail";
+  var self = this;
+  this._containerElement.addEventListener("click", function (e) {
+    if (!e.target.classList.contains("breadcrumbTrailItem"))
+      return;
+    self._enter(e.target.breadcrumbIndex);
+  });
+}
+BreadcrumbTrail.prototype = {
+  getContainer: function BreadcrumbTrail_getContainer() {
+    return this._containerElement;
+  },
+  /**
+   * Add a breadcrumb. The breadcrumb parameter is an object with the following
+   * properties:
+   *  - title: The text that will be shown in the breadcrumb's button.
+   *  - enterCallback: A function that will be called when entering this
+   *                   breadcrumb.
+   */
+  add: function BreadcrumbTrail_add(breadcrumb) {
+    for (var i = this._breadcrumbs.length - 1; i > this._selectedBreadcrumbIndex; i--) {
+      var rearLi = this._breadcrumbs[i];
+      if (!rearLi.breadcrumbIsTransient)
+        throw "Can only add new breadcrumbs if after the current one there are only transient ones.";
+      rearLi.breadcrumbDiscarder.discard();
+    }
+    var div = document.createElement("div");
+    div.className = "breadcrumbTrailItem";
+    div.textContent = breadcrumb.title;
+    var index = this._breadcrumbs.length;
+    div.breadcrumbIndex = index;
+    div.breadcrumbEnterCallback = breadcrumb.enterCallback;
+    div.breadcrumbIsTransient = true;
+    div.style.zIndex = 1000 - index;
+    this._containerElement.appendChild(div);
+    this._breadcrumbs.push(div);
+    if (index == 0)
+      this._enter(index);
+    var self = this;
+    div.breadcrumbDiscarder = {
+      discard: function () {
+        if (div.breadcrumbIsTransient) {
+          self._deleteBeyond(index - 1);
+          delete div.breadcrumbIsTransient;
+          delete div.breadcrumbDiscarder;
+        }
+      }
+    };
+    return div.breadcrumbDiscarder;
+  },
+  addAndEnter: function BreadcrumbTrail_addAndEnter(breadcrumb) {
+    var removalHandle = this.add(breadcrumb);
+    this._enter(this._breadcrumbs.length - 1);
+  },
+  pop : function BreadcrumbTrail_pop() {
+    if (this._breadcrumbs.length-2 >= 0)
+      this._enter(this._breadcrumbs.length-2);
+  },
+  _enter: function BreadcrumbTrail__select(index) {
+    if (index == this._selectedBreadcrumbIndex)
+      return;
+    gTreeManager.saveSelectionSnapshot();
+    var prevSelected = this._breadcrumbs[this._selectedBreadcrumbIndex];
+    if (prevSelected)
+      prevSelected.classList.remove("selected");
+    var li = this._breadcrumbs[index];
+    if (this === gBreadcrumbTrail && index != 0) {
+      // Support for back button, disabled until the forward button is implemented.
+      //var state = {action: "popbreadcrumb",};
+      //window.history.pushState(state, "Cleopatra");
+    }
+    if (!li)
+      console.log("li at index " + index + " is null!");
+    delete li.breadcrumbIsTransient;
+    li.classList.add("selected");
+    this._deleteBeyond(index);
+    this._selectedBreadcrumbIndex = index;
+    li.breadcrumbEnterCallback();
+    // Add history state
+  },
+  _deleteBeyond: function BreadcrumbTrail__deleteBeyond(index) {
+    while (this._breadcrumbs.length > index + 1) {
+      this._hide(this._breadcrumbs[index + 1]);
+      this._breadcrumbs.splice(index + 1, 1);
+    }
+  },
+  _hide: function BreadcrumbTrail__hide(breadcrumb) {
+    delete breadcrumb.breadcrumbIsTransient;
+    breadcrumb.classList.add("deleted");
+    setTimeout(function () {
+      breadcrumb.parentNode.removeChild(breadcrumb);
+    }, 1000);
+  },
+};
+
+function maxResponsiveness() {
+  var data = gCurrentlyShownSampleData;
+  var maxRes = 0.0;
+  for (var i = 0; i < data.length; ++i) {
+    if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["responsiveness"])
+      continue;
+    if (maxRes < data[i].extraInfo["responsiveness"])
+      maxRes = data[i].extraInfo["responsiveness"];
+  }
+  return maxRes;
+}
+
+function effectiveInterval() {
+  var data = gCurrentlyShownSampleData;
+  var interval = 0.0;
+  var sampleCount = 0;
+  var timeCount = 0;
+  var lastTime = null;
+  for (var i = 0; i < data.length; ++i) {
+    if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["time"]) {
+      lastTime = null;
+      continue;
+    }
+    if (lastTime) {
+      sampleCount++;
+      timeCount += data[i].extraInfo["time"] - lastTime;
+    }
+    lastTime = data[i].extraInfo["time"];
+  }
+  var effectiveInterval = timeCount/sampleCount;
+  // Biggest diff
+  var biggestDiff = 0;
+  lastTime = null;
+  for (var i = 0; i < data.length; ++i) {
+    if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["time"]) {
+      lastTime = null;
+      continue;
+    }
+    if (lastTime) {
+      if (biggestDiff < Math.abs(effectiveInterval - (data[i].extraInfo["time"] - lastTime)))
+        biggestDiff = Math.abs(effectiveInterval - (data[i].extraInfo["time"] - lastTime));
+    }
+    lastTime = data[i].extraInfo["time"];
+  }
+
+  if (effectiveInterval != effectiveInterval)
+    return "Time info not collected";
+
+  return (effectiveInterval).toFixed(2) + " ms ±" + biggestDiff.toFixed(2);
+}
+
+function numberOfCurrentlyShownSamples() {
+  var data = gCurrentlyShownSampleData;
+  var num = 0;
+  for (var i = 0; i < data.length; ++i) {
+    if (data[i])
+      num++;
+  }
+  return num;
+}
+
+function avgResponsiveness() {
+  var data = gCurrentlyShownSampleData;
+  var totalRes = 0.0;
+  for (var i = 0; i < data.length; ++i) {
+    if (!data[i] || !data[i].extraInfo || !data[i].extraInfo["responsiveness"])
+      continue;
+    totalRes += data[i].extraInfo["responsiveness"];
+  }
+  return totalRes / numberOfCurrentlyShownSamples();
+}
+
+function copyProfile() {
+  window.prompt ("Copy to clipboard: Ctrl+C, Enter", document.getElementById("data").value);
+}
+
+function saveProfileToLocalStorage() {
+  Parser.getSerializedProfile(true, function (serializedProfile) {
+    gLocalStorage.storeLocalProfile(serializedProfile, function profileSaved() {
+
+    });
+  });
+}
+function downloadProfile() {
+  Parser.getSerializedProfile(true, function (serializedProfile) {
+    var blob = new Blob([serializedProfile], { "type": "application/octet-stream" });
+    location.href = window.URL.createObjectURL(blob);
+  });
+}
+
+function uploadProfile(selected) {
+  Parser.getSerializedProfile(!selected, function (dataToUpload) {
+    var oXHR = new XMLHttpRequest();
+    oXHR.open("POST", "http://profile-store.appspot.com/store", true);
+    oXHR.onload = function (oEvent) {
+      if (oXHR.status == 200) {
+        document.getElementById("upload_status").innerHTML = "Success! Use this <a href='" + document.URL.split('?')[0] + "?report=" + oXHR.responseText + "'>link</a>";
+      } else {
+        document.getElementById("upload_status").innerHTML = "Error " + oXHR.status + " occurred uploading your file.";
+      }
+    };
+    oXHR.onerror = function (oEvent) {
+      document.getElementById("upload_status").innerHTML = "Error " + oXHR.status + " occurred uploading your file.";
+    }
+    oXHR.onprogress = function (oEvent) {
+      if (oEvent.lengthComputable) {
+        document.getElementById("upload_status").innerHTML = "Uploading: " + ((oEvent.loaded / oEvent.total)*100) + "%";
+      }
+    }
+
+    var dataSize;
+    if (dataToUpload.length > 1024*1024) {
+      dataSize = (dataToUpload.length/1024/1024) + " MB(s)";
+    } else {
+      dataSize = (dataToUpload.length/1024) + " KB(s)";
+    }
+
+    var formData = new FormData();
+    formData.append("file", dataToUpload);
+    document.getElementById("upload_status").innerHTML = "Uploading Profile (" + dataSize + ")";
+    oXHR.send(formData);
+  });
+}
+
+function populate_skip_symbol() {
+  var skipSymbolCtrl = document.getElementById('skipsymbol')
+  //skipSymbolCtrl.options = gSkipSymbols;
+  for (var i = 0; i < gSkipSymbols.length; i++) {
+    var elOptNew = document.createElement('option');
+    elOptNew.text = gSkipSymbols[i];
+    elOptNew.value = gSkipSymbols[i];
+    elSel.add(elOptNew);
+  }
+
+}
+
+function delete_skip_symbol() {
+  var skipSymbol = document.getElementById('skipsymbol').value
+}
+
+function add_skip_symbol() {
+
+}
+
+var gFilterChangeCallback = null;
+var gFilterChangeDelay = 1200;
+function filterOnChange() {
+  if (gFilterChangeCallback != null) {
+    clearTimeout(gFilterChangeCallback);
+    gFilterChangeCallback = null;
+  }
+
+  gFilterChangeCallback = setTimeout(filterUpdate, gFilterChangeDelay);
+}
+function filterUpdate() {
+  gFilterChangeCallback = null;
+
+  filtersChanged();
+
+  var filterNameInput = document.getElementById("filterName");
+  if (filterNameInput != null) {
+    filterNameInput.focus();
+  }
+}
+
+// Maps document id to a tooltip description
+var tooltip = {
+  "mergeFunctions" : "Ignore line information and merge samples based on function names.",
+  "showJank" : "Show only samples with >50ms responsiveness.",
+  "showJS" : "Show only samples which involve running chrome or content Javascript code.",
+  "mergeUnbranched" : "Collapse unbranched call paths in the call tree into a single node.",
+  "filterName" : "Show only samples with a frame containing the filter as a substring.",
+  "invertCallstack" : "Invert the callstack (Heavy view) to find the most expensive leaf functions.",
+  "upload" : "Upload the full profile to public cloud storage to share with others.",
+  "upload_select" : "Upload only the selected view.",
+  "download" : "Initiate a download of the full profile.",
+}
+
+function addTooltips() {
+  for (var elemId in tooltip) {
+    var elem = document.getElementById(elemId);
+    if (!elem)
+      continue;
+    if (elem.parentNode.nodeName.toLowerCase() == "label")
+      elem = elem.parentNode;
+    elem.title = tooltip[elemId];
+  }
+}
+
+function InfoBar() {
+  this._container = document.createElement("div");
+  this._container.id = "infoBar";
+  this._container.className = "sideBar";
+}
+
+InfoBar.prototype = {
+  getContainer: function InfoBar_getContainer() {
+    return this._container;
+  },
+  display: function InfoBar_display() {
+    function getMetaFeatureString() {
+      features = "<dt>Stackwalk:</dt><dd>" + (gMeta.stackwalk ? "True" : "False") + "</dd>";
+      features += "<dt>Jank:</dt><dd>" + (gMeta.stackwalk ? "True" : "False") + "</dd>";
+      return features;
+    }
+    function getPlatformInfo() {
+      return gMeta.oscpu + " (" + gMeta.toolkit + ")";
+    }
+    var infobar = this._container;
+    var infoText = "";
+
+    if (gMeta) {
+      infoText += "<h2>Profile Info</h2>\n<dl>\n";
+      infoText += "<dt>Product:</dt><dd>" + gMeta.product + "</dd>";
+      infoText += "<dt>Platform:</dt><dd>" + getPlatformInfo() + "</dd>";
+      infoText += getMetaFeatureString();
+      infoText += "<dt>Interval:</dt><dd>" + gMeta.interval + " ms</dd></dl>";
+    }
+    infoText += "<h2>Selection Info</h2>\n<dl>\n";
+    infoText += "  <dt>Avg. Responsiveness:</dt><dd>" + avgResponsiveness().toFixed(2) + " ms</dd>\n";
+    infoText += "  <dt>Max Responsiveness:</dt><dd>" + maxResponsiveness().toFixed(2) + " ms</dd>\n";
+    infoText += "  <dt>Real Interval:</dt><dd>" + effectiveInterval() + "</dd>";
+    infoText += "</dl>\n";
+    infoText += "<h2>Pre Filtering</h2>\n";
+    // Disable for now since it's buggy and not useful
+    //infoText += "<label><input type='checkbox' id='mergeFunctions' " + (gMergeFunctions ?" checked='true' ":" ") + " onchange='toggleMergeFunctions()'/>Functions, not lines</label><br>\n";
+    infoText += "<label><input type='checkbox' id='showJS' " + (gJavascriptOnly ?" checked='true' ":" ") + " onchange='toggleJavascriptOnly()'/>Javascript only</label><br>\n";
+
+    var filterNameInputOld = document.getElementById("filterName");
+    infoText += "<label>Filter:\n";
+    infoText += "<input type='search' id='filterName' oninput='filterOnChange()'/></label>\n";
+
+    infoText += "<h2>Post Filtering</h2>\n";
+    infoText += "<label><input type='checkbox' id='showJank' " + (gJankOnly ?" checked='true' ":" ") + " onchange='toggleJank()'/>Show Jank only</label>\n";
+    infoText += "<h2>View Options</h2>\n";
+    infoText += "<label><input type='checkbox' id='mergeUnbranched' " + (gMergeUnbranched ?" checked='true' ":" ") + " onchange='toggleMergeUnbranched()'/>Merge unbranched call paths</label><br>\n";
+    infoText += "<label><input type='checkbox' id='invertCallstack' " + (gInvertCallstack ?" checked='true' ":" ") + " onchange='toggleInvertCallStack()'/>Invert callstack</label><br>\n";
+
+    infoText += "<h2>Share</h2>\n";
+    infoText += "<div id='upload_status' aria-live='polite'>No upload in progress</div><br>\n";
+    infoText += "<input type='button' id='upload' value='Upload full profile'>\n";
+    infoText += "<input type='button' id='upload_select' value='Upload view'><br>\n";
+    infoText += "<input type='button' id='download' value='Download full profile'>\n";
+
+    //infoText += "<br>\n";
+    //infoText += "Skip functions:<br>\n";
+    //infoText += "<select size=8 id='skipsymbol'></select><br />"
+    //infoText += "<input type='button' id='delete_skipsymbol' value='Delete'/><br />\n";
+    //infoText += "<input type='button' id='add_skipsymbol' value='Add'/><br />\n";
+
+    infobar.innerHTML = infoText;
+    addTooltips();
+
+    var filterNameInputNew = document.getElementById("filterName");
+    if (filterNameInputOld != null && filterNameInputNew != null) {
+      filterNameInputNew.parentNode.replaceChild(filterNameInputOld, filterNameInputNew);
+      //filterNameInputNew.value = filterNameInputOld.value;
+    }
+    document.getElementById('upload').onclick = function() {
+      uploadProfile(false);
+    };
+    document.getElementById('download').onclick = downloadProfile;
+    document.getElementById('upload_select').onclick = function() {
+      uploadProfile(true);
+    };
+    //document.getElementById('delete_skipsymbol').onclick = delete_skip_symbol;
+    //document.getElementById('add_skipsymbol').onclick = add_skip_symbol;
+
+    //populate_skip_symbol();
+  }
+}
+
+// in light mode we simplify the UI by default
+var gLightMode = false;
+var gNumSamples = 0;
+var gMeta = null;
+var gSymbols = {};
+var gFunctions = {};
+var gHighlightedCallstack = [];
+var gTreeManager = null;
+var gSampleBar = null;
+var gBreadcrumbTrail = null;
+var gHistogramView = null;
+var gDiagnosticBar = null;
+var gVideoPane = null;
+var gPluginView = null;
+var gFileList = null;
+var gInfoBar = null;
+var gMainArea = null;
+var gCurrentlyShownSampleData = null;
+var gSkipSymbols = ["test2", "test1"];
+var gAppendVideoCapture = null;
+
+function getTextData() {
+  var data = [];
+  var samples = gCurrentlyShownSampleData;
+  for (var i = 0; i < samples.length; i++) {
+    data.push(samples[i].lines.join("\n"));
+  }
+  return data.join("\n");
+}
+
+function loadProfileFile(fileList) {
+  if (fileList.length == 0)
+    return;
+  var file = fileList[0];
+  var reporter = enterProgressUI();
+  var subreporters = reporter.addSubreporters({
+    fileLoading: 1000,
+    parsing: 1000
+  });
+
+  var reader = new FileReader();
+  reader.onloadend = function () {
+    subreporters.fileLoading.finish();
+    loadRawProfile(subreporters.parsing, reader.result);
+  };
+  reader.onprogress = function (e) {
+    subreporters.fileLoading.setProgress(e.loaded / e.total);
+  };
+  reader.readAsText(file, "utf-8");
+  subreporters.fileLoading.begin("Reading local file...");
+}
+
+function loadLocalStorageProfile(profileKey) {
+  var reporter = enterProgressUI();
+  var subreporters = reporter.addSubreporters({
+    fileLoading: 1000,
+    parsing: 1000
+  });
+
+  gLocalStorage.getProfile(profileKey, function(profile) {
+    subreporters.fileLoading.finish();
+    loadRawProfile(subreporters.parsing, JSON.stringify(profile));
+  });
+  subreporters.fileLoading.begin("Reading local storage...");
+}
+
+function appendVideoCapture(videoCapture) {
+  if (videoCapture.indexOf("://") == -1) {
+    videoCapture = EIDETICKER_BASE_URL + videoCapture;
+  }
+  gAppendVideoCapture = videoCapture;
+}
+
+function loadZippedProfileURL(url) {
+  var reporter = enterProgressUI();
+  var subreporters = reporter.addSubreporters({
+    fileLoading: 1000,
+    parsing: 1000
+  });
+
+  // Crude way to detect if we're using a relative URL or not :(
+  if (url.indexOf("://") == -1) {
+    url = EIDETICKER_BASE_URL + url;
+  }
+  reporter.begin("Fetching " + url);
+  PROFILERTRACE("Fetch url: " + url);
+
+  function onerror(e) {
+    PROFILERERROR("zip.js error");
+    PROFILERERROR(JSON.stringify(e));
+  }
+
+  zip.workerScriptsPath = "js/zip.js/";
+  zip.createReader(new zip.HttpReader(url), function(zipReader) {
+    subreporters.fileLoading.setProgress(0.4);
+    zipReader.getEntries(function(entries) {
+      for (var i = 0; i < entries.length; i++) {
+        var entry = entries[i];
+        PROFILERTRACE("Zip file: " + entry.filename);
+        if (entry.filename === "symbolicated_profile.txt") {
+          reporter.begin("Decompressing " + url);
+          subreporters.fileLoading.setProgress(0.8);
+          entry.getData(new zip.TextWriter(), function(profileText) {
+            subreporters.fileLoading.finish();
+            loadRawProfile(subreporters.parsing, profileText);
+          });
+          return;
+        }
+        onerror("symbolicated_profile.txt not found in zip file.");
+      }
+    });
+  }, onerror);
+}
+
+function loadProfileURL(url) {
+  var reporter = enterProgressUI();
+  var subreporters = reporter.addSubreporters({
+    fileLoading: 1000,
+    parsing: 1000
+  });
+
+  var xhr = new XMLHttpRequest();
+  xhr.open("GET", url, true);
+  xhr.responseType = "text";
+  xhr.onreadystatechange = function (e) {
+    if (xhr.readyState === 4 && xhr.status === 200) {
+      subreporters.fileLoading.finish();
+      PROFILERLOG("Got profile from '" + url + "'.");
+      loadRawProfile(subreporters.parsing, xhr.responseText);
+    }
+  };
+  xhr.onerror = function (e) {
+    subreporters.fileLoading.begin("Error fetching profile :(. URL:  " + url);
+  }
+  xhr.onprogress = function (e) {
+    if (e.lengthComputable && (e.loaded <= e.total)) {
+      subreporters.fileLoading.setProgress(e.loaded / e.total);
+    } else {
+      subreporters.fileLoading.setProgress(NaN);
+    }
+  };
+  xhr.send(null);
+  subreporters.fileLoading.begin("Loading remote file...");
+}
+
+function loadProfile(rawProfile) {
+  if (!rawProfile)
+    return;
+  var reporter = enterProgressUI();
+  loadRawProfile(reporter, rawProfile);
+}
+
+function loadRawProfile(reporter, rawProfile) {
+  PROFILERLOG("Parse raw profile: ~" + rawProfile.length + " bytes");
+  reporter.begin("Parsing...");
+  var startTime = Date.now();
+  var parseRequest = Parser.parse(rawProfile, {
+    appendVideoCapture : gAppendVideoCapture,
+  });
+  gVideoCapture = null;
+  parseRequest.addEventListener("progress", function (progress, action) {
+    if (action)
+      reporter.setAction(action);
+    reporter.setProgress(progress);
+  });
+  parseRequest.addEventListener("finished", function (result) {
+    console.log("parsing (in worker): " + (Date.now() - startTime) + "ms");
+    reporter.finish();
+    gMeta = result.meta;
+    gNumSamples = result.numSamples;
+    gSymbols = result.symbols;
+    gFunctions = result.functions;
+    enterFinishedProfileUI();
+    if (gFileList)
+      gFileList.profileParsingFinished();
+  });
+}
+
+var gImportFromAddonSubreporters = null;
+
+window.addEventListener("message", function messageFromAddon(msg) {
+  // This is triggered by the profiler add-on.
+  var o = JSON.parse(msg.data);
+  switch (o.task) {
+    case "importFromAddonStart":
+      var totalReporter = enterProgressUI();
+      gImportFromAddonSubreporters = totalReporter.addSubreporters({
+        import: 10000,
+        parsing: 1000
+      });
+      gImportFromAddonSubreporters.import.begin("Symbolicating...");
+      break;
+    case "importFromAddonProgress":
+      gImportFromAddonSubreporters.import.setProgress(o.progress);
+      if (o.action != null) {
+          gImportFromAddonSubreporters.import.setAction(o.action);
+      }
+      break;
+    case "importFromAddonFinish":
+      importFromAddonFinish(o.rawProfile);
+      break;
+    case "receiveProfileData":
+      receiveProfileData(o.rawProfile);
+      break;
+  }
+});
+
+function receiveProfileData(data) {
+ loadProfile(JSON.stringify(data));
+}
+
+function importFromAddonFinish(rawProfile) {
+  gImportFromAddonSubreporters.import.finish();
+  loadRawProfile(gImportFromAddonSubreporters.parsing, rawProfile);
+}
+
+var gInvertCallstack = false;
+function toggleInvertCallStack() {
+  gTreeManager.saveReverseSelectionSnapshot(gJavascriptOnly);
+  gInvertCallstack = !gInvertCallstack;
+  var startTime = Date.now();
+  viewOptionsChanged();
+  console.log("invert time: " + (Date.now() - startTime) + "ms");
+}
+
+var gMergeUnbranched = false;
+function toggleMergeUnbranched() {
+  gMergeUnbranched = !gMergeUnbranched;
+  viewOptionsChanged();
+}
+
+var gMergeFunctions = true;
+function toggleMergeFunctions() {
+  gMergeFunctions = !gMergeFunctions;
+  filtersChanged();
+}
+
+var gJankOnly = false;
+var gJankThreshold = 50 /* ms */;
+function toggleJank(/* optional */ threshold) {
+  // Currently we have no way to change the threshold in the UI
+  // once we add this we will need to change the tooltip.
+  gJankOnly = !gJankOnly;
+  if (threshold != null ) {
+    gJankThreshold = threshold;
+  }
+  filtersChanged();
+}
+
+var gJavascriptOnly = false;
+function toggleJavascriptOnly() {
+  if (gJavascriptOnly) {
+    // When going from JS only to non js there's going to be new C++
+    // frames in the selection so we need to restore the selection
+    // while allowing non contigous symbols to be in the stack (the c++ ones)
+    gTreeManager.setAllowNonContigous();
+  }
+  gJavascriptOnly = !gJavascriptOnly;
+  gTreeManager.saveSelectionSnapshot(gJavascriptOnly);
+  filtersChanged();
+}
+
+var gSampleFilters = [];
+function focusOnSymbol(focusSymbol, name) {
+  var newFilterChain = gSampleFilters.concat([{type: "FocusedFrameSampleFilter", focusedSymbol: focusSymbol}]);
+  gBreadcrumbTrail.addAndEnter({
+    title: name,
+    enterCallback: function () {
+      gSampleFilters = newFilterChain;
+      filtersChanged();
+    }
+  });
+}
+
+function focusOnCallstack(focusedCallstack, name) {
+  var filter = {
+    type: gInvertCallstack ? "FocusedCallstackPostfixSampleFilter" : "FocusedCallstackPrefixSampleFilter",
+    focusedCallstack: focusedCallstack
+  };
+  var newFilterChain = gSampleFilters.concat([filter]);
+  gBreadcrumbTrail.addAndEnter({
+    title: name,
+    enterCallback: function () {
+      gSampleFilters = newFilterChain;
+      filtersChanged();
+    }
+  })
+}
+
+function focusOnPluginView(pluginName, param) {
+  var filter = {
+    type: "PluginView",
+    pluginName: pluginName,
+    param: param,
+  };
+  var newFilterChain = gSampleFilters.concat([filter]);
+  gBreadcrumbTrail.addAndEnter({
+    title: "Plugin View: " + pluginName,
+    enterCallback: function () {
+      gSampleFilters = newFilterChain;
+      filtersChanged();
+    }
+  })
+}
+
+function viewJSSource(sample) {
+  var sourceView = new SourceView();
+  sourceView.setScriptLocation(sample.scriptLocation);
+  sourceView.setSource(gMeta.js.source[sample.scriptLocation.scriptURI]);
+  gMainArea.appendChild(sourceView.getContainer());
+
+}
+
+function setHighlightedCallstack(samples, heaviestSample) {
+  PROFILERTRACE("highlight: " + samples);
+  gHighlightedCallstack = samples;
+  gHistogramView.highlightedCallstackChanged(gHighlightedCallstack);
+  if (!gInvertCallstack) {
+    // Always show heavy
+    heaviestSample = heaviestSample.clone().reverse();
+  }
+  if (gSampleBar)
+    gSampleBar.setSample(heaviestSample);
+}
+
+function enterMainUI(isLightMode) {
+  if (isLightMode !== undefined) {
+    gLightMode = isLightMode;
+    if (gLightMode) {
+      gJavascriptOnly = true;
+    }
+  }
+
+  var uiContainer = document.createElement("div");
+  uiContainer.id = "ui";
+
+  //gFileList.loadProfileListFromLocalStorage();
+  if (!gLightMode) {
+    gFileList = new FileList();
+    uiContainer.appendChild(gFileList.getContainer());
+
+    gFileList.addFile();
+    gInfoBar = new InfoBar();
+    uiContainer.appendChild(gInfoBar.getContainer());
+  }
+
+  gMainArea = document.createElement("div");
+  gMainArea.id = "mainarea";
+  uiContainer.appendChild(gMainArea);
+  document.body.appendChild(uiContainer);
+
+  var profileEntryPane = document.createElement("div");
+  profileEntryPane.className = "profileEntryPane";
+  profileEntryPane.innerHTML = '' +
+    '<h1>Upload your profile here:</h1>' +
+    '<input type="file" id="datafile" onchange="loadProfileFile(this.files);">' +
+    '<h1>Or, alternatively, enter your profile data here:</h1>' +
+    '<textarea rows=20 cols=80 id=data autofocus spellcheck=false></textarea>' +
+    '<p><button onclick="loadProfile(document.getElementById(\'data\').value);">Parse</button></p>' +
+    '';
+
+  gMainArea.appendChild(profileEntryPane);
+}
+
+function enterProgressUI() {
+  var profileProgressPane = document.createElement("div");
+  profileProgressPane.className = "profileProgressPane";
+
+  var progressLabel = document.createElement("a");
+  profileProgressPane.appendChild(progressLabel);
+
+  var progressBar = document.createElement("progress");
+  profileProgressPane.appendChild(progressBar);
+
+  var totalProgressReporter = new ProgressReporter();
+  totalProgressReporter.addListener(function (r) {
+    var progress = r.getProgress();
+    progressLabel.innerHTML = r.getAction();
+    console.log("Action: " + r.getAction());
+    if (isNaN(progress))
+      progressBar.removeAttribute("value");
+    else
+      progressBar.value = progress;
+  });
+
+  gMainArea.appendChild(profileProgressPane);
+
+  Parser.updateLogSetting();
+
+  return totalProgressReporter;
+}
+
+function enterFinishedProfileUI() {
+  //dump("prepare to save\n");
+  //saveProfileToLocalStorage();
+  //dump("prepare to saved\n");
+
+  var finishedProfilePaneBackgroundCover = document.createElement("div");
+  finishedProfilePaneBackgroundCover.className = "finishedProfilePaneBackgroundCover";
+
+  var finishedProfilePane = document.createElement("table");
+  var rowIndex = 0;
+  var currRow;
+  finishedProfilePane.style.width = "100%";
+  finishedProfilePane.style.height = "100%";
+  finishedProfilePane.border = "0";
+  finishedProfilePane.cellPadding = "0";
+  finishedProfilePane.cellSpacing = "0";
+  finishedProfilePane.borderCollapse = "collapse";
+  finishedProfilePane.className = "finishedProfilePane";
+  setTimeout(function() {
+    // Work around a webkit bug. For some reason the table doesn't show up
+    // until some actions happen such as focusing this box
+    var filterNameInput = document.getElementById("filterName");
+    if (filterNameInput != null) {
+      filterNameInput.focus();
+     }
+  }, 100);
+
+  gBreadcrumbTrail = new BreadcrumbTrail();
+  currRow = finishedProfilePane.insertRow(rowIndex++);
+  currRow.insertCell(0).appendChild(gBreadcrumbTrail.getContainer());
+
+  gHistogramView = new HistogramView();
+  currRow = finishedProfilePane.insertRow(rowIndex++);
+  currRow.insertCell(0).appendChild(gHistogramView.getContainer());
+
+  if (typeof DiagnosticBar !== "undefined") {
+    gDiagnosticBar = new DiagnosticBar();
+    gDiagnosticBar.setDetailsListener(function(details) {
+      if (details.indexOf("bug ") == 0) {
+        window.open('https://bugzilla.mozilla.org/show_bug.cgi?id=' + details.substring(4));
+      } else {
+        var sourceView = new SourceView();
+        sourceView.setText("Diagnostic", js_beautify(details));
+        gMainArea.appendChild(sourceView.getContainer());
+      }
+    });
+    currRow = finishedProfilePane.insertRow(rowIndex++);
+    currRow.insertCell(0).appendChild(gDiagnosticBar.getContainer());
+  }
+
+  // For testing:
+  //gMeta.videoCapture = {
+  //  src: "http://videos-cdn.mozilla.net/brand/Mozilla_Firefox_Manifesto_v0.2_640.webm",
+  //};
+
+  if (!gLightMode && gMeta && gMeta.videoCapture) {
+    gVideoPane = new VideoPane(gMeta.videoCapture);
+    gVideoPane.onTimeChange(videoPaneTimeChange);
+    currRow = finishedProfilePane.insertRow(rowIndex++);
+    currRow.insertCell(0).appendChild(gVideoPane.getContainer());
+  }
+
+  var treeContainerDiv = document.createElement("div");
+  treeContainerDiv.className = "treeContainer";
+  treeContainerDiv.style.width = "100%";
+  treeContainerDiv.style.height = "100%";
+
+  gTreeManager = new ProfileTreeManager();
+  currRow = finishedProfilePane.insertRow(rowIndex++);
+  currRow.style.height = "100%";
+  var cell = currRow.insertCell(0);
+  cell.appendChild(treeContainerDiv);
+  treeContainerDiv.appendChild(gTreeManager.getContainer());
+
+  if (!gLightMode) {
+    gSampleBar = new SampleBar();
+    treeContainerDiv.appendChild(gSampleBar.getContainer());
+  }
+
+  // sampleBar
+
+  if (!gLightMode) {
+    gPluginView = new PluginView();
+    //currRow = finishedProfilePane.insertRow(4);
+    treeContainerDiv.appendChild(gPluginView.getContainer());
+  }
+
+  gMainArea.appendChild(finishedProfilePaneBackgroundCover);
+  gMainArea.appendChild(finishedProfilePane);
+
+  gBreadcrumbTrail.add({
+    title: "Complete Profile",
+    enterCallback: function () {
+      gSampleFilters = [];
+      filtersChanged();
+    }
+  });
+}
+
+function filtersChanged() {
+  var data = { symbols: {}, functions: {}, samples: [] };
+
+  gHistogramView.dataIsOutdated();
+  var filterNameInput = document.getElementById("filterName");
+  var updateRequest = Parser.updateFilters({
+    mergeFunctions: gMergeFunctions,
+    nameFilter: (filterNameInput && filterNameInput.value) || "",
+    sampleFilters: gSampleFilters,
+    jankOnly: gJankOnly,
+    javascriptOnly: gJavascriptOnly
+  });
+  var start = Date.now();
+  updateRequest.addEventListener("finished", function (filteredSamples) {
+    console.log("profile filtering (in worker): " + (Date.now() - start) + "ms.");
+    gCurrentlyShownSampleData = filteredSamples;
+    if (gInfoBar)
+      gInfoBar.display();
+
+    if (gPluginView && gSampleFilters.length > 0 && gSampleFilters[gSampleFilters.length-1].type === "PluginView") {
+      start = Date.now();
+      gPluginView.display(gSampleFilters[gSampleFilters.length-1].pluginName, gSampleFilters[gSampleFilters.length-1].param,
+                          gCurrentlyShownSampleData, gHighlightedCallstack);
+      console.log("plugin displaying: " + (Date.now() - start) + "ms.");
+    } else if (gPluginView) {
+      gPluginView.hide();
+    }
+  });
+
+  var histogramRequest = Parser.calculateHistogramData();
+  histogramRequest.addEventListener("finished", function (data) {
+    start = Date.now();
+    gHistogramView.display(data.histogramData, data.frameStart, data.widthSum, gHighlightedCallstack);
+    console.log("histogram displaying: " + (Date.now() - start) + "ms.");
+  });
+
+  if (gDiagnosticBar) {
+    var diagnosticsRequest = Parser.calculateDiagnosticItems(gMeta);
+    diagnosticsRequest.addEventListener("finished", function (diagnosticItems) {
+      start = Date.now();
+      gDiagnosticBar.display(diagnosticItems);
+      console.log("diagnostic items displaying: " + (Date.now() - start) + "ms.");
+    });
+  }
+
+  viewOptionsChanged();
+}
+
+function viewOptionsChanged() {
+  gTreeManager.dataIsOutdated();
+  var filterNameInput = document.getElementById("filterName");
+  var updateViewOptionsRequest = Parser.updateViewOptions({
+    invertCallstack: gInvertCallstack,
+    mergeUnbranched: gMergeUnbranched
+  });
+  updateViewOptionsRequest.addEventListener("finished", function (calltree) {
+    var start = Date.now();
+    gTreeManager.display(calltree, gSymbols, gFunctions, gMergeFunctions, filterNameInput && filterNameInput.value);
+    console.log("tree displaying: " + (Date.now() - start) + "ms.");
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/profiler.css
@@ -0,0 +1,18 @@
+.profile-name {
+  font-size: 13px;
+  padding: 8px;
+}
+
+#profiles-list > li {
+  width: 180px;
+  cursor: pointer;
+}
+
+.splitview-nav-container button {
+  color: white;
+  background-clip: padding-box;
+  border-bottom: 1px solid hsla(210,16%,76%,.1);
+  box-shadow: inset 0 -1px 0 hsla(210,8%,5%,.25);
+  -moz-padding-end: 8px;
+  -moz-box-align: center;
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/profiler.xul
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/splitview.css"?>
+<?xml-stylesheet href="chrome://browser/content/splitview.css"?>
+<?xml-stylesheet href="chrome://browser/content/profiler.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <box flex="1" id="profiler-chrome" class="splitview-root">
+    <box class="splitview-controller" width="180px">
+      <box class="splitview-main"></box>
+
+      <box class="splitview-nav-container">
+        <ol class="splitview-nav" id="profiles-list">
+          <!-- Example:
+          <li class="splitview-active" id="profile-1" data-uid="1">
+            <h1 class="profile-name">Profile 1</h1>
+          </li>
+          -->
+        </ol>
+
+        <spacer flex="1"/>
+
+        <toolbar class="devtools-toolbar" mode="full">
+          <toolbarbutton id="profiler-create"
+                         class="devtools-toolbarbutton"
+                         label="New"
+                         disabled="true"/>
+        </toolbar>
+      </box> <!-- splitview-nav-container -->
+    </box> <!-- splitview-controller -->
+
+    <box flex="1">
+      <vbox flex="1" id="profiler-report">
+        <!-- Example:
+        <iframe id="profiler-cleo-1"
+          src="devtools/cleopatra.html" flex="1"></iframe>
+        -->
+      </vbox>
+    </box>
+  </box>
+</window>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/test/Makefile.in
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH          = @DEPTH@
+topsrcdir      = @top_srcdir@
+srcdir         = @srcdir@
+VPATH          = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+		browser_profiler_run.js \
+		browser_profiler_controller.js \
+		browser_profiler_profiles.js \
+		head.js \
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_controller.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel;
+
+function test() {
+  waitForExplicitFinish();
+
+  setUp(URL, function onSetUp(tab, browser, panel) {
+    gTab = tab;
+    gPanel = panel;
+
+    testInactive(testStart);
+  });
+}
+
+function testInactive(next=function(){}) {
+  gPanel.controller.isActive(function (err, isActive) {
+    ok(!err, "isActive didn't return any errors");
+    ok(!isActive, "Profiler is not active");
+    next();
+  });
+}
+
+function testActive(next=function(){}) {
+  gPanel.controller.isActive(function (err, isActive) {
+    ok(!err, "isActive didn't return any errors");
+    ok(isActive, "Profiler is active");
+    next();
+  });
+}
+
+function testStart() {
+  gPanel.controller.start(function (err) {
+    ok(!err, "Profiler started without errors");
+    testActive(testStop);
+  });
+}
+
+function testStop() {
+  gPanel.controller.stop(function (err, data) {
+    ok(!err, "Profiler stopped without errors");
+    ok(data, "Profiler returned some data");
+
+    testInactive(function () {
+      tearDown(gTab, function () {
+        gTab = null;
+        gPanel = null;
+      });
+    });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_profiles.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel;
+
+function test() {
+  waitForExplicitFinish();
+
+  setUp(URL, function onSetUp(tab, browser, panel) {
+    gTab = tab;
+    gPanel = panel;
+
+    panel.once("profileCreated", onProfileCreated);
+    panel.once("profileSwitched", onProfileSwitched);
+
+    testNewProfile();
+  });
+}
+
+function testNewProfile() {
+  is(gPanel.profiles.size, 1, "There is only one profile");
+
+  let btn = gPanel.document.getElementById("profiler-create");
+  ok(!btn.getAttribute("disabled"), "Create Profile button is not disabled");
+  btn.click();
+}
+
+function onProfileCreated(name, uid) {
+  is(gPanel.profiles.size, 2, "There are two profiles now");
+  ok(gPanel.activeProfile.uid !== uid, "New profile is not yet active");
+
+  let btn = gPanel.document.getElementById("profile-" + uid);
+  ok(btn, "Profile item has been added to the sidebar");
+  btn.click();
+}
+
+function onProfileSwitched(name, uid) {
+  ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
+
+  tearDown(gTab, function onTearDown() {
+    gPanel = null;
+    gTab = null;
+  });
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/test/browser_profiler_run.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
+
+let gTab, gPanel, gAttempts = 0;
+
+function test() {
+  waitForExplicitFinish();
+
+  setUp(URL, function onSetUp(tab, browser, panel) {
+    gTab = tab;
+    gPanel = panel;
+
+    panel.once("started", onStart);
+    panel.once("stopped", onStop);
+    panel.once("parsed", onParsed);
+
+    testUI();
+  });
+}
+
+function attemptTearDown() {
+  gAttempts += 1;
+
+  if (gAttempts < 2) {
+    return;
+  }
+
+  tearDown(gTab, function onTearDown() {
+    gPanel = null;
+    gTab = null;
+  });
+}
+
+function getProfileInternals() {
+  let win = gPanel.activeProfile.iframe.contentWindow;
+  let doc = win.document;
+
+  return [win, doc];
+}
+
+function testUI() {
+  ok(gPanel, "Profiler panel exists");
+  ok(gPanel.activeProfile, "Active profile exists");
+
+  let [win, doc] = getProfileInternals();
+  let startButton = doc.querySelector(".controlPane #startWrapper button");
+  let stopButton = doc.querySelector(".controlPane #stopWrapper button");
+
+  ok(startButton, "Start button exists");
+  ok(stopButton, "Stop button exists");
+
+  startButton.click();
+}
+
+function onStart() {
+  gPanel.controller.isActive(function (err, isActive) {
+    ok(isActive, "Profiler is running");
+
+    let [win, doc] = getProfileInternals();
+    let stopButton = doc.querySelector(".controlPane #stopWrapper button");
+
+    stopButton.click();
+  });
+}
+
+function onStop() {
+  gPanel.controller.isActive(function (err, isActive) {
+    ok(!isActive, "Profiler is idle");
+    attemptTearDown();
+  });
+}
+
+function onParsed() {
+  function assertSample() {
+    let [win,doc] = getProfileInternals();
+    let sample = doc.getElementsByClassName("samplePercentage");
+
+    if (sample.length <= 0) {
+      return void setTimeout(assertSample, 100);
+    }
+
+    ok(sample.length > 0, "We have some items displayed");
+    is(sample[0].innerHTML, "100.0%", "First percentage is 100%");
+    attemptTearDown();
+  }
+
+  assertSample();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/profiler/test/head.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let temp = {};
+const PROFILER_ENABLED = "devtools.profiler.enabled";
+
+Cu.import("resource:///modules/devtools/Target.jsm", temp);
+let TargetFactory = temp.TargetFactory;
+
+Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
+let gDevTools = temp.gDevTools;
+
+function loadTab(url, callback) {
+  let tab = gBrowser.addTab();
+  gBrowser.selectedTab = tab;
+  content.location.assign(url);
+
+  let browser = gBrowser.getBrowserForTab(tab);
+  if (browser.contentDocument.readyState === "complete") {
+    callback(tab, browser);
+    return;
+  }
+
+  let onLoad = function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    callback(tab, browser);
+  };
+
+  browser.addEventListener("load", onLoad, true);
+}
+
+function openProfiler(tab, callback) {
+  let target = TargetFactory.forTab(tab);
+  gDevTools.showToolbox(target, "jsprofiler").then(callback);
+}
+
+function closeProfiler(tab, callback) {
+  let target = TargetFactory.forTab(tab);
+  let panel = gDevTools.getToolbox(target).getPanel("jsprofiler");
+  panel.once("destroyed", callback);
+
+  gDevTools.closeToolbox(target);
+}
+
+function setUp(url, callback=function(){}) {
+  Services.prefs.setBoolPref(PROFILER_ENABLED, true);
+
+  loadTab(url, function onTabLoad(tab, browser) {
+    openProfiler(tab, function onProfilerOpen() {
+      let target = TargetFactory.forTab(tab);
+      let panel = gDevTools.getToolbox(target).getPanel("jsprofiler");
+      callback(tab, browser, panel);
+    });
+  });
+}
+
+function tearDown(tab, callback=function(){}) {
+  closeProfiler(tab, function onProfilerClose() {
+    callback();
+
+    while (gBrowser.tabs.length > 1) {
+      gBrowser.removeCurrentTab();
+    }
+
+    finish();
+    Services.prefs.setBoolPref(PROFILER_ENABLED, false);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.properties
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE These strings are used inside the Debugger
+# which is available from the Web Developer sub-menu -> 'Debugger'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (profiler.label):
+# This string is displayed in the title of the tab when the profiler is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+profiler.label=Profiler
\ No newline at end of file
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -30,16 +30,17 @@
     locale/browser/devtools/scratchpad.dtd            (%chrome/browser/devtools/scratchpad.dtd)
     locale/browser/devtools/styleeditor.properties    (%chrome/browser/devtools/styleeditor.properties)
     locale/browser/devtools/styleeditor.dtd           (%chrome/browser/devtools/styleeditor.dtd)
     locale/browser/devtools/styleinspector.properties (%chrome/browser/devtools/styleinspector.properties)
     locale/browser/devtools/styleinspector.dtd        (%chrome/browser/devtools/styleinspector.dtd)
     locale/browser/devtools/webConsole.dtd            (%chrome/browser/devtools/webConsole.dtd)
     locale/browser/devtools/sourceeditor.properties   (%chrome/browser/devtools/sourceeditor.properties)
     locale/browser/devtools/sourceeditor.dtd          (%chrome/browser/devtools/sourceeditor.dtd)
+    locale/browser/devtools/profiler.properties       (%chrome/browser/devtools/profiler.properties)
     locale/browser/devtools/layoutview.dtd            (%chrome/browser/devtools/layoutview.dtd)
     locale/browser/devtools/responsiveUI.properties   (%chrome/browser/devtools/responsiveUI.properties)
     locale/browser/devtools/toolbox.dtd            (%chrome/browser/devtools/toolbox.dtd)
     locale/browser/devtools/inspector.dtd          (%chrome/browser/devtools/inspector.dtd)
     locale/browser/devtools/connection-screen.dtd  (%chrome/browser/devtools/connection-screen.dtd)
     locale/browser/newTab.dtd                      (%chrome/browser/newTab.dtd)
     locale/browser/newTab.properties               (%chrome/browser/newTab.properties)
     locale/browser/openLocation.dtd                (%chrome/browser/openLocation.dtd)