diff --git a/toolkit/devtools/server/actors/timeline.js b/toolkit/devtools/server/actors/timeline.js index b92e44da4a97..9d562d8e6704 100644 --- a/toolkit/devtools/server/actors/timeline.js +++ b/toolkit/devtools/server/actors/timeline.js @@ -7,8 +7,7 @@ /** * 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. * @@ -28,21 +27,24 @@ 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" : { @@ -53,7 +55,13 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({ 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); }, /** @@ -67,29 +75,57 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({ 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: { @@ -98,21 +134,46 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({ }), /** - * 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, { diff --git a/toolkit/devtools/server/tests/browser/browser.ini b/toolkit/devtools/server/tests/browser/browser.ini index f981979665c9..6e20a3b9bd39 100644 --- a/toolkit/devtools/server/tests/browser/browser.ini +++ b/toolkit/devtools/server/tests/browser/browser.ini @@ -3,17 +3,21 @@ 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' diff --git a/toolkit/devtools/server/tests/browser/browser_navigateEvents.js b/toolkit/devtools/server/tests/browser/browser_navigateEvents.js index 8dbf88bf1461..68487fd27dfc 100644 --- a/toolkit/devtools/server/tests/browser/browser_navigateEvents.js +++ b/toolkit/devtools/server/tests/browser/browser_navigateEvents.js @@ -1,17 +1,13 @@ +/* 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 @@ -97,24 +93,18 @@ function onLoad() { 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); }); }); diff --git a/toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js b/toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js index 795c4eb90f36..9c1f0a482bef 100644 --- a/toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js +++ b/toolkit/devtools/server/tests/browser/browser_storage_dynamic_windows.js @@ -1,11 +1,8 @@ -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; @@ -60,7 +57,7 @@ function finishTests(client) { forceCollections(); DebuggerServer.destroy(); forceCollections(); - gFront = gWindow = DebuggerClient = DebuggerServer = null; + gFront = gWindow = null; finish(); }); } @@ -306,23 +303,13 @@ function testRemoveIframe() { 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)); }); }; diff --git a/toolkit/devtools/server/tests/browser/browser_storage_listings.js b/toolkit/devtools/server/tests/browser/browser_storage_listings.js index f8da7c0de4a0..11c9b558d879 100644 --- a/toolkit/devtools/server/tests/browser/browser_storage_listings.js +++ b/toolkit/devtools/server/tests/browser/browser_storage_listings.js @@ -1,13 +1,10 @@ -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 = { @@ -348,7 +345,7 @@ function finishTests(client) { forceCollections(); DebuggerServer.destroy(); forceCollections(); - gWindow = DebuggerClient = DebuggerServer = null; + gWindow = null; finish(); }); } @@ -640,24 +637,14 @@ let testIDBEntries = Task.async(function*(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)); }); }; diff --git a/toolkit/devtools/server/tests/browser/browser_storage_updates.js b/toolkit/devtools/server/tests/browser/browser_storage_updates.js index 6ff18ce9270b..0af662150499 100644 --- a/toolkit/devtools/server/tests/browser/browser_storage_updates.js +++ b/toolkit/devtools/server/tests/browser/browser_storage_updates.js @@ -1,11 +1,8 @@ -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; @@ -25,7 +22,7 @@ function finishTests(client) { forceCollections(); DebuggerServer.destroy(); forceCollections(); - DebuggerClient = DebuggerServer = gTests = null; + gTests = null; finish(); }); } @@ -232,24 +229,15 @@ function* UpdateTests(front, win, 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()); }); }) } diff --git a/toolkit/devtools/server/tests/browser/browser_timeline.js b/toolkit/devtools/server/tests/browser/browser_timeline.js index bc805756abbf..35833b95dcc8 100644 --- a/toolkit/devtools/server/tests/browser/browser_timeline.js +++ b/toolkit/devtools/server/tests/browser/browser_timeline.js @@ -1,29 +1,21 @@ /* 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"; -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; +// 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*() { 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(); @@ -59,9 +51,6 @@ let test = asyncTest(function*() { 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(); }); diff --git a/toolkit/devtools/server/tests/browser/browser_timeline_iframes.js b/toolkit/devtools/server/tests/browser/browser_timeline_iframes.js new file mode 100644 index 000000000000..441500370bb0 --- /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; +} diff --git a/toolkit/devtools/server/tests/browser/head.js b/toolkit/devtools/server/tests/browser/head.js index d4008f6dc903..02e419ea9e63 100644 --- a/toolkit/devtools/server/tests/browser/head.js +++ b/toolkit/devtools/server/tests/browser/head.js @@ -1,23 +1,28 @@ /* 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); @@ -47,6 +52,43 @@ let addTab = Task.async(function* (url) { 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 diff --git a/toolkit/devtools/server/tests/browser/timeline-iframe-child.html b/toolkit/devtools/server/tests/browser/timeline-iframe-child.html new file mode 100644 index 000000000000..5385c64859eb --- /dev/null +++ b/toolkit/devtools/server/tests/browser/timeline-iframe-child.html @@ -0,0 +1,19 @@ + + +
+ +