Bug 1077442 - Create a pseudo-PerformanceFront, r=vp
authorJordan Santell <jsantell@gmail.com>
Thu, 23 Oct 2014 12:47:00 +0200
changeset 212549 4760de1a0120ee904196e3e77b40b584f4d48ce4
parent 212548 f38d36a4224a8c951e6e0d270c5276a47a1ef789
child 212550 3adeab7e472d51d2dcfd20e3ad79e3c802748ec3
push id27720
push usercbook@mozilla.com
push dateTue, 28 Oct 2014 14:51:21 +0000
treeherdermozilla-central@a2d58c6420f4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp
bugs1077442
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1077442 - Create a pseudo-PerformanceFront, r=vp
browser/app/profile/firefox.js
browser/devtools/performance/modules/front.js
browser/devtools/performance/moz.build
browser/devtools/performance/panel.js
browser/devtools/performance/test/browser.ini
browser/devtools/performance/test/browser_perf-aaa-run-first-leaktest.js
browser/devtools/performance/test/browser_perf-data-massaging-01.js
browser/devtools/performance/test/browser_perf-data-samples.js
browser/devtools/performance/test/browser_perf-front-basic-profiler-01.js
browser/devtools/performance/test/browser_perf-front-basic-timeline-01.js
browser/devtools/performance/test/browser_perf-front-profiler-02.js
browser/devtools/performance/test/browser_perf-front-profiler-03.js
browser/devtools/performance/test/browser_perf-front-profiler-04.js
browser/devtools/performance/test/browser_perf-shared-connection-02.js
browser/devtools/performance/test/browser_perf-shared-connection-03.js
browser/devtools/performance/test/doc_simple-test.html
browser/devtools/performance/test/head.js
browser/devtools/shared/frame-script-utils.js
toolkit/devtools/server/actors/timeline.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1381,16 +1381,18 @@ pref("devtools.timeline.enabled", false)
 
 // Enable perftools via build command
 #ifdef MOZ_DEVTOOLS_PERFTOOLS
   pref("devtools.performance_dev.enabled", true);
 #else
   pref("devtools.performance_dev.enabled", false);
 #endif
 
+pref("devtools.performance.ui.show-timeline-memory", false);
+
 // The default Profiler UI settings
 pref("devtools.profiler.ui.show-platform-data", false);
 
 // The default cache UI setting
 pref("devtools.cache.disabled", false);
 
 // Enable the Network Monitor
 pref("devtools.netmonitor.enabled", true);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/modules/front.js
@@ -0,0 +1,322 @@
+/* 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, Ci, Cu, Cr } = require("chrome");
+const { extend } = require("sdk/util/object");
+const { Task } = require("resource://gre/modules/Task.jsm");
+
+loader.lazyRequireGetter(this, "Services");
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "EventEmitter",
+  "devtools/toolkit/event-emitter");
+loader.lazyRequireGetter(this, "TimelineFront",
+  "devtools/server/actors/timeline", true);
+loader.lazyRequireGetter(this, "DevToolsUtils",
+  "devtools/toolkit/DevToolsUtils");
+
+loader.lazyImporter(this, "gDevTools",
+  "resource:///modules/devtools/gDevTools.jsm");
+
+let showTimelineMemory = () => Services.prefs.getBoolPref("devtools.performance.ui.show-timeline-memory");
+
+/**
+ * A cache of all PerformanceActorsConnection instances. The keys are Target objects.
+ */
+let SharedPerformanceActors = new WeakMap();
+
+/**
+ * Instantiates a shared PerformanceActorsConnection for the specified target.
+ * Consumers must yield on `open` to make sure the connection is established.
+ *
+ * @param Target target
+ *        The target owning this connection.
+ */
+SharedPerformanceActors.forTarget = function(target) {
+  if (this.has(target)) {
+    return this.get(target);
+  }
+
+  let instance = new PerformanceActorsConnection(target);
+  this.set(target, instance);
+  return instance;
+};
+
+/**
+ * A connection to underlying actors (profiler, memory, framerate, etc)
+ * shared by all tools in a target.
+ *
+ * Use `SharedPerformanceActors.forTarget` to make sure you get the same
+ * instance every time, and the `PerformanceFront` to start/stop recordings.
+ *
+ * @param Target target
+ *        The target owning this connection.
+ */
+function PerformanceActorsConnection(target) {
+  EventEmitter.decorate(this);
+
+  this._target = target;
+  this._client = this._target.client;
+  this._request = this._request.bind(this);
+
+  Services.obs.notifyObservers(null, "performance-actors-connection-created", null);
+}
+
+PerformanceActorsConnection.prototype = {
+
+  /**
+   * Initializes a connection to the profiler and other miscellaneous actors.
+   * If already open, nothing happens.
+   *
+   * @return object
+   *         A promise that is resolved once the connection is established.
+   */
+  open: Task.async(function*() {
+    if (this._connected) {
+      return;
+    }
+
+    // Local debugging needs to make the target remote.
+    yield this._target.makeRemote();
+
+    // Sets `this._profiler`
+    yield this._connectProfilerActor();
+
+    // Sets or shims `this._timeline`
+    yield this._connectTimelineActor();
+
+    this._connected = true;
+
+    Services.obs.notifyObservers(null, "performance-actors-connection-opened", null);
+  }),
+
+  /**
+   * Destroys this connection.
+   */
+  destroy: function () {
+    this._disconnectActors();
+    this._connected = false;
+  },
+
+  /**
+   * Initializes a connection to the profiler actor.
+   */
+  _connectProfilerActor: Task.async(function*() {
+    // Chrome debugging targets have already obtained a reference
+    // to the profiler actor.
+    if (this._target.chrome) {
+      this._profiler = this._target.form.profilerActor;
+    }
+    // Or when we are debugging content processes, we already have the tab
+    // specific one. Use it immediately.
+    else if (this._target.form && this._target.form.profilerActor) {
+      this._profiler = this._target.form.profilerActor;
+    }
+    // Check if we already have a grip to the `listTabs` response object
+    // and, if we do, use it to get to the profiler actor.
+    else if (this._target.root && this._target.root.profilerActor) {
+      this._profiler = this._target.root.profilerActor;
+    }
+    // Otherwise, call `listTabs`.
+    else {
+      this._profiler = (yield listTabs(this._client)).profilerActor;
+    }
+  }),
+
+  /**
+   * Initializes a connection to a timeline actor.
+   */
+  _connectTimelineActor: function() {
+    // Only initialize the timeline front if the respective actor is available.
+    // Older Gecko versions don't have an existing implementation, in which case
+    // all the methods we need can be easily mocked.
+    //
+    // If the timeline actor exists, all underlying actors (memory, framerate) exist,
+    // with the expected methods and behaviour. If using the Performance tool,
+    // and timeline actor does not exist (FxOS devices < Gecko 35),
+    // then just use the mocked actor and do not display timeline data.
+    //
+    // TODO use framework level feature detection from bug 1069673
+    if (this._target.form && this._target.form.timelineActor) {
+      this._timeline = new TimelineFront(this._target.client, this._target.form);
+    } else {
+      this._timeline = {
+        start: () => {},
+        stop: () => {},
+        isRecording: () => false,
+        on: () => {},
+        off: () => {},
+        destroy: () => {}
+      };
+    }
+  },
+
+  /**
+   * Closes the connections to non-profiler actors.
+   */
+  _disconnectActors: function () {
+    this._timeline.destroy();
+  },
+
+  /**
+   * Sends the request over the remote debugging protocol to the
+   * specified actor.
+   *
+   * @param string actor
+   *        The designated actor. Currently supported: "profiler", "timeline".
+   * @param string method
+   *        Method to call on the backend.
+   * @param any args [optional]
+   *        Additional data or arguments to send with the request.
+   * @return object
+   *         A promise resolved with the response once the request finishes.
+   */
+  _request: function(actor, method, ...args) {
+    // Handle requests to the profiler actor.
+    if (actor == "profiler") {
+      let deferred = promise.defer();
+      let data = args[0] || {};
+      data.to = this._profiler;
+      data.type = method;
+      this._client.request(data, deferred.resolve);
+      return deferred.promise;
+    }
+
+    // Handle requests to the timeline actor.
+    if (actor == "timeline") {
+      return this._timeline[method].apply(this._timeline, args);
+    }
+  }
+};
+
+/**
+ * A thin wrapper around a shared PerformanceActorsConnection for the parent target.
+ * Handles manually starting and stopping a recording.
+ *
+ * @param PerformanceActorsConnection connection
+ *        The shared instance for the parent target.
+ */
+function PerformanceFront(connection) {
+  EventEmitter.decorate(this);
+
+  this._request = connection._request;
+
+  // Pipe events from TimelineActor to the PerformanceFront
+  connection._timeline.on("markers", markers => this.emit("markers", markers));
+  connection._timeline.on("memory", (delta, measurement) => this.emit("memory", delta, measurement));
+  connection._timeline.on("ticks", (delta, timestamps) => this.emit("ticks", delta, timestamps));
+}
+
+PerformanceFront.prototype = {
+  /**
+   * Manually begins a recording session.
+   *
+   * @return object
+   *         A promise that is resolved once recording has started.
+   */
+  startRecording: Task.async(function*() {
+    let { isActive, currentTime } = yield this._request("profiler", "isActive");
+
+    // Start the profiler only if it wasn't already active. The built-in
+    // nsIPerformance module will be kept recording, because it's the same instance
+    // for all targets and interacts with the whole platform, so we don't want
+    // to affect other clients by stopping (or restarting) it.
+    if (!isActive) {
+      // Extend the options so that protocol.js doesn't modify
+      // the source object.
+      let options = extend({}, this._customPerformanceOptions);
+      yield this._request("profiler", "startProfiler", options);
+      this._profilingStartTime = 0;
+      this.emit("profiler-activated");
+    } else {
+      this._profilingStartTime = currentTime;
+      this.emit("profiler-already-active");
+    }
+
+    // The timeline actor is target-dependent, so just make sure
+    // it's recording.
+    let withMemory = showTimelineMemory();
+    yield this._request("timeline", "start", { withTicks: true, withMemory: withMemory });
+  }),
+
+  /**
+   * Manually ends the current recording session.
+   *
+   * @return object
+   *         A promise that is resolved once recording has stopped,
+   *         with the profiler and timeline data.
+   */
+  stopRecording: Task.async(function*() {
+    // We'll need to filter out all samples that fall out of current profile's
+    // range. This is necessary because the profiler is continuously running.
+    let profilerData = yield this._request("profiler", "getProfile");
+    filterSamples(profilerData, this._profilingStartTime);
+    offsetSampleTimes(profilerData, this._profilingStartTime);
+
+    yield this._request("timeline", "stop");
+
+    // Join all the acquired data and return it for outside consumers.
+    return {
+      recordingDuration: profilerData.currentTime - this._profilingStartTime,
+      profilerData: profilerData
+    };
+  }),
+
+  /**
+   * Overrides the options sent to the built-in profiler module when activating,
+   * such as the maximum entries count, the sampling interval etc.
+   *
+   * Used in tests and for older backend implementations.
+   */
+  _customPerformanceOptions: {
+    entries: 1000000,
+    interval: 1,
+    features: ["js"]
+  }
+};
+
+/**
+ * Filters all the samples in the provided profiler data to be more recent
+ * than the specified start time.
+ *
+ * @param object profilerData
+ *        The profiler data received from the backend.
+ * @param number profilingStartTime
+ *        The earliest acceptable sample time (in milliseconds).
+ */
+function filterSamples(profilerData, profilingStartTime) {
+  let firstThread = profilerData.profile.threads[0];
+
+  firstThread.samples = firstThread.samples.filter(e => {
+    return e.time >= profilingStartTime;
+  });
+}
+
+/**
+ * Offsets all the samples in the provided profiler data by the specified time.
+ *
+ * @param object profilerData
+ *        The profiler data received from the backend.
+ * @param number timeOffset
+ *        The amount of time to offset by (in milliseconds).
+ */
+function offsetSampleTimes(profilerData, timeOffset) {
+  let firstThreadSamples = profilerData.profile.threads[0].samples;
+
+  for (let sample of firstThreadSamples) {
+    sample.time -= timeOffset;
+  }
+}
+
+/**
+ * A collection of small wrappers promisifying functions invoking callbacks.
+ */
+function listTabs(client) {
+  let deferred = promise.defer();
+  client.listTabs(deferred.resolve);
+  return deferred.promise;
+}
+
+exports.getPerformanceActorsConnection = target => SharedPerformanceActors.forTarget(target);
+exports.PerformanceFront = PerformanceFront;
--- a/browser/devtools/performance/moz.build
+++ b/browser/devtools/performance/moz.build
@@ -1,8 +1,11 @@
 # vim: set filetype=python:
 # 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/.
 
 EXTRA_JS_MODULES.devtools.performance += [
+    'modules/front.js',
     'panel.js'
 ]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/browser/devtools/performance/panel.js
+++ b/browser/devtools/performance/panel.js
@@ -1,16 +1,17 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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, Ci, Cu, Cr} = require("chrome");
+const { PerformanceFront, getPerformanceActorsConnection } = require("devtools/performance/front");
 
 Cu.import("resource://gre/modules/Task.jsm");
 
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
 function PerformancePanel(iframeWindow, toolbox) {
@@ -22,26 +23,27 @@ function PerformancePanel(iframeWindow, 
 
 exports.PerformancePanel = PerformancePanel;
 
 PerformancePanel.prototype = {
   /**
    * Open is effectively an asynchronous constructor.
    *
    * @return object
-   *         A promise that is resolved when the Profiler completes opening.
+   *         A promise that is resolved when the Performance tool
+   *         completes opening.
    */
   open: Task.async(function*() {
     this.panelWin.gToolbox = this._toolbox;
     this.panelWin.gTarget = this.target;
 
-    // Mock Front for now
-    let gFront = {};
-    EventEmitter.decorate(gFront);
-    this.panelWin.gFront = gFront;
+    this._connection = getPerformanceActorsConnection(this.target);
+    yield this._connection.open();
+
+    this.panelWin.gFront = new PerformanceFront(this._connection);
 
     yield this.panelWin.startupPerformance();
 
     this.isReady = true;
     this.emit("ready");
     return this;
   }),
 
@@ -50,13 +52,16 @@ PerformancePanel.prototype = {
   get target() this._toolbox.target,
 
   destroy: Task.async(function*() {
     // Make sure this panel is not already destroyed.
     if (this._destroyed) {
       return;
     }
 
+    // Destroy the connection to ensure packet handlers are removed from client.
+    this._connection.destroy();
+
     yield this.panelWin.shutdownPerformance();
     this.emit("destroyed");
     this._destroyed = true;
   })
 };
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser.ini
@@ -0,0 +1,30 @@
+[DEFAULT]
+skip-if = e10s # Handle in Bug 1077464 for profiler
+subsuite = devtools
+support-files =
+  doc_simple-test.html
+  head.js
+
+# Commented out tests are profiler tests
+# that need to be moved over to performance tool
+
+[browser_perf-aaa-run-first-leaktest.js]
+[browser_perf-front-basic-timeline-01.js]
+[browser_perf-front-basic-profiler-01.js]
+# bug 1077464
+#[browser_perf-front-profiler-01.js]
+[browser_perf-front-profiler-02.js]
+[browser_perf-front-profiler-03.js]
+[browser_perf-front-profiler-04.js]
+# bug 1077464
+#[browser_perf-front-profiler-05.js]
+# bug 1077464
+#[browser_perf-front-profiler-06.js]
+# needs shared connection with profiler's shared connection
+#[browser_perf-shared-connection-01.js]
+[browser_perf-shared-connection-02.js]
+[browser_perf-shared-connection-03.js]
+# bug 1077464
+#[browser_perf-shared-connection-04.js]
+[browser_perf-data-samples.js]
+[browser_perf-data-massaging-01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-aaa-run-first-leaktest.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the performance tool leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+function spawnTest () {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+
+  ok(target, "Should have a target available.");
+  ok(toolbox, "Should have a toolbox available.");
+  ok(panel, "Should have a panel available.");
+
+  ok(panel.panelWin.gToolbox, "Should have a toolbox reference on the panel window.");
+  ok(panel.panelWin.gTarget, "Should have a target reference on the panel window.");
+  ok(panel.panelWin.gFront, "Should have a front reference on the panel window.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-data-massaging-01.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the retrieved profiler data samples are correctly filtered and
+ * normalized before passed to consumers.
+ */
+
+const WAIT_TIME = 1000; // ms
+
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let front = panel.panelWin.gFront;
+
+  // Perform the first recording...
+
+  yield front.startRecording();
+  let profilingStartTime = front._profilingStartTime;
+  info("Started profiling at: " + profilingStartTime);
+
+  busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
+
+  let firstRecordingData = yield front.stopRecording();
+  let firstRecordingFinishTime = firstRecordingData.profilerData.currentTime;
+
+  is(profilingStartTime, 0,
+    "The profiling start time should be 0 for the first recording.");
+  ok(firstRecordingData.recordingDuration >= WAIT_TIME,
+    "The first recording duration is correct.");
+  ok(firstRecordingFinishTime >= WAIT_TIME,
+    "The first recording finish time is correct.");
+
+  // Perform the second recording...
+
+  yield front.startRecording();
+  profilingStartTime = front._profilingStartTime;
+  info("Started profiling at: " + profilingStartTime);
+
+  busyWait(WAIT_TIME); // allow the profiler module to sample more cpu activity
+
+  let secondRecordingData = yield front.stopRecording();
+  let secondRecordingFinishTime = secondRecordingData.profilerData.currentTime;
+  let secondRecordingProfile = secondRecordingData.profilerData.profile;
+  let secondRecordingSamples = secondRecordingProfile.threads[0].samples;
+
+  isnot(profilingStartTime, 0,
+    "The profiling start time should not be 0 on the second recording.");
+  ok(secondRecordingData.recordingDuration >= WAIT_TIME,
+    "The second recording duration is correct.");
+  ok(secondRecordingFinishTime - firstRecordingFinishTime >= WAIT_TIME,
+    "The second recording finish time is correct.");
+
+  ok(secondRecordingSamples[0].time < profilingStartTime,
+    "The second recorded sample times were normalized.");
+  ok(secondRecordingSamples[0].time > 0,
+    "The second recorded sample times were normalized correctly.");
+  ok(!secondRecordingSamples.find(e => e.time + profilingStartTime <= firstRecordingFinishTime),
+    "There should be no samples from the first recording in the second one, " +
+    "even though the total number of frames did not overflow.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-data-samples.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the retrieved profiler data samples always have a (root) node.
+ * If this ever changes, the |ThreadNode.prototype.insert| function in
+ * browser/devtools/profiler/utils/tree-model.js will have to be changed.
+ */
+
+const WAIT_TIME = 1000; // ms
+
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let front = panel.panelWin.gFront;
+
+  yield front.startRecording();
+  busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
+
+  let recordingData = yield front.stopRecording();
+  let profile = recordingData.profilerData.profile;
+
+  for (let thread of profile.threads) {
+    info("Checking thread: " + thread.name);
+
+    for (let sample of thread.samples) {
+      if (sample.frames[0].location != "(root)") {
+        ok(false, "The sample " + sample.toSource() + " doesn't have a root node.");
+      }
+    }
+  }
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-front-basic-profiler-01.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test basic functionality of PerformanceFront
+ */
+
+let WAIT = 1000;
+
+function spawnTest () {
+  let { target, front } = yield initBackend(SIMPLE_URL);
+
+  yield front.startRecording();
+
+  yield busyWait(WAIT);
+
+  let { recordingDuration, profilerData } = yield front.stopRecording();
+
+  ok(recordingDuration > 500, "recordingDuration exists");
+  ok(profilerData, "profilerData exists");
+
+  yield removeTab(target.tab);
+  finish();
+
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-front-basic-timeline-01.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test basic functionality of PerformanceFront, retrieving timeline data.
+ */
+
+function spawnTest () {
+  Services.prefs.setBoolPref("devtools.performance.ui.show-timeline-memory", true);
+
+  let { target, front } = yield initBackend(SIMPLE_URL);
+
+  let lastMemoryDelta = 0;
+  let lastTickDelta = 0;
+
+  let counters = {
+    markers: [],
+    memory: [],
+    ticks: []
+  };
+
+  let deferreds = {
+    markers: Promise.defer(),
+    memory: Promise.defer(),
+    ticks: Promise.defer()
+  }
+
+  front.on("markers", handler);
+  front.on("memory", handler);
+  front.on("ticks", handler);
+
+  yield front.startRecording();
+
+  yield Promise.all(Object.keys(deferreds).map(type => deferreds[type].promise));
+
+  yield front.stopRecording();
+
+  is(counters.markers.length, 1, "one marker event fired.");
+  is(counters.memory.length, 3, "three memory events fired.");
+  is(counters.ticks.length, 3, "three ticks events fired.");
+
+  yield removeTab(target.tab);
+  finish();
+
+  function handler (name, ...args) {
+    if (name === "memory") {
+      let [delta, measurement] = args;
+      is(typeof delta, "number", "received `delta` in memory event");
+      ok(delta > lastMemoryDelta, "received `delta` in memory event");
+      ok(measurement.total, "received `total` in memory event");
+      ok(measurement.domSize, "received `domSize` in memory event");
+      ok(measurement.jsObjectsSize, "received `jsObjectsSize` in memory event");
+
+      counters.memory.push({ delta: delta, measurement: measurement });
+      lastMemoryDelta = delta;
+    } else if (name === "ticks") {
+      let [delta, timestamps] = args;
+      ok(delta > lastTickDelta, "received `delta` in ticks event");
+
+      // First tick doesn't contain any timestamps
+      if (counters.ticks.length) {
+        ok(timestamps.length, "received `timestamps` in ticks event");
+      }
+
+      counters.ticks.push({ delta: delta, timestamps: timestamps});
+      lastTickDelta = delta;
+    } else if (name === "markers") {
+      let [markers] = args;
+      ok(markers[0].start, "received atleast one marker with `start`");
+      ok(markers[0].end, "received atleast one marker with `end`");
+      ok(markers[0].name, "received atleast one marker with `name`");
+      counters.markers.push(markers);
+      front.off(name, handler);
+      deferreds[name].resolve();
+    } else {
+      throw new Error("unknown event");
+    }
+
+    if (name !== "markers" && counters[name].length === 3) {
+      front.off(name, handler);
+      deferreds[name].resolve();
+    }
+  };
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-front-profiler-02.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the profiler connection front does not activate the built-in
+ * profiler module if not necessary, and doesn't deactivate it when
+ * a recording is stopped.
+ */
+
+let test = Task.async(function*() {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+  let front = panel.panelWin.gFront;
+
+  ok(!nsIProfilerModule.IsActive(),
+    "The built-in profiler module should not have been automatically started.");
+
+  let activated = front.once("profiler-activated");
+  yield front.startRecording();
+  yield activated;
+  yield front.stopRecording();
+  ok(nsIProfilerModule.IsActive(),
+    "The built-in profiler module should still be active (1).");
+
+  let alreadyActive = front.once("profiler-already-active");
+  yield front.startRecording();
+  yield alreadyActive;
+  yield front.stopRecording();
+  ok(nsIProfilerModule.IsActive(),
+    "The built-in profiler module should still be active (2).");
+
+  yield teardown(panel);
+
+  ok(!nsIProfilerModule.IsActive(),
+    "The built-in profiler module should have been automatically stoped.");
+
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-front-profiler-03.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the built-in profiler module doesn't deactivate when the toolbox
+ * is destroyed if there are other consumers using it.
+ */
+
+let test = Task.async(function*() {
+  let { panel: firstPanel } = yield initPerformance(SIMPLE_URL);
+  let firstFront = firstPanel.panelWin.gFront;
+
+  let activated = firstFront.once("profiler-activated");
+  yield firstFront.startRecording();
+  yield activated;
+
+  let { panel: secondPanel } = yield initPerformance(SIMPLE_URL);
+  let secondFront = secondPanel.panelWin.gFront;
+
+  let alreadyActive = secondFront.once("profiler-already-active");
+  yield secondFront.startRecording();
+  yield alreadyActive;
+
+  yield teardown(firstPanel);
+  ok(nsIProfilerModule.IsActive(),
+    "The built-in profiler module should still be active.");
+
+  yield teardown(secondPanel);
+  ok(!nsIProfilerModule.IsActive(),
+    "The built-in profiler module should have been automatically stoped.");
+
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-front-profiler-04.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the built-in profiler module is not reactivated if no other
+ * consumer was using it over the remote debugger protocol, and ensures
+ * that the actor will work properly even in such cases (e.g. the Gecko Profiler
+ * addon was installed and automatically activated the profiler module).
+ */
+
+let test = Task.async(function*() {
+  // Ensure the profiler is already running when the test starts.
+  let ENTRIES = 1000000;
+  let INTERVAL = 1;
+  let FEATURES = ["js"];
+  nsIProfilerModule.StartProfiler(ENTRIES, INTERVAL, FEATURES, FEATURES.length);
+
+  let { panel: firstPanel } = yield initPerformance(SIMPLE_URL);
+  let firstFront = firstPanel.panelWin.gFront;
+
+  let alredyActive = firstFront.once("profiler-already-active");
+  yield firstFront.startRecording();
+  yield alredyActive;
+  ok(firstFront._profilingStartTime > 0, "The profiler was not restarted.");
+
+  let { panel: secondPanel } = yield initPerformance(SIMPLE_URL);
+  let secondFront = secondPanel.panelWin.gFront;
+
+  let alreadyActive = secondFront.once("profiler-already-active");
+  yield secondFront.startRecording();
+  yield alreadyActive;
+  ok(secondFront._profilingStartTime > 0, "The profiler was not restarted.");
+
+  yield teardown(firstPanel);
+  ok(nsIProfilerModule.IsActive(),
+    "The built-in profiler module should still be active.");
+
+  yield teardown(secondPanel);
+  ok(!nsIProfilerModule.IsActive(),
+    "The built-in profiler module should have been automatically stoped.");
+
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-shared-connection-02.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shared PerformanceActorsConnection is only opened once.
+ */
+
+let gProfilerConnectionsOpened = 0;
+Services.obs.addObserver(profilerConnectionObserver, "performance-actors-connection-opened", false);
+
+function spawnTest () {
+  let { target, panel } = yield initPerformance(SIMPLE_URL);
+
+  is(gProfilerConnectionsOpened, 1,
+    "Only one profiler connection was opened.");
+
+  let sharedConnection = getPerformanceActorsConnection(target);
+
+  ok(sharedConnection,
+    "A shared profiler connection for the current toolbox was retrieved.");
+  is(sharedConnection._request, panel.panelWin.gFront._request,
+    "The same shared profiler connection is used by the panel's front.");
+
+  yield sharedConnection.open();
+  is(gProfilerConnectionsOpened, 1,
+    "No additional profiler connections were opened.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function profilerConnectionObserver(subject, topic, data) {
+  is(topic, "performance-actors-connection-opened", "The correct topic was observed.");
+  gProfilerConnectionsOpened++;
+}
+
+registerCleanupFunction(() => {
+  Services.obs.removeObserver(profilerConnectionObserver, "performance-actors-connection-opened");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-shared-connection-03.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shared PerformanceActorsConnection can properly send requests.
+ */
+
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let front = panel.panelWin.gFront;
+
+  ok(!nsIProfilerModule.IsActive(),
+    "The built-in profiler module should not have been automatically started.");
+
+  let result = yield front._request("profiler", "startProfiler");
+  is(result.started, true,
+    "The request finished successfully and the profiler should've been started.");
+  ok(nsIProfilerModule.IsActive(),
+    "The built-in profiler module should now be active.");
+
+  result = yield front._request("profiler", "stopProfiler");
+  is(result.started, false,
+    "The request finished successfully and the profiler should've been stopped.");
+  ok(!nsIProfilerModule.IsActive(),
+    "The built-in profiler module should now be inactive.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/doc_simple-test.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Profiler test page</title>
+  </head>
+
+  <body>
+    <script type="text/javascript">
+      function test() {
+        var a = "Hello world!";
+      }
+
+      // Prevent this script from being garbage collected.
+      window.setInterval(test, 1);
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/head.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+// Enable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+let { getPerformanceActorsConnection, PerformanceFront } = devtools.require("devtools/performance/front");
+let nsIProfilerModule = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
+let TargetFactory = devtools.TargetFactory;
+let mm = null;
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js"
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/performance/test/";
+const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+let gToolEnabled = Services.prefs.getBoolPref("devtools.performance_dev.enabled");
+let gShowTimelineMemory = Services.prefs.getBoolPref("devtools.performance.ui.show-timeline-memory");
+
+gDevTools.testing = true;
+
+/**
+ * Call manually in tests that use frame script utils after initializing
+ * the tool. Must be called after initializing so we can detect
+ * whether or not `content` is a CPOW or not. Call after init but before navigating
+ * to different pages.
+ */
+function loadFrameScripts () {
+  mm = gBrowser.selectedBrowser.messageManager;
+  mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
+
+registerCleanupFunction(() => {
+  gDevTools.testing = false;
+  info("finish() was called, cleaning up...");
+
+  Services.prefs.setBoolPref("devtools.performance.ui.show-timeline-memory", gShowTimelineMemory);
+  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+  Services.prefs.setBoolPref("devtools.performance_dev.enabled", gToolEnabled);
+  // Make sure the profiler module is stopped when the test finishes.
+  nsIProfilerModule.StopProfiler();
+
+  Cu.forceGC();
+});
+
+function addTab(aUrl, aWindow) {
+  info("Adding tab: " + aUrl);
+
+  let deferred = Promise.defer();
+  let targetWindow = aWindow || window;
+  let targetBrowser = targetWindow.gBrowser;
+
+  targetWindow.focus();
+  let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+  let linkedBrowser = tab.linkedBrowser;
+
+  linkedBrowser.addEventListener("load", function onLoad() {
+    linkedBrowser.removeEventListener("load", onLoad, true);
+    info("Tab added and finished loading: " + aUrl);
+    deferred.resolve(tab);
+  }, true);
+
+  return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+  info("Removing tab.");
+
+  let deferred = Promise.defer();
+  let targetWindow = aWindow || window;
+  let targetBrowser = targetWindow.gBrowser;
+  let tabContainer = targetBrowser.tabContainer;
+
+  tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+    tabContainer.removeEventListener("TabClose", onClose, false);
+    info("Tab removed and finished closing.");
+    deferred.resolve();
+  }, false);
+
+  targetBrowser.removeTab(aTab);
+  return deferred.promise;
+}
+
+function handleError(aError) {
+  ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+  finish();
+}
+
+function once(aTarget, aEventName, aUseCapture = false) {
+  info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+  let deferred = Promise.defer();
+
+  for (let [add, remove] of [
+    ["on", "off"], // Use event emitter before DOM events for consistency
+    ["addEventListener", "removeEventListener"],
+    ["addListener", "removeListener"]
+  ]) {
+    if ((add in aTarget) && (remove in aTarget)) {
+      aTarget[add](aEventName, function onEvent(...aArgs) {
+        aTarget[remove](aEventName, onEvent, aUseCapture);
+        deferred.resolve(...aArgs);
+      }, aUseCapture);
+      break;
+    }
+  }
+
+  return deferred.promise;
+}
+
+function test () {
+  Task.spawn(spawnTest).then(finish, handleError);
+}
+
+function initBackend(aUrl) {
+  info("Initializing a performance front.");
+
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+  }
+
+  return Task.spawn(function*() {
+    let tab = yield addTab(aUrl);
+    let target = TargetFactory.forTab(tab);
+
+    yield target.makeRemote();
+
+    yield gDevTools.showToolbox(target, "performance");
+
+    let connection = getPerformanceActorsConnection(target);
+    yield connection.open();
+    let front = new PerformanceFront(connection);
+    return { target, front };
+  });
+}
+
+function initPerformance(aUrl) {
+  info("Initializing a performance pane.");
+
+  return Task.spawn(function*() {
+    let tab = yield addTab(aUrl);
+    let target = TargetFactory.forTab(tab);
+
+    yield target.makeRemote();
+
+    Services.prefs.setBoolPref("devtools.performance_dev.enabled", true);
+    let toolbox = yield gDevTools.showToolbox(target, "performance");
+    let panel = toolbox.getCurrentPanel();
+    return { target, panel, toolbox };
+  });
+}
+
+function* teardown(panel) {
+  info("Destroying the performance tool.");
+
+  let tab = panel.target.tab;
+  yield panel._toolbox.destroy();
+  yield removeTab(tab);
+}
+
+function idleWait(time) {
+  return DevToolsUtils.waitForTime(time);
+}
+
+function consoleMethod (...args) {
+  if (!mm) {
+    throw new Error("`loadFrameScripts()` must be called before using frame scripts.");
+  }
+  mm.sendAsyncMessage("devtools:test:console", args);
+}
+
+function* consoleProfile(connection, label) {
+  let notified = connection.once("profile");
+  consoleMethod("profile", label);
+  yield notified;
+}
+
+function* consoleProfileEnd(connection) {
+  let notified = connection.once("profileEnd");
+  consoleMethod("profileEnd");
+  yield notified;
+}
+
+function busyWait(time) {
+  let start = Date.now();
+  let stack;
+  while (Date.now() - start < time) { stack = Components.stack; }
+}
+
+function idleWait(time) {
+  return DevToolsUtils.waitForTime(time);
+}
+
--- a/browser/devtools/shared/frame-script-utils.js
+++ b/browser/devtools/shared/frame-script-utils.js
@@ -12,11 +12,16 @@ addMessageListener("devtools:test:naviga
   content.location = data.location;
 });
 
 addMessageListener("devtools:test:reload", function ({ data }) {
   data = data || {};
   content.location.reload(data.forceget);
 });
 
+addMessageListener("devtools:test:console", function ({ data }) {
+  let method = data.shift();
+  content.console[method].apply(content.console, data);
+});
+
 addEventListener("load", function() {
   sendAsyncMessage("devtools:test:load");
 }, true);
--- a/toolkit/devtools/server/actors/timeline.js
+++ b/toolkit/devtools/server/actors/timeline.js
@@ -207,17 +207,17 @@ let TimelineActor = exports.TimelineActo
     this._startTime = this.docShells[0].now();
 
     for (let docShell of this.docShells) {
       docShell.recordProfileTimelineMarkers = true;
     }
 
     if (withMemory) {
       this._memoryActor = new MemoryActor(this.conn, this.tabActor);
-      events.emit(this, "memory", Date.now(), this._memoryActor.measure());
+      events.emit(this, "memory", this._startTime, this._memoryActor.measure());
     }
     if (withTicks) {
       this._framerateActor = new FramerateActor(this.conn, this.tabActor);
       this._framerateActor.startRecording();
     }
 
     this._pullTimelineData();
     return this._startTime;