Bug 1648149 - Split GeckoViewContent in enable and init phases. r=snorp

This commit splits the `GeckoViewContent` actor in two parts:

- `GeckoViewContent` proper, which runs unconditionally and handles code that
  needs to run regardless of whether we have a delegate installed or not.

- `ContentDelegate` which runs only when a delegate is first installed.

This emulates the previous paradigm of installing some listeners only when the
delegate is installed.

I discussed it briefly with :nika and she thinks that splitting modules in two
should not affect performance in a measurable manner.

Note that actors cannot be registered per-window, so we will get messages from
all windows as long as one content delegate is registered.

Differential Revision: https://phabricator.services.mozilla.com/D86776
This commit is contained in:
Agi Sferro 2020-08-13 20:24:52 +00:00
parent 4c8aa34ba2
commit aaa44e891b
8 changed files with 258 additions and 162 deletions

View file

@ -0,0 +1,169 @@
/* 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/. */
const { GeckoViewActorChild } = ChromeUtils.import(
"resource://gre/modules/GeckoViewActorChild.jsm"
);
var { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
ManifestObtainer: "resource://gre/modules/ManifestObtainer.jsm",
});
var EXPORTED_SYMBOLS = ["ContentDelegateChild"];
class ContentDelegateChild extends GeckoViewActorChild {
notifyParentOfViewportFit() {
if (this.triggerViewportFitChange) {
this.contentWindow.cancelIdleCallback(this.triggerViewportFitChange);
}
this.triggerViewportFitChange = this.contentWindow.requestIdleCallback(
() => {
this.triggerViewportFitChange = null;
const viewportFit = this.contentWindow.windowUtils.getViewportFitInfo();
if (this.lastViewportFit === viewportFit) {
return;
}
this.lastViewportFit = viewportFit;
this.eventDispatcher.sendRequest({
type: "GeckoView:DOMMetaViewportFit",
viewportfit: viewportFit,
});
}
);
}
// eslint-disable-next-line complexity
handleEvent(aEvent) {
debug`handleEvent: ${aEvent.type}`;
switch (aEvent.type) {
case "contextmenu": {
function nearestParentAttribute(aNode, aAttribute) {
while (
aNode &&
aNode.hasAttribute &&
!aNode.hasAttribute(aAttribute)
) {
aNode = aNode.parentNode;
}
return aNode && aNode.getAttribute && aNode.getAttribute(aAttribute);
}
function createAbsoluteUri(aBaseUri, aUri) {
if (!aUri || !aBaseUri || !aBaseUri.displaySpec) {
return null;
}
return Services.io.newURI(aUri, null, aBaseUri).displaySpec;
}
const node = aEvent.composedTarget;
const baseUri = node.ownerDocument.baseURIObject;
const uri = createAbsoluteUri(
baseUri,
nearestParentAttribute(node, "href")
);
const title = nearestParentAttribute(node, "title");
const alt = nearestParentAttribute(node, "alt");
const elementType = ChromeUtils.getClassName(node);
const isImage = elementType === "HTMLImageElement";
const isMedia =
elementType === "HTMLVideoElement" ||
elementType === "HTMLAudioElement";
const elementSrc =
(isImage || isMedia) && (node.currentSrc || node.src);
if (uri || isImage || isMedia) {
const msg = {
type: "GeckoView:ContextMenu",
screenX: aEvent.screenX,
screenY: aEvent.screenY,
baseUri: (baseUri && baseUri.displaySpec) || null,
uri,
title,
alt,
elementType,
elementSrc: elementSrc || null,
};
this.eventDispatcher.sendRequest(msg);
aEvent.preventDefault();
}
break;
}
case "MozDOMFullscreen:Request": {
this.sendAsyncMessage("GeckoView:DOMFullscreenRequest", {});
break;
}
case "MozDOMFullscreen:Entered":
case "MozDOMFullscreen:Exited":
// Content may change fullscreen state by itself, and we should ensure
// that the parent always exits fullscreen when content has left
// full screen mode.
if (this.contentWindow?.document.fullscreenElement) {
break;
}
// fall-through
case "MozDOMFullscreen:Exit":
this.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
break;
case "DOMMetaViewportFitChanged":
if (aEvent.originalTarget.ownerGlobal == this.contentWindow) {
this.notifyParentOfViewportFit();
}
break;
case "DOMTitleChanged":
this.eventDispatcher.sendRequest({
type: "GeckoView:DOMTitleChanged",
title: this.contentWindow.document.title,
});
break;
case "DOMWindowClose":
if (!aEvent.isTrusted) {
return;
}
aEvent.preventDefault();
this.eventDispatcher.sendRequest({
type: "GeckoView:DOMWindowClose",
});
break;
case "DOMContentLoaded": {
if (aEvent.originalTarget.ownerGlobal == this.contentWindow) {
// If loaded content doesn't have viewport-fit, parent still
// uses old value of previous content.
this.notifyParentOfViewportFit();
}
this.contentWindow.requestIdleCallback(async () => {
const manifest = await ManifestObtainer.contentObtainManifest(
this.contentWindow
);
if (manifest) {
this.eventDispatcher.sendRequest({
type: "GeckoView:WebAppManifest",
manifest,
});
}
});
break;
}
case "MozFirstContentfulPaint": {
this.eventDispatcher.sendRequest({
type: "GeckoView:FirstContentfulPaint",
});
break;
}
}
}
}
// eslint-disable-next-line no-unused-vars
const { debug, warn } = ContentDelegateChild.initLogging(
"ContentDelegateChild"
);

View file

@ -3,15 +3,15 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["GeckoViewContentParent"];
var EXPORTED_SYMBOLS = ["ContentDelegateParent"];
const { GeckoViewUtils } = ChromeUtils.import(
"resource://gre/modules/GeckoViewUtils.jsm"
);
const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewContentParent"); // eslint-disable-line no-unused-vars
const { debug, warn } = GeckoViewUtils.initLogging("ContentDelegateParent"); // eslint-disable-line no-unused-vars
class GeckoViewContentParent extends JSWindowActorParent {
class ContentDelegateParent extends JSWindowActorParent {
async receiveMessage(aMsg) {
debug`receiveMessage: ${aMsg.name} ${aMsg}`;

View file

@ -25,7 +25,6 @@ const SCROLL_BEHAVIOR_AUTO = 1;
XPCOMUtils.defineLazyModuleGetters(this, {
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
ManifestObtainer: "resource://gre/modules/ManifestObtainer.jsm",
PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm",
SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm",
Utils: "resource://gre/modules/sessionstore/Utils.jsm",
@ -65,26 +64,6 @@ class GeckoViewContentChild extends GeckoViewActorChild {
return windowUtils.SCROLL_MODE_SMOOTH;
}
notifyParentOfViewportFit() {
if (this.triggerViewportFitChange) {
this.contentWindow.cancelIdleCallback(this.triggerViewportFitChange);
}
this.triggerViewportFitChange = this.contentWindow.requestIdleCallback(
() => {
this.triggerViewportFitChange = null;
const viewportFit = this.contentWindow.windowUtils.getViewportFitInfo();
if (this.lastViewportFit === viewportFit) {
return;
}
this.lastViewportFit = viewportFit;
this.eventDispatcher.sendRequest({
type: "GeckoView:DOMMetaViewportFit",
viewportfit: viewportFit,
});
}
);
}
collectSessionState() {
const { docShell, contentWindow } = this;
const history = SessionHistory.collect(docShell);
@ -355,96 +334,6 @@ class GeckoViewContentChild extends GeckoViewActorChild {
debug`handleEvent: ${aEvent.type}`;
switch (aEvent.type) {
case "contextmenu": {
function nearestParentAttribute(aNode, aAttribute) {
while (
aNode &&
aNode.hasAttribute &&
!aNode.hasAttribute(aAttribute)
) {
aNode = aNode.parentNode;
}
return aNode && aNode.getAttribute && aNode.getAttribute(aAttribute);
}
function createAbsoluteUri(aBaseUri, aUri) {
if (!aUri || !aBaseUri || !aBaseUri.displaySpec) {
return null;
}
return Services.io.newURI(aUri, null, aBaseUri).displaySpec;
}
const node = aEvent.composedTarget;
const baseUri = node.ownerDocument.baseURIObject;
const uri = createAbsoluteUri(
baseUri,
nearestParentAttribute(node, "href")
);
const title = nearestParentAttribute(node, "title");
const alt = nearestParentAttribute(node, "alt");
const elementType = ChromeUtils.getClassName(node);
const isImage = elementType === "HTMLImageElement";
const isMedia =
elementType === "HTMLVideoElement" ||
elementType === "HTMLAudioElement";
const elementSrc =
(isImage || isMedia) && (node.currentSrc || node.src);
if (uri || isImage || isMedia) {
const msg = {
type: "GeckoView:ContextMenu",
screenX: aEvent.screenX,
screenY: aEvent.screenY,
baseUri: (baseUri && baseUri.displaySpec) || null,
uri,
title,
alt,
elementType,
elementSrc: elementSrc || null,
};
this.eventDispatcher.sendRequest(msg);
aEvent.preventDefault();
}
break;
}
case "MozDOMFullscreen:Request": {
this.sendAsyncMessage("GeckoView:DOMFullscreenRequest", {});
break;
}
case "MozDOMFullscreen:Entered":
case "MozDOMFullscreen:Exited":
// Content may change fullscreen state by itself, and we should ensure
// that the parent always exits fullscreen when content has left
// full screen mode.
if (this.contentWindow?.document.fullscreenElement) {
break;
}
// fall-through
case "MozDOMFullscreen:Exit":
this.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
break;
case "DOMMetaViewportFitChanged":
if (aEvent.originalTarget.ownerGlobal == this.contentWindow) {
this.notifyParentOfViewportFit();
}
break;
case "DOMTitleChanged":
this.eventDispatcher.sendRequest({
type: "GeckoView:DOMTitleChanged",
title: this.contentWindow.document.title,
});
break;
case "DOMWindowClose":
if (!aEvent.isTrusted) {
return;
}
aEvent.preventDefault();
this.eventDispatcher.sendRequest({
type: "GeckoView:DOMWindowClose",
});
break;
case "mozcaretstatechanged":
if (
aEvent.reason === "presscaret" ||
@ -456,31 +345,6 @@ class GeckoViewContentChild extends GeckoViewActorChild {
});
}
break;
case "DOMContentLoaded": {
if (aEvent.originalTarget.ownerGlobal == this.contentWindow) {
// If loaded content doesn't have viewport-fit, parent still
// uses old value of previous content.
this.notifyParentOfViewportFit();
}
this.contentWindow.requestIdleCallback(async () => {
const manifest = await ManifestObtainer.contentObtainManifest(
this.contentWindow
);
if (manifest) {
this.eventDispatcher.sendRequest({
type: "GeckoView:WebAppManifest",
manifest,
});
}
});
break;
}
case "MozFirstContentfulPaint": {
this.eventDispatcher.sendRequest({
type: "GeckoView:FirstContentfulPaint",
});
break;
}
}
}
}

View file

@ -7,8 +7,9 @@ with Files('**'):
FINAL_TARGET_FILES.actors += [
'BrowserTabParent.jsm',
'ContentDelegateChild.jsm',
'ContentDelegateParent.jsm',
'GeckoViewContentChild.jsm',
'GeckoViewContentParent.jsm',
'LoadURIDelegateChild.jsm',
'WebBrowserChromeChild.jsm',
]

View file

@ -15,6 +15,7 @@ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
EventDispatcher: "resource://gre/modules/Messaging.jsm",
GeckoViewActorManager: "resource://gre/modules/GeckoViewActorManager.jsm",
GeckoViewSettings: "resource://gre/modules/GeckoViewSettings.jsm",
GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
HistogramStopwatch: "resource://gre/modules/GeckoViewTelemetry.jsm",
@ -346,6 +347,10 @@ class ModuleInfo {
// onInitBrowser() override. However, load content module after initializing
// browser, because we don't have a message manager before then.
this._loadResource(onInit);
this._loadActors(onInit);
if (this._enabledOnInit) {
this._loadActors(onEnable);
}
this._onInitPhase = onInit;
this._onEnablePhase = onEnable;
@ -383,6 +388,14 @@ class ModuleInfo {
this._contentModuleLoaded = false;
}
_loadActors(aPhase) {
if (!aPhase || !aPhase.actors) {
return;
}
GeckoViewActorManager.addJSWindowActors(aPhase.actors);
}
/**
* Load resource according to a phase object that contains possible keys,
*
@ -445,6 +458,7 @@ class ModuleInfo {
if (aEnabled) {
this._loadResource(this._onEnablePhase);
this._loadFrameScript(this._onEnablePhase);
this._loadActors(this._onEnablePhase);
if (this._impl) {
this._impl.onEnable();
this._impl.onSettingsUpdate();
@ -518,6 +532,42 @@ function startup() {
name: "GeckoViewContent",
onInit: {
resource: "resource://gre/modules/GeckoViewContent.jsm",
actors: {
GeckoViewContent: {
child: {
moduleURI: "resource:///actors/GeckoViewContentChild.jsm",
events: {
mozcaretstatechanged: { capture: true, mozSystemGroup: true },
},
allFrames: true,
},
},
},
},
onEnable: {
actors: {
ContentDelegate: {
parent: {
moduleURI: "resource:///actors/ContentDelegateParent.jsm",
},
child: {
moduleURI: "resource:///actors/ContentDelegateChild.jsm",
events: {
DOMContentLoaded: {},
DOMMetaViewportFitChanged: {},
DOMTitleChanged: {},
DOMWindowClose: {},
"MozDOMFullscreen:Entered": {},
"MozDOMFullscreen:Exit": {},
"MozDOMFullscreen:Exited": {},
"MozDOMFullscreen:Request": {},
MozFirstContentfulPaint: {},
contextmenu: { capture: true },
},
allFrames: true,
},
},
},
},
},
{

View file

@ -28,28 +28,6 @@ const JSWINDOWACTORS = {
moduleURI: "resource:///actors/BrowserTabParent.jsm",
},
},
GeckoViewContent: {
parent: {
moduleURI: "resource:///actors/GeckoViewContentParent.jsm",
},
child: {
moduleURI: "resource:///actors/GeckoViewContentChild.jsm",
events: {
DOMContentLoaded: {},
DOMMetaViewportFitChanged: {},
DOMTitleChanged: {},
DOMWindowClose: {},
"MozDOMFullscreen:Entered": {},
"MozDOMFullscreen:Exit": {},
"MozDOMFullscreen:Exited": {},
"MozDOMFullscreen:Request": {},
MozFirstContentfulPaint: {},
contextmenu: { capture: true },
mozcaretstatechanged: { capture: true, mozSystemGroup: true },
},
allFrames: true,
},
},
LoadURIDelegate: {
child: {
moduleURI: "resource:///actors/LoadURIDelegateChild.jsm",

View file

@ -0,0 +1,33 @@
/* 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";
const EXPORTED_SYMBOLS = ["GeckoViewActorManager"];
const { GeckoViewUtils } = ChromeUtils.import(
"resource://gre/modules/GeckoViewUtils.jsm"
);
const actors = new Set();
var GeckoViewActorManager = {
addJSWindowActors(actors) {
for (const [actorName, actor] of Object.entries(actors)) {
this._register(actorName, actor);
}
},
_register(actorName, actor) {
if (actors.has(actorName)) {
// Actor already registered, nothing to do
return;
}
ChromeUtils.registerWindowActor(actorName, actor);
actors.add(actorName);
},
};
const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewActorManager"); // eslint-disable-line no-unused-vars

View file

@ -10,6 +10,7 @@ EXTRA_JS_MODULES += [
'ContentCrashHandler.jsm',
'DelayedInit.jsm',
'GeckoViewActorChild.jsm',
'GeckoViewActorManager.jsm',
'GeckoViewAutocomplete.jsm',
'GeckoViewAutofill.jsm',
'GeckoViewChildModule.jsm',