Bug 1432803 - Prevent using timeline actor to get DOMContentLoaded and load timings. r=Honza

MozReview-Commit-ID: Kkn36gDFTKN

--HG--
extra : rebase_source : e52861432f5651b864cada0c70568d504d883c6c
This commit is contained in:
Alexandre Poirot 2018-01-24 08:48:26 -08:00
Родитель 8780a97e4e
Коммит a273cd43a5
8 изменённых файлов: 189 добавлений и 41 удалений

Просмотреть файл

@ -5,10 +5,12 @@
"use strict";
const Services = require("Services");
const { TimelineFront } = require("devtools/shared/fronts/timeline");
const { ACTIVITY_TYPE, EVENTS } = require("../constants");
const FirefoxDataProvider = require("./firefox-data-provider");
// To be removed once FF60 is deprecated
loader.lazyRequireGetter(this, "TimelineFront", "devtools/shared/fronts/timeline", true);
class FirefoxConnector {
constructor() {
// Public methods
@ -18,6 +20,7 @@ class FirefoxConnector {
this.navigate = this.navigate.bind(this);
this.displayCachedEvents = this.displayCachedEvents.bind(this);
this.onDocLoadingMarker = this.onDocLoadingMarker.bind(this);
this.onDocEvent = this.onDocEvent.bind(this);
this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
this.setPreferences = this.setPreferences.bind(this);
this.triggerActivity = this.triggerActivity.bind(this);
@ -44,7 +47,7 @@ class FirefoxConnector {
actions: this.actions,
});
this.addListeners();
await this.addListeners();
// Listener for `will-navigate` event is (un)registered outside
// of the `addListeners` and `removeListeners` methods since
@ -54,64 +57,71 @@ class FirefoxConnector {
this.tabTarget.on("will-navigate", this.willNavigate);
this.tabTarget.on("navigate", this.navigate);
// Don't start up waiting for timeline markers if the server isn't
// recent enough to emit the markers we're interested in.
if (this.tabTarget.getTrait("documentLoadingMarkers")) {
this.timelineFront = new TimelineFront(this.tabTarget.client, this.tabTarget.form);
this.timelineFront.on("doc-loading", this.onDocLoadingMarker);
await this.timelineFront.start({ withDocLoadingEvents: true });
}
this.displayCachedEvents();
}
async disconnect() {
this.actions.batchReset();
this.removeListeners();
await this.removeListeners();
if (this.tabTarget) {
// Unregister `will-navigate` needs to be done before `this.timelineFront.destroy()`
// since this.tabTarget might be nullified after timelineFront.destroy().
this.tabTarget.off("will-navigate");
// The timeline front wasn't initialized and started if the server wasn't
// recent enough to emit the markers we were interested in.
if (this.tabTarget.getTrait("documentLoadingMarkers") && this.timelineFront) {
this.timelineFront.off("doc-loading", this.onDocLoadingMarker);
await this.timelineFront.destroy();
}
this.tabTarget = null;
}
this.webConsoleClient = null;
this.timelineFront = null;
this.dataProvider = null;
this.panel = null;
}
pause() {
this.removeListeners();
async pause() {
await this.removeListeners();
}
resume() {
this.addListeners();
async resume() {
await this.addListeners();
}
addListeners() {
async addListeners() {
this.tabTarget.on("close", this.disconnect);
this.webConsoleClient.on("networkEvent",
this.dataProvider.onNetworkEvent);
this.webConsoleClient.on("networkEventUpdate",
this.dataProvider.onNetworkEventUpdate);
this.webConsoleClient.on("documentEvent", this.onDocEvent);
// With FF60+ console actor supports listening to document events like
// DOMContentLoaded and load. We used to query Timeline actor, but it was too CPU
// intensive.
let { startedListeners } = await this.webConsoleClient.startListeners(
["DocumentEvents"]);
// Allows to know if we are on FF60 and support these events.
let supportsDocEvents = startedListeners.includes("DocumentEvents");
// Don't start up waiting for timeline markers if the server isn't
// recent enough (<FF45) to emit the markers we're interested in.
if (!supportsDocEvents && !this.timelineFront &&
this.tabTarget.getTrait("documentLoadingMarkers")) {
this.timelineFront = new TimelineFront(this.tabTarget.client, this.tabTarget.form);
this.timelineFront.on("doc-loading", this.onDocLoadingMarker);
await this.timelineFront.start({ withDocLoadingEvents: true });
}
}
removeListeners() {
async removeListeners() {
if (this.tabTarget) {
this.tabTarget.off("close");
}
if (this.timelineFront) {
this.timelineFront.off("doc-loading", this.onDocLoadingMarker);
await this.timelineFront.destroy();
this.timelineFront = null;
}
if (this.webConsoleClient) {
this.webConsoleClient.off("networkEvent");
this.webConsoleClient.off("networkEventUpdate");
this.webConsoleClient.off("docEvent");
}
}
@ -176,11 +186,32 @@ class FirefoxConnector {
/**
* The "DOMContentLoaded" and "Load" events sent by the timeline actor.
*
* To be removed once FF60 is deprecated.
*
* @param {object} marker
*/
onDocLoadingMarker(marker) {
window.emit(EVENTS.TIMELINE_EVENT, marker);
this.actions.addTimingMarker(marker);
// Translate marker into event similar to newer "docEvent" event sent by the console
// actor
let event = {
name: marker.name == "document::DOMContentLoaded" ?
"dom-interactive" : "dom-complete",
time: marker.unixTime / 1000
};
window.emit(EVENTS.TIMELINE_EVENT, event);
this.actions.addTimingMarker(event);
}
/**
* The "DOMContentLoaded" and "Load" events sent by the console actor.
*
* Only used by FF60+.
*
* @param {object} marker
*/
onDocEvent(type, event) {
window.emit(EVENTS.TIMELINE_EVENT, event);
this.actions.addTimingMarker(event);
}
/**

Просмотреть файл

@ -30,7 +30,7 @@ class Connector {
// Connect/Disconnect API
connect(connection, actions, getState) {
async connect(connection, actions, getState) {
if (!connection || !connection.tab) {
return;
}
@ -38,10 +38,10 @@ class Connector {
let { clientType } = connection.tab;
switch (clientType) {
case "chrome":
this.connectChrome(connection, actions, getState);
await this.connectChrome(connection, actions, getState);
break;
case "firefox":
this.connectFirefox(connection, actions, getState);
await this.connectFirefox(connection, actions, getState);
break;
default:
throw Error(`Unknown client type - ${clientType}`);
@ -54,20 +54,20 @@ class Connector {
connectChrome(connection, actions, getState) {
this.connector = require("./chrome-connector");
this.connector.connect(connection, actions, getState);
return this.connector.connect(connection, actions, getState);
}
connectFirefox(connection, actions, getState) {
this.connector = require("./firefox-connector");
this.connector.connect(connection, actions, getState);
return this.connector.connect(connection, actions, getState);
}
pause() {
this.connector.pause();
return this.connector.pause();
}
resume() {
this.connector.resume();
return this.connector.resume();
}
// Public API

Просмотреть файл

@ -20,15 +20,15 @@ function TimingMarkers() {
function addTimingMarker(state, action) {
state = { ...state };
if (action.marker.name === "document::DOMContentLoaded" &&
if (action.marker.name === "dom-interactive" &&
state.firstDocumentDOMContentLoadedTimestamp === -1) {
state.firstDocumentDOMContentLoadedTimestamp = action.marker.unixTime / 1000;
state.firstDocumentDOMContentLoadedTimestamp = action.marker.time;
return state;
}
if (action.marker.name === "document::Load" &&
if (action.marker.name === "dom-complete" &&
state.firstDocumentLoadTimestamp === -1) {
state.firstDocumentLoadTimestamp = action.marker.unixTime / 1000;
state.firstDocumentLoadTimestamp = action.marker.time;
return state;
}

Просмотреть файл

@ -23,9 +23,9 @@ add_task(function* () {
ok(true, "Reloading finished");
is(markers[0].name, "document::DOMContentLoaded",
is(markers[0].name, "dom-interactive",
"The first received marker is correct.");
is(markers[1].name, "document::Load",
is(markers[1].name, "dom-complete",
"The second received marker is correct.");
return teardown(monitor);

Просмотреть файл

@ -40,6 +40,7 @@ if (isWorker) {
loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/server/actors/webconsole/listeners", true);
loader.lazyRequireGetter(this, "ConsoleReflowListener", "devtools/server/actors/webconsole/listeners", true);
loader.lazyRequireGetter(this, "ContentProcessListener", "devtools/server/actors/webconsole/listeners", true);
loader.lazyRequireGetter(this, "DocumentEventsListener", "devtools/server/actors/webconsole/listeners", true);
}
function isObject(value) {
@ -694,6 +695,16 @@ WebConsoleActor.prototype =
}
startedListeners.push(listener);
break;
case "DocumentEvents":
// Workers don't support this message type
if (isWorker) {
break;
}
if (!this.documentEventsListener) {
this.documentEventsListener = new DocumentEventsListener(this);
}
startedListeners.push(listener);
break;
}
}
@ -786,6 +797,13 @@ WebConsoleActor.prototype =
}
stoppedListeners.push(listener);
break;
case "DocumentEvents":
if (this.documentEventsListener) {
this.documentEventsListener.destroy();
this.documentEventsListener = null;
}
stoppedListeners.push(listener);
break;
}
}

Просмотреть файл

@ -14,6 +14,7 @@ XPCOMUtils.defineLazyServiceGetter(this,
"swm",
"@mozilla.org/serviceworkers/manager;1",
"nsIServiceWorkerManager");
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
// Process script used to forward console calls from content processes to parent process
const CONTENT_PROCESS_SCRIPT = "resource://devtools/server/actors/webconsole/content-process-forward.js";
@ -490,3 +491,83 @@ ContentProcessListener.prototype = {
this.listener = null;
}
};
/**
* Forward `DOMContentLoaded` and `load` events with precise timing
* of when events happened according to window.performance numbers.
*
* @constructor
* @param object console
* The web console actor.
*/
function DocumentEventsListener(console) {
this.console = console;
this.onWindowReady = this.onWindowReady.bind(this);
this.onContentLoaded = this.onContentLoaded.bind(this);
this.onLoad = this.onLoad.bind(this);
this.listen();
}
exports.DocumentEventsListener = DocumentEventsListener;
DocumentEventsListener.prototype = {
listen() {
EventEmitter.on(this.console.parentActor, "window-ready", this.onWindowReady);
this.onWindowReady({ window: this.console.window, isTopLevel: true });
},
onWindowReady({ window, isTopLevel }) {
// Ignore iframes
if (!isTopLevel) {
return;
}
let { readyState } = window.document;
if (readyState != "interactive" && readyState != "complete") {
window.addEventListener("DOMContentLoaded", this.onContentLoaded, { once: true });
} else {
this.onContentLoaded({ target: window.document });
}
if (readyState != "complete") {
window.addEventListener("load", this.onLoad, { once: true });
} else {
this.onLoad({ target: window.document });
}
},
onContentLoaded(event) {
let window = event.target.defaultView;
let packet = {
from: this.console.actorID,
type: "documentEvent",
name: "dom-interactive",
// milliseconds since the UNIX epoch, when the parser finished its work
// on the main document, that is when its Document.readyState changes to
// 'interactive' and the corresponding readystatechange event is thrown
time: window.performance.timing.domInteractive
};
this.console.conn.send(packet);
},
onLoad(event) {
let window = event.target.defaultView;
let packet = {
from: this.console.actorID,
type: "documentEvent",
name: "dom-complete",
// milliseconds since the UNIX epoch, when the parser finished its work
// on the main document, that is when its Document.readyState changes to
// 'complete' and the corresponding readystatechange event is thrown
time: window.performance.timing.domComplete
};
this.console.conn.send(packet);
},
destroy() {
EventEmitter.off(this.console.parentActor, "window-ready", this.onWindowReady);
this.listener = null;
}
};

Просмотреть файл

@ -27,6 +27,7 @@ const UnsolicitedNotifications = {
"logMessage": "logMessage",
"networkEvent": "networkEvent",
"networkEventUpdate": "networkEventUpdate",
"documentEvent": "documentEvent",
"newGlobal": "newGlobal",
"newScript": "newScript",
"tabDetached": "tabDetached",

Просмотреть файл

@ -33,11 +33,13 @@ function WebConsoleClient(debuggerClient, response) {
this.onNetworkEvent = this._onNetworkEvent.bind(this);
this.onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
this.onInspectObject = this._onInspectObject.bind(this);
this.onDocEvent = this._onDocEvent.bind(this);
this._client.addListener("evaluationResult", this.onEvaluationResult);
this._client.addListener("networkEvent", this.onNetworkEvent);
this._client.addListener("networkEventUpdate", this.onNetworkEventUpdate);
this._client.addListener("inspectObject", this.onInspectObject);
this._client.addListener("documentEvent", this.onDocEvent);
EventEmitter.decorate(this);
}
@ -187,6 +189,20 @@ WebConsoleClient.prototype = {
this.emit("inspectObject", packet);
},
/**
* The "docEvent" message type handler. We just re-emit it so that
* the tools can listen for them on the console client.
*
* @private
* @param string type
* Message type.
* @param object packet
* The message received from the server.
*/
_onDocEvent: function (type, packet) {
this.emit("documentEvent", packet);
},
/**
* Retrieve the cached messages from the server.
*
@ -676,6 +692,7 @@ WebConsoleClient.prototype = {
this._client.removeListener("networkEventUpdate",
this.onNetworkEventUpdate);
this._client.removeListener("inspectObject", this.onInspectObject);
this._client.removeListener("documentEvent", this.onDocEvent);
this.stopListeners(null, onResponse);
this._longStrings = null;
this._client = null;