Bug 1050773 - Timeline actor pulls profiletimeline markers from all docShells; r=paul
authorPatrick Brosset <pbrosset@mozilla.com>
Thu, 18 Sep 2014 11:12:33 +0200
changeset 206042 a48989ed6aa6a9bdf170d728e8a24c2135c6ca24
parent 206041 34a00845e2e02234dd101d3728518c88327295e8
child 206043 849a1b3074c3f8849f2578c9872dfe1ea14d81f7
push id49338
push userkwierso@gmail.com
push dateThu, 18 Sep 2014 23:10:09 +0000
treeherdermozilla-inbound@245051c6a7ed [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaul
bugs1050773
milestone35.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 1050773 - Timeline actor pulls profiletimeline markers from all docShells; r=paul
toolkit/devtools/server/actors/timeline.js
toolkit/devtools/server/tests/browser/browser.ini
toolkit/devtools/server/tests/browser/browser_navigateEvents.js
toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js
toolkit/devtools/server/tests/browser/browser_storage_listings.js
toolkit/devtools/server/tests/browser/browser_storage_updates.js
toolkit/devtools/server/tests/browser/browser_timeline.js
toolkit/devtools/server/tests/browser/browser_timeline_iframes.js
toolkit/devtools/server/tests/browser/head.js
toolkit/devtools/server/tests/browser/timeline-iframe-child.html
toolkit/devtools/server/tests/browser/timeline-iframe-parent.html
--- a/toolkit/devtools/server/actors/timeline.js
+++ b/toolkit/devtools/server/actors/timeline.js
@@ -2,18 +2,17 @@
  * 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";
 
 /**
  * Many Gecko operations (painting, reflows, restyle, ...) can be tracked
  * in real time. A marker is a representation of one operation. A marker
- * has a name, and start and end timestamps. Markers are stored within
- * a docshell.
+ * has a name, and start and end timestamps. Markers are stored in docShells.
  *
  * This actor exposes this tracking mechanism to the devtools protocol.
  *
  * To start/stop recording markers:
  *   TimelineFront.start()
  *   TimelineFront.stop()
  *   TimelineFront.isRecording()
  *
@@ -23,101 +22,163 @@
  */
 
 const {Ci, Cu} = require("chrome");
 const protocol = require("devtools/server/protocol");
 const {method, Arg, RetVal} = protocol;
 const events = require("sdk/event/core");
 const {setTimeout, clearTimeout} = require("sdk/timers");
 
+// How often do we pull markers from the docShells, and therefore, how often do
+// we send events to the front (knowing that when there are no markers in the
+// docShell, no event is sent).
 const DEFAULT_TIMELINE_DATA_PULL_TIMEOUT = 200; // ms
 
 /**
- * The timeline actor pops and forwards timeline markers registered in
- * a docshell.
+ * The timeline actor pops and forwards timeline markers registered in docshells.
  */
 let TimelineActor = exports.TimelineActor = protocol.ActorClass({
   typeName: "timeline",
 
   events: {
     /**
-     * "markers" events are emitted at regular intervals when profile markers
-     * are found. A marker has the following properties:
-     * - start {Number}
-     * - end {Number}
+     * "markers" events are emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms
+     * at most, when profile markers are found. A marker has the following
+     * properties:
+     * - start {Number} ms
+     * - end {Number} ms
      * - name {String}
      */
     "markers" : {
       type: "markers",
       markers: Arg(0, "array:json")
     }
   },
 
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
-    this.docshell = tabActor.docShell;
+    this.tabActor = tabActor;
+
+    this._isRecording = false;
+
+    // Make sure to get markers from new windows as they become available
+    this._onWindowReady = this._onWindowReady.bind(this);
+    events.on(this.tabActor, "window-ready", this._onWindowReady);
   },
 
   /**
    * The timeline actor is the first (and last) in its hierarchy to use protocol.js
    * so it doesn't have a parent protocol actor that takes care of its lifetime.
    * So it needs a disconnect method to cleanup.
    */
   disconnect: function() {
     this.destroy();
   },
 
   destroy: function() {
     this.stop();
-    this.docshell = null;
+
+    events.off(this.tabActor, "window-ready", this._onWindowReady);
+    this.tabActor = null;
+
     protocol.Actor.prototype.destroy.call(this);
   },
 
   /**
+   * Convert a window to a docShell.
+   * @param {nsIDOMWindow}
+   * @return {nsIDocShell}
+   */
+  toDocShell: win => win.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIWebNavigation)
+                        .QueryInterface(Ci.nsIDocShell),
+
+  /**
+   * Get the list of docShells in the currently attached tabActor.
+   * @return {Array}
+   */
+  get docShells() {
+    return this.tabActor.windows.map(this.toDocShell);
+  },
+
+  /**
    * At regular intervals, pop the markers from the docshell, and forward
    * markers if any.
    */
   _pullTimelineData: function() {
-    let markers = this.docshell.popProfileTimelineMarkers();
+    if (!this._isRecording) {
+      return;
+    }
+
+    let markers = [];
+    for (let docShell of this.docShells) {
+      markers = [...markers, ...docShell.popProfileTimelineMarkers()];
+    }
     if (markers.length > 0) {
       events.emit(this, "markers", markers);
     }
+
     this._dataPullTimeout = setTimeout(() => {
       this._pullTimelineData();
     }, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
   },
 
   /**
-   * Are we recording profile markers for the current docshell (window)?
+   * Are we recording profile markers currently?
    */
   isRecording: method(function() {
-    return this.docshell.recordProfileTimelineMarkers;
+    return this._isRecording;
   }, {
     request: {},
     response: {
       value: RetVal("boolean")
     }
   }),
 
   /**
-   * Start/stop recording profile markers.
+   * Start recording profile markers.
    */
   start: method(function() {
-    if (!this.docshell.recordProfileTimelineMarkers) {
-      this.docshell.recordProfileTimelineMarkers = true;
-      this._pullTimelineData();
+    if (this._isRecording) {
+      return;
     }
+    this._isRecording = true;
+
+    for (let docShell of this.docShells) {
+      docShell.recordProfileTimelineMarkers = true;
+    }
+
+    this._pullTimelineData();
   }, {}),
 
+  /**
+   * Stop recording profile markers.
+   */
   stop: method(function() {
-    if (this.docshell.recordProfileTimelineMarkers) {
-      this.docshell.recordProfileTimelineMarkers = false;
-      clearTimeout(this._dataPullTimeout);
+    if (!this._isRecording) {
+      return;
+    }
+    this._isRecording = false;
+
+    for (let docShell of this.docShells) {
+      docShell.recordProfileTimelineMarkers = false;
     }
+
+    clearTimeout(this._dataPullTimeout);
   }, {}),
+
+  /**
+   * When a new window becomes available in the tabActor, start recording its
+   * markers if we were recording.
+   */
+  _onWindowReady: function({window}) {
+    if (this._isRecording) {
+      this.toDocShell(window).recordProfileTimelineMarkers = true;
+    }
+  }
 });
 
 exports.TimelineFront = protocol.FrontClass(TimelineActor, {
   initialize: function(client, {timelineActor}) {
     protocol.Front.prototype.initialize.call(this, client, {actor: timelineActor});
     this.manage(this);
   },
 
--- a/toolkit/devtools/server/tests/browser/browser.ini
+++ b/toolkit/devtools/server/tests/browser/browser.ini
@@ -1,19 +1,23 @@
 [DEFAULT]
 skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
 subsuite = devtools
 support-files =
   head.js
+  navigate-first.html
+  navigate-second.html
   storage-dynamic-windows.html
   storage-listings.html
   storage-unsecured-iframe.html
   storage-updates.html
   storage-secured-iframe.html
-  navigate-first.html
-  navigate-second.html
+  timeline-iframe-child.html
+  timeline-iframe-parent.html
 
+[browser_navigateEvents.js]
 [browser_storage_dynamic_windows.js]
 [browser_storage_listings.js]
 [browser_storage_updates.js]
-[browser_navigateEvents.js]
 [browser_timeline.js]
 skip-if = buildapp == 'mulet'
+[browser_timeline_iframes.js]
+skip-if = buildapp == 'mulet'
--- a/toolkit/devtools/server/tests/browser/browser_navigateEvents.js
+++ b/toolkit/devtools/server/tests/browser/browser_navigateEvents.js
@@ -1,22 +1,18 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
 
-let Cu = Components.utils;
-let Cc = Components.classes;
-let Ci = Components.interfaces;
+"use strict";
 
 const URL1 = MAIN_DOMAIN + "navigate-first.html";
 const URL2 = MAIN_DOMAIN + "navigate-second.html";
 
-let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
-let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
-
-let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
-let events = devtools.require("sdk/event/core");
-
+let events = require("sdk/event/core");
 let client;
 
 // State machine to check events order
 let i = 0;
 function assertEvent(event, data) {
   let x = 0;
   switch(i++) {
     case x++:
@@ -92,34 +88,28 @@ function onDOMContentLoaded() {
   assertEvent("DOMContentLoaded");
 }
 function onLoad() {
   assertEvent("load");
 }
 
 function getServerTabActor(callback) {
   // Ensure having a minimal server
-  if (!DebuggerServer.initialized) {
-    DebuggerServer.init(function () { return true; });
-    DebuggerServer.addBrowserActors();
-  }
+  initDebuggerServer();
 
   // Connect to this tab
   let transport = DebuggerServer.connectPipe();
   client = new DebuggerClient(transport);
-  client.connect(function onConnect() {
-    client.listTabs(function onListTabs(aResponse) {
-      // Fetch the BrowserTabActor for this tab
-      let actorID = aResponse.tabs[aResponse.selected].actor;
-      client.attachTab(actorID, function(aResponse, aTabClient) {
-        // !Hack! Retrieve a server side object, the BrowserTabActor instance
-        let conn = transport._serverConnection;
-        let tabActor = conn.getActor(actorID);
-        callback(tabActor);
-      });
+  connectDebuggerClient(client).then(form => {
+    let actorID = form.actor;
+    client.attachTab(actorID, function(aResponse, aTabClient) {
+      // !Hack! Retrieve a server side object, the BrowserTabActor instance
+      let conn = transport._serverConnection;
+      let tabActor = conn.getActor(actorID);
+      callback(tabActor);
     });
   });
 
   client.addListener("tabNavigated", function (aEvent, aPacket) {
     assertEvent("tabNavigated", aPacket);
   });
 }
 
--- a/toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js
+++ b/toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js
@@ -1,16 +1,13 @@
-const Cu = Components.utils;
-Cu.import("resource://gre/modules/Services.jsm");
-let tempScope = {};
-Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
-Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
-Cu.import("resource://gre/modules/Promise.jsm", tempScope);
-let {DebuggerServer, DebuggerClient, Promise} = tempScope;
-tempScope = null;
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
 
 const {StorageFront} = require("devtools/server/actors/storage");
 let gFront, gWindow;
 
 const beforeReload = {
   cookies: {
     "test1.example.org": ["c1", "cs2", "c3", "uc1"],
     "sectest1.example.org": ["uc1", "cs2"]
@@ -55,17 +52,17 @@ function finishTests(client) {
 
   let closeConnection = () => {
     // Forcing GC/CC to get rid of docshells and windows created by this test.
     forceCollections();
     client.close(() => {
       forceCollections();
       DebuggerServer.destroy();
       forceCollections();
-      gFront = gWindow = DebuggerClient = DebuggerServer = null;
+      gFront = gWindow = null;
       finish();
     });
   }
   gWindow.clearIterator = gWindow.clear(() => {
     clearIDB(gWindow, 0, closeConnection);
   });
   gWindow.clearIterator.next();
 }
@@ -301,33 +298,23 @@ function testRemoveIframe() {
       break;
     }
   }
   return reloaded.promise;
 }
 
 function test() {
   addTab(MAIN_DOMAIN + "storage-dynamic-windows.html").then(function(doc) {
-    try {
-      // Sometimes debugger server does not get destroyed correctly by previous
-      // tests.
-      DebuggerServer.destroy();
-    } catch (ex) { }
-    DebuggerServer.init(function () { return true; });
-    DebuggerServer.addBrowserActors();
+    initDebuggerServer();
 
     let createConnection = () => {
       let client = new DebuggerClient(DebuggerServer.connectPipe());
-      client.connect(function onConnect() {
-        client.listTabs(function onListTabs(aResponse) {
-          let form = aResponse.tabs[aResponse.selected];
-          gFront = StorageFront(client, form);
-
-          gFront.listStores().then(data => testStores(data, client));
-        });
+      connectDebuggerClient(client).then(form => {
+        gFront = StorageFront(client, form);
+        gFront.listStores().then(data => testStores(data, client));
       });
     };
 
     /**
      * This method iterates over iframes in a window and setups the indexed db
      * required for this test.
      */
     let setupIDBInFrames = (w, i, c) => {
--- a/toolkit/devtools/server/tests/browser/browser_storage_listings.js
+++ b/toolkit/devtools/server/tests/browser/browser_storage_listings.js
@@ -1,18 +1,15 @@
-const Cu = Components.utils;
-Cu.import("resource://gre/modules/Services.jsm");
-let tempScope = {};
-Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
-Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
-let {DebuggerServer, DebuggerClient} = tempScope;
-tempScope = null;
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
 
 const {StorageFront} = require("devtools/server/actors/storage");
-let {Task} = require("resource://gre/modules/Task.jsm");
 let gWindow = null;
 
 const storeMap = {
   cookies: {
     "test1.example.org": [
       {
         name: "c1",
         value: "foobar",
@@ -343,17 +340,17 @@ function finishTests(client) {
 
   let closeConnection = () => {
     // Forcing GC/CC to get rid of docshells and windows created by this test.
     forceCollections();
     client.close(() => {
       forceCollections();
       DebuggerServer.destroy();
       forceCollections();
-      gWindow = DebuggerClient = DebuggerServer = null;
+      gWindow = null;
       finish();
     });
   }
   gWindow.clearIterator = gWindow.clear(() => {
     clearIDB(gWindow, 0, closeConnection);
   });
   gWindow.clearIterator.next();
 }
@@ -635,34 +632,24 @@ let testIDBEntries = Task.async(function
   if (index == Object.keys(hosts).length - 1) {
     return;
   }
   yield testObjectStores(++index, hosts, indexedDBActor);
 });
 
 function test() {
   addTab(MAIN_DOMAIN + "storage-listings.html").then(function(doc) {
-    try {
-      // Sometimes debugger server does not get destroyed correctly by previous
-      // tests.
-      DebuggerServer.destroy();
-    } catch (ex) { }
-    DebuggerServer.init(function () { return true; });
-    DebuggerServer.addBrowserActors();
+    initDebuggerServer();
 
     let createConnection = () => {
       let client = new DebuggerClient(DebuggerServer.connectPipe());
-      client.connect(function onConnect() {
-        client.listTabs(function onListTabs(aResponse) {
-          let form = aResponse.tabs[aResponse.selected];
-          let front = StorageFront(client, form);
-
-          front.listStores().then(data => testStores(data))
-               .then(() => finishTests(client));
-        });
+      connectDebuggerClient(client).then(form => {
+        let front = StorageFront(client, form);
+        front.listStores().then(data => testStores(data))
+                          .then(() => finishTests(client));
       });
     };
 
     /**
      * This method iterates over iframes in a window and setups the indexed db
      * required for this test.
      */
     let setupIDBInFrames = (w, i, c) => {
--- a/toolkit/devtools/server/tests/browser/browser_storage_updates.js
+++ b/toolkit/devtools/server/tests/browser/browser_storage_updates.js
@@ -1,16 +1,13 @@
-const Cu = Components.utils;
-Cu.import("resource://gre/modules/Services.jsm");
-let tempScope = {};
-Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
-Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
-Cu.import("resource://gre/modules/Promise.jsm", tempScope);
-let {DebuggerServer, DebuggerClient, Promise} = tempScope;
-tempScope = null;
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
 
 const {StorageFront} = require("devtools/server/actors/storage");
 let gTests;
 let gExpected;
 let index = 0;
 
 const beforeReload = {
   cookies: ["test1.example.org", "sectest1.example.org"],
@@ -20,17 +17,17 @@ const beforeReload = {
 
 function finishTests(client) {
   // Forcing GC/CC to get rid of docshells and windows created by this test.
   forceCollections();
   client.close(() => {
     forceCollections();
     DebuggerServer.destroy();
     forceCollections();
-    DebuggerClient = DebuggerServer = gTests = null;
+    gTests = null;
     finish();
   });
 }
 
 function markOutMatched(toBeEmptied, data, deleted) {
   if (!Object.keys(toBeEmptied).length) {
     info("Object empty")
     return;
@@ -227,29 +224,20 @@ function* UpdateTests(front, win, client
   front.off("stores-cleared", onStoresCleared);
   front.off("stores-update", onStoresUpdate);
   finishTests(client);
 }
 
 
 function test() {
   addTab(MAIN_DOMAIN + "storage-updates.html").then(function(doc) {
-    try {
-      // Sometimes debugger server does not get destroyed correctly by previous
-      // tests.
-      DebuggerServer.destroy();
-    } catch (ex) { }
-    DebuggerServer.init(function () { return true; });
-    DebuggerServer.addBrowserActors();
+    initDebuggerServer();
 
     let client = new DebuggerClient(DebuggerServer.connectPipe());
-    client.connect(function onConnect() {
-      client.listTabs(function onListTabs(aResponse) {
-        let form = aResponse.tabs[aResponse.selected];
-        let front = StorageFront(client, form);
-        gTests = UpdateTests(front, doc.defaultView.wrappedJSObject,
-                             client);
-        // Make an initial call to initialize the actor
-        front.listStores().then(() => gTests.next());
-      });
+    connectDebuggerClient(client).then(form => {
+      let front = StorageFront(client, form);
+      gTests = UpdateTests(front, doc.defaultView.wrappedJSObject,
+                           client);
+      // Make an initial call to initialize the actor
+      front.listStores().then(() => gTests.next());
     });
   })
 }
--- a/toolkit/devtools/server/tests/browser/browser_timeline.js
+++ b/toolkit/devtools/server/tests/browser/browser_timeline.js
@@ -1,34 +1,26 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+
 "use strict";
 
+// Test that the timeline front's start/stop/isRecording methods work in a
+// simple use case, and that markers events are sent when operations occur.
+
+const {TimelineFront} = require("devtools/server/actors/timeline");
+
 let test = asyncTest(function*() {
-  const {TimelineFront} = require("devtools/server/actors/timeline");
-  const Cu = Components.utils;
-  let tempScope = {};
-  Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
-  Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
-  let {DebuggerServer, DebuggerClient} = tempScope;
-
   let doc = yield addTab("data:text/html;charset=utf-8,mop");
 
-  DebuggerServer.init(function () { return true; });
-  DebuggerServer.addBrowserActors();
+  initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
-  let onListTabs = promise.defer();
-  client.connect(() => {
-    client.listTabs(onListTabs.resolve);
-  });
 
-  let listTabs = yield onListTabs.promise;
-
-  let form = listTabs.tabs[listTabs.selected];
+  let form = yield connectDebuggerClient(client);
   let front = TimelineFront(client, form);
 
   let isActive = yield front.isRecording();
   ok(!isActive, "Not initially recording");
 
   doc.body.innerHeight; // flush any pending reflow
 
   yield front.start();
@@ -54,14 +46,11 @@ let test = asyncTest(function*() {
   ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
   ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
 
   yield front.stop();
 
   isActive = yield front.isRecording();
   ok(!isActive, "Not recording after stop()");
 
-  let onClose = promise.defer();
-  client.close(onClose.resolve);
-  yield onClose;
-
+  yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_timeline_iframes.js
@@ -0,0 +1,41 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the timeline front receives markers events for operations that occur in
+// iframes.
+
+const {TimelineFront} = require("devtools/server/actors/timeline");
+
+let test = asyncTest(function*() {
+  let doc = yield addTab(MAIN_DOMAIN + "timeline-iframe-parent.html");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let front = TimelineFront(client, form);
+
+  info("Start timeline marker recording");
+  yield front.start();
+
+  // Check that we get markers for a few iterations of the timer that runs in
+  // the child frame.
+  for (let i = 0; i < 3; i ++) {
+    yield wait(300); // That's the time the child frame waits before changing styles.
+    let markers = yield once(front, "markers");
+    ok(markers.length, "Markers were received for operations in the child frame");
+  }
+
+  info("Stop timeline marker recording");
+  yield front.stop();
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
+
+function wait(ms) {
+  let def = promise.defer();
+  setTimeout(def.resolve, ms);
+  return def.promise;
+}
--- a/toolkit/devtools/server/tests/browser/head.js
+++ b/toolkit/devtools/server/tests/browser/head.js
@@ -1,28 +1,33 @@
 /* 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/. */
-let tempScope = {};
-Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
-Cu.import("resource://gre/modules/devtools/Console.jsm", tempScope);
-const require = tempScope.devtools.require;
-const console = tempScope.console;
-tempScope = null;
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const {devtools: {require}} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const {DebuggerClient} = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
+const {DebuggerServer} = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+
 const PATH = "browser/toolkit/devtools/server/tests/browser/";
 const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
 const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
 const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
-const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 
-// All test are asynchronous
+// All tests are asynchronous.
 waitForExplicitFinish();
 
 /**
- * Define an async test based on a generator function
+ * Define an async test based on a generator function.
  */
 function asyncTest(generator) {
   return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
 }
 
 /**
  * Add a new test tab in the browser and load the given url.
  * @param {String} url The url to be loaded in the new tab
@@ -42,16 +47,53 @@ let addTab = Task.async(function* (url) 
   let isBlank = url == "about:blank";
   waitForFocus(def.resolve, content, isBlank);
 
   yield def.promise;
 
   return tab.linkedBrowser.contentWindow.document;
 });
 
+function initDebuggerServer() {
+  try {
+    // Sometimes debugger server does not get destroyed correctly by previous
+    // tests.
+    DebuggerServer.destroy();
+  } catch (ex) { }
+  DebuggerServer.init(() => true);
+  DebuggerServer.addBrowserActors();
+}
+
+/**
+ * Connect a debugger client.
+ * @param {DebuggerClient}
+ * @return {Promise} Resolves to the selected tabActor form when the client is
+ * connected.
+ */
+function connectDebuggerClient(client) {
+  let def = promise.defer();
+  client.connect(() => {
+    client.listTabs(tabs => {
+      def.resolve(tabs.tabs[tabs.selected]);
+    });
+  });
+  return def.promise;
+}
+
+/**
+ * Close a debugger client's connection.
+ * @param {DebuggerClient}
+ * @return {Promise} Resolves when the connection is closed.
+ */
+function closeDebuggerClient(client) {
+  let def = promise.defer();
+  client.close(def.resolve);
+  return def.promise;
+}
+
 /**
  * Wait for eventName on target.
  * @param {Object} target An observable object that either supports on/off or
  * addEventListener/removeEventListener
  * @param {String} eventName
  * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
  * @return A promise that resolves when the event has been handled
  */
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/timeline-iframe-child.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Timeline iframe test - child frame</title>
+</head>
+<body>
+  <h1>Child frame</h1>
+  <script>
+    var h1 = document.querySelector("h1");
+    setInterval(function() {
+      h1.style.backgroundColor = "rgb(" + ((Math.random()*255)|0) + "," +
+                                          ((Math.random()*255)|0) + "," +
+                                          ((Math.random()*255)|0) +")";
+      h1.style.width = ((Math.random()*500)|0) + "px";
+    }, 300);
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/timeline-iframe-parent.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Timeline iframe test - parent frame</title>
+</head>
+<body>
+  <h1>Parent frame</h1>
+  <iframe src="timeline-iframe-child.html"></iframe>
+</body>
+</html>