Bug 1050773 - Timeline actor pulls profiletimeline markers from all docShells; r=paul

This commit is contained in:
Patrick Brosset 2014-09-18 11:12:33 +02:00
parent 67eed48f5b
commit 331201c0d8
11 changed files with 266 additions and 147 deletions

View File

@ -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, {

View File

@ -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'

View File

@ -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);
});
});

View File

@ -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));
});
};

View File

@ -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));
});
};

View File

@ -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());
});
})
}

View File

@ -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();
});

View File

@ -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;
}

View File

@ -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

View File

@ -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>

View File

@ -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>