summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/webconsole/listeners/document-events.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/webconsole/listeners/document-events.js')
-rw-r--r--devtools/server/actors/webconsole/listeners/document-events.js247
1 files changed, 247 insertions, 0 deletions
diff --git a/devtools/server/actors/webconsole/listeners/document-events.js b/devtools/server/actors/webconsole/listeners/document-events.js
new file mode 100644
index 0000000000..1c1f926436
--- /dev/null
+++ b/devtools/server/actors/webconsole/listeners/document-events.js
@@ -0,0 +1,247 @@
+/* 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/. */
+
+// XPCNativeWrapper is not defined globally in ESLint as it may be going away.
+// See bug 1481337.
+/* global XPCNativeWrapper */
+
+"use strict";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+/**
+ * About "navigationStart - ${WILL_NAVIGATE_TIME_SHIFT}ms":
+ * Unfortunately dom-loading's navigationStart timestamp is older than the navigationStart we receive from will-navigate.
+ *
+ * That's because we record `navigationStart` before will-navigate code is called.
+ * And will-navigate code don't have access to performance.timing.navigationStart that dom-loading is using.
+ * The `performance.timing.navigationStart` is recorded earlier from `DocumentLoadListener.SetNavigating`, here:
+ * https://searchfox.org/mozilla-central/rev/9b430bb1a11d7152cab2af4574f451ffb906b052/netwerk/ipc/DocumentLoadListener.cpp#907-908
+ * https://searchfox.org/mozilla-central/rev/9b430bb1a11d7152cab2af4574f451ffb906b052/netwerk/ipc/DocumentLoadListener.cpp#820-823
+ * While this function is being called via `nsIWebProgressListener.onStateChange`, here:
+ * https://searchfox.org/mozilla-central/rev/9b430bb1a11d7152cab2af4574f451ffb906b052/netwerk/ipc/DocumentLoadListener.cpp#934-939
+ * And we record the navigationStart timestamp from onStateChange by using Date.now(), which is more recent
+ * than performance.timing.navigationStart.
+ *
+ * We do this workaround because all DOCUMENT_EVENT comes with a "time" timestamp.
+ * Each event relates to a particular event in the lifecycle of documents and are supposed to follow a particular order:
+ * - will-navigate (on the previous target)
+ * - dom-loading (on the new target)
+ * - dom-interactive
+ * - dom-complete
+ * And some tests are asserting this.
+ */
+const WILL_NAVIGATE_TIME_SHIFT = 20;
+exports.WILL_NAVIGATE_TIME_SHIFT = WILL_NAVIGATE_TIME_SHIFT;
+
+/**
+ * Forward `DOMContentLoaded` and `load` events with precise timing
+ * of when events happened according to window.performance numbers.
+ *
+ * @constructor
+ * @param WindowGlobalTarget targetActor
+ */
+function DocumentEventsListener(targetActor) {
+ this.targetActor = targetActor;
+
+ EventEmitter.decorate(this);
+ this.onWillNavigate = this.onWillNavigate.bind(this);
+ this.onWindowReady = this.onWindowReady.bind(this);
+ this.onContentLoaded = this.onContentLoaded.bind(this);
+ this.onLoad = this.onLoad.bind(this);
+}
+
+exports.DocumentEventsListener = DocumentEventsListener;
+
+DocumentEventsListener.prototype = {
+ listen() {
+ // When EFT is enabled, the Target Actor won't dispatch any will-navigate/window-ready event
+ // Instead listen to WebProgressListener interface directly, so that we can later drop the whole
+ // DebuggerProgressListener interface in favor of this class.
+ // Also, do not wait for "load" event as it can be blocked in case of error during the load
+ // or when calling window.stop(). We still want to emit "dom-complete" in these scenarios.
+ if (this.targetActor.ignoreSubFrames) {
+ // Ignore listening to anything if the page is already fully loaded.
+ // This can be the case when opening DevTools against an already loaded page
+ // or when doing bfcache navigations.
+ if (this.targetActor.window.document.readyState != "complete") {
+ this.webProgress = this.targetActor.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ this.webProgress.addProgressListener(
+ this,
+ Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
+ );
+ }
+ } else {
+ // Listen to will-navigate and do not emit a fake one as we only care about upcoming navigation
+ this.targetActor.on("will-navigate", this.onWillNavigate);
+
+ // Listen to window-ready and then fake one in order to notify about dom-loading for the existing document
+ this.targetActor.on("window-ready", this.onWindowReady);
+ }
+ // The target actor already emitted a window-ready for the top document when instantiating.
+ // So fake one for the top document right away.
+ this.onWindowReady({
+ window: this.targetActor.window,
+ isTopLevel: true,
+ });
+ },
+
+ onWillNavigate({
+ window,
+ isTopLevel,
+ newURI,
+ navigationStart,
+ isFrameSwitching,
+ }) {
+ // Ignore iframes
+ if (!isTopLevel) {
+ return;
+ }
+
+ this.emit("will-navigate", {
+ time: navigationStart - WILL_NAVIGATE_TIME_SHIFT,
+ newURI,
+ isFrameSwitching,
+ });
+ },
+
+ onWindowReady({ window, isTopLevel, isFrameSwitching }) {
+ // Ignore iframes
+ if (!isTopLevel) {
+ return;
+ }
+
+ const time = window.performance.timing.navigationStart;
+
+ this.emit("dom-loading", {
+ time,
+ isFrameSwitching,
+ });
+
+ const { readyState } = window.document;
+ if (readyState != "interactive" && readyState != "complete") {
+ // When EFT is enabled, we track this event via the WebProgressListener interface.
+ if (!this.targetActor.ignoreSubFrames) {
+ window.addEventListener(
+ "DOMContentLoaded",
+ e => this.onContentLoaded(e, isFrameSwitching),
+ {
+ once: true,
+ }
+ );
+ }
+ } else {
+ this.onContentLoaded({ target: window.document }, isFrameSwitching);
+ }
+ if (readyState != "complete") {
+ // When EFT is enabled, we track the load event via the WebProgressListener interface.
+ if (!this.targetActor.ignoreSubFrames) {
+ window.addEventListener("load", e => this.onLoad(e, isFrameSwitching), {
+ once: true,
+ });
+ }
+ } else {
+ this.onLoad({ target: window.document }, isFrameSwitching);
+ }
+ },
+
+ onContentLoaded(event, isFrameSwitching) {
+ if (this.destroyed) {
+ return;
+ }
+ // 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
+ const window = event.target.defaultView;
+ const time = window.performance.timing.domInteractive;
+ this.emit("dom-interactive", { time, isFrameSwitching });
+ },
+
+ onLoad(event, isFrameSwitching) {
+ if (this.destroyed) {
+ return;
+ }
+ // 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
+ const window = event.target.defaultView;
+ const time = window.performance.timing.domComplete;
+ this.emit("dom-complete", {
+ time,
+ isFrameSwitching,
+ hasNativeConsoleAPI: this.hasNativeConsoleAPI(window),
+ });
+ },
+
+ onStateChange(progress, request, flag, status) {
+ progress.QueryInterface(Ci.nsIDocShell);
+ // Ignore destroyed, or progress for same-process iframes
+ if (progress.isBeingDestroyed() || progress != this.webProgress) {
+ return;
+ }
+
+ const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
+ const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
+ const window = progress.DOMWindow;
+ if (isDocument && isStop) {
+ const time = window.performance.timing.domInteractive;
+ this.emit("dom-interactive", { time });
+ } else if (isWindow && isStop) {
+ const time = window.performance.timing.domComplete;
+ this.emit("dom-complete", {
+ time,
+ hasNativeConsoleAPI: this.hasNativeConsoleAPI(window),
+ });
+ }
+ },
+
+ /**
+ * Tells if the window.console object is native or overwritten by script in
+ * the page.
+ *
+ * @param nsIDOMWindow window
+ * The window object you want to check.
+ * @return boolean
+ * True if the window.console object is native, or false otherwise.
+ */
+ hasNativeConsoleAPI(window) {
+ let isNative = false;
+ try {
+ // We are very explicitly examining the "console" property of
+ // the non-Xrayed object here.
+ const console = window.wrappedJSObject.console;
+ // In xpcshell tests, console ends up being undefined and XPCNativeWrapper
+ // crashes in debug builds.
+ if (console) {
+ isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE === true;
+ }
+ } catch (ex) {
+ // ignore
+ }
+ return isNative;
+ },
+
+ destroy() {
+ // Also use a flag to silent onContentLoad and onLoad events
+ this.destroyed = true;
+ this.targetActor.off("will-navigate", this.onWillNavigate);
+ this.targetActor.off("window-ready", this.onWindowReady);
+ if (this.webProgress) {
+ this.webProgress.removeProgressListener(
+ this,
+ Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
+ );
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};