Bug 1050384 - [timeline] build an actor to forward gecko operations. r=pbrosset

This commit is contained in:
Paul Rouget 2014-09-09 21:43:39 +02:00
parent 08e9b4f4ac
commit 27c72bfa18
10 changed files with 266 additions and 22 deletions

View File

@ -0,0 +1,136 @@
/* 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";
/**
* 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.
*
* This actor exposes this tracking mechanism to the devtools protocol.
*
* To start/stop recording markers:
* TimelineFront.start()
* TimelineFront.stop()
* TimelineFront.isRecording()
*
* When markers are available, an event is emitted:
* TimelineFront.on("markers", function(markers) {...})
*
*/
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");
const TIMELINE_DATA_PULL_TIMEOUT = 300;
exports.register = function(handle) {
handle.addGlobalActor(TimelineActor, "timelineActor");
handle.addTabActor(TimelineActor, "timelineActor");
};
exports.unregister = function(handle) {
handle.removeGlobalActor(TimelineActor);
handle.removeTabActor(TimelineActor);
};
/**
* The timeline actor pops and forwards timeline markers registered in
* a docshell.
*/
let 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}
* - 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;
},
/**
* 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;
protocol.Actor.prototype.destroy.call(this);
},
/**
* At regular intervals, pop the markers from the docshell, and forward
* markers if any.
*/
_pullTimelineData: function() {
let markers = this.docshell.popProfileTimelineMarkers();
if (markers.length > 0) {
events.emit(this, "markers", markers);
}
this._dataPullTimeout = setTimeout(() => this._pullTimelineData(),
TIMELINE_DATA_PULL_TIMEOUT);
},
/**
* Are we recording profile markers for the current docshell (window)?
*/
isRecording: method(function() {
return this.docshell.recordProfileTimelineMarkers;
}, {
request: {},
response: {
value: RetVal("boolean")
}
}),
/**
* Start/stop recording profile markers.
*/
start: method(function() {
if (!this.docshell.recordProfileTimelineMarkers) {
this.docshell.recordProfileTimelineMarkers = true;
this._pullTimelineData();
}
}, {oneway: true}),
stop: method(function() {
if (this.docshell.recordProfileTimelineMarkers) {
this.docshell.recordProfileTimelineMarkers = false;
clearTimeout(this._dataPullTimeout);
}
}, {oneway: true}),
});
exports.TimelineFront = protocol.FrontClass(TimelineActor, {
initialize: function(client, {timelineActor}) {
protocol.Front.prototype.initialize.call(this, client, {actor: timelineActor});
this.manage(this);
},
destroy: function() {
protocol.Front.prototype.destroy.call(this);
},
});

View File

@ -405,6 +405,7 @@ var DebuggerServer = {
this.registerModule("devtools/server/actors/layout");
this.registerModule("devtools/server/actors/csscoverage");
this.registerModule("devtools/server/actors/monitor");
this.registerModule("devtools/server/actors/timeline");
if ("nsIProfiler" in Ci) {
this.registerModule("devtools/server/actors/profiler");
}

View File

@ -56,6 +56,7 @@ EXTRA_JS_MODULES.devtools.server.actors += [
'actors/styleeditor.js',
'actors/styles.js',
'actors/stylesheets.js',
'actors/timeline.js',
'actors/tracer.js',
'actors/webapps.js',
'actors/webaudio.js',

View File

@ -15,4 +15,5 @@ support-files =
[browser_storage_listings.js]
[browser_storage_updates.js]
[browser_navigateEvents.js]
[browser_timeline.js]
skip-if = buildapp == 'mulet'

View File

@ -124,10 +124,8 @@ function getServerTabActor(callback) {
}
function test() {
waitForExplicitFinish();
// Open a test tab
addTab(URL1, function(doc) {
addTab(URL1).then(function(doc) {
getServerTabActor(function (tabActor) {
// In order to listen to internal will-navigate/navigate events
events.on(tabActor, "will-navigate", function (data) {

View File

@ -305,8 +305,7 @@ function testRemoveIframe() {
}
function test() {
waitForExplicitFinish();
addTab(MAIN_DOMAIN + "storage-dynamic-windows.html", function(doc) {
addTab(MAIN_DOMAIN + "storage-dynamic-windows.html").then(function(doc) {
try {
// Sometimes debugger server does not get destroyed correctly by previous
// tests.

View File

@ -639,8 +639,7 @@ let testIDBEntries = Task.async(function*(index, hosts, indexedDBActor) {
});
function test() {
waitForExplicitFinish();
addTab(MAIN_DOMAIN + "storage-listings.html", function(doc) {
addTab(MAIN_DOMAIN + "storage-listings.html").then(function(doc) {
try {
// Sometimes debugger server does not get destroyed correctly by previous
// tests.

View File

@ -231,8 +231,7 @@ function* UpdateTests(front, win, client) {
function test() {
waitForExplicitFinish();
addTab(MAIN_DOMAIN + "storage-updates.html", function(doc) {
addTab(MAIN_DOMAIN + "storage-updates.html").then(function(doc) {
try {
// Sometimes debugger server does not get destroyed correctly by previous
// tests.

View File

@ -0,0 +1,67 @@
/* 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;
let doc = yield addTab("data:text/html;charset=utf-8,mop");
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
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 front = TimelineFront(client, form);
let isActive = yield front.isRecording();
ok(!isActive, "Not initially recording");
doc.body.innerHeight; // flush any pending reflow
yield front.start();
isActive = yield front.isRecording();
ok(isActive, "Recording after start()");
doc.body.style.padding = "10px";
let markers = yield once(front, "markers");
ok(markers.length > 0, "markers were returned");
ok(markers.some(m => m.name == "Reflow"), "markers includes Reflow");
ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
doc.body.style.backgroundColor = "red";
markers = yield once(front, "markers");
ok(markers.length > 0, "markers were returned");
ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
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;
gBrowser.removeCurrentTab();
});

View File

@ -11,28 +11,71 @@ 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
waitForExplicitFinish();
/**
* Open a new tab at a URL and call a callback on load
* Define an async test based on a generator function
*/
function addTab(aURL, aCallback) {
waitForExplicitFinish();
function asyncTest(generator) {
return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
}
gBrowser.selectedTab = gBrowser.addTab();
content.location = aURL;
/**
* 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
* @return a promise that resolves to the document when the url is loaded
*/
let addTab = Task.async(function* (url) {
info("Adding a new tab with URL: '" + url + "'");
let tab = gBrowser.selectedTab = gBrowser.addTab();
let loaded = once(gBrowser.selectedBrowser, "load", true);
let tab = gBrowser.selectedTab;
let browser = gBrowser.getBrowserForTab(tab);
content.location = url;
yield loaded;
function onTabLoad(event) {
if (event.originalTarget.location.href != aURL) {
return;
info("URL '" + url + "' loading complete");
let def = promise.defer();
let isBlank = url == "about:blank";
waitForFocus(def.resolve, content, isBlank);
yield def.promise;
return tab.linkedBrowser.contentWindow.document;
});
/**
* 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
*/
function once(target, eventName, useCapture=false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
let deferred = promise.defer();
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
["on", "off"]
]) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
info("Got event: '" + eventName + "' on " + target + ".");
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}, useCapture);
break;
}
browser.removeEventListener("load", onTabLoad, true);
aCallback(browser.contentDocument);
}
browser.addEventListener("load", onTabLoad, true);
return deferred.promise;
}
/**