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 idunknown
push userunknown
push dateunknown
reviewersrcampbell
bugs795268
milestone20.0a1
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)