browser(firefox): use browsingContextID for frame IDs (#3999)

BrowsingContextIDs are consistent across the processes, so we can use
them to target frames in both browser and content processes. This will
aid browser-side navigation.

As a nice side-effect, we can drop a round-trip to the content process
for every `requestWillBeSent` event since we *almost* always can
attribute all network events to the proper parent frames.

I say "almost", because we in fact **fail** to correctly attribute requests
from workers that are instantiated by subframes. This, however, is
not working in Chromium ATM, so I consider this to be a minor regression
that is worth the simplification.
This commit is contained in:
Andrey Lushnikov 2020-09-29 11:22:00 -07:00 committed by GitHub
parent b3497b333e
commit 2631e1a809
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 32 additions and 156 deletions

View file

@ -1,2 +1,2 @@
1173
Changed: yurys@chromium.org Thu Sep 10 12:35:25 PDT 2020
1174
Changed: lushnikov@chromium.org Tue Sep 29 02:02:37 MDT 2020

View file

@ -6,6 +6,7 @@ const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerat
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
class Helper {
addObserver(handler, topic) {
Services.obs.addObserver(handler, topic);
return () => Services.obs.removeObserver(handler, topic);
@ -112,6 +113,12 @@ class Helper {
}
return '<unknown error>';
}
browsingContextToFrameId(browsingContext) {
if (!browsingContext)
return undefined;
return 'frame-' + browsingContext.id;
}
}
var EXPORTED_SYMBOLS = [ "Helper" ];

View file

@ -489,10 +489,24 @@ class NetworkRequest {
const pageNetwork = this._activePageNetwork();
if (!pageNetwork)
return;
const causeType = this.httpChannel.loadInfo ? this.httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
const internalCauseType = this.httpChannel.loadInfo ? this.httpChannel.loadInfo.internalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
const loadInfo = this.httpChannel.loadInfo;
const causeType = loadInfo?.externalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER;
const internalCauseType = loadInfo?.internalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER;
let browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.browsingContext;
// TODO: Unfortunately, requests from web workers don't have frameBrowsingContext or
// browsingContext.
//
// We fail to attribute them to the original frames on the browser side, but we
// can use load context top frame to attribute them to the top frame at least.
if (!browsingContext) {
const loadContext = helper.getLoadContext(this.httpChannel);
browsingContext = loadContext?.topFrameElement?.browsingContext;
}
pageNetwork.emit(PageNetwork.Events.Request, {
url: this.httpChannel.URI.spec,
frameId: helper.browsingContextToFrameId(browsingContext),
isIntercepted,
requestId: this.requestId,
redirectedFrom: this.redirectedFromId,

View file

@ -318,7 +318,7 @@ class Frame {
this._runtime = runtime;
this._docShell = docShell;
this._children = new Set();
this._frameId = helper.generateId();
this._frameId = helper.browsingContextToFrameId(this._docShell.browsingContext);
this._parentFrame = null;
this._url = '';
if (docShell.domWindow && docShell.domWindow.location)

View file

@ -1,61 +0,0 @@
/* 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 Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const helper = new Helper();
class NetworkMonitor {
constructor(rootDocShell, frameTree) {
this._frameTree = frameTree;
this._requestDetails = new Map();
this._eventListeners = [
helper.addObserver(this._onRequest.bind(this), 'http-on-opening-request'),
];
}
_onRequest(channel) {
if (!(channel instanceof Ci.nsIHttpChannel))
return;
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
const loadContext = helper.getLoadContext(httpChannel);
if (!loadContext)
return;
try {
const window = loadContext.associatedWindow;
const frame = this._frameTree.frameForDocShell(window.docShell);
if (!frame)
return;
const typeId = httpChannel.loadInfo ? httpChannel.loadInfo.internalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
// Channel ids are not unique. We combine them with the typeId
// to better distinguish requests. For example, favicon requests
// have the same channel id as their associated document request.
const channelKey = httpChannel.channelId + ':' + typeId;
this._requestDetails.set(channelKey, {
frameId: frame.id(),
});
} catch (e) {
// Accessing loadContext.associatedWindow sometimes throws.
}
}
requestDetails(channelKey) {
return this._requestDetails.get(channelKey) || null;
}
dispose() {
this._requestDetails.clear();
helper.removeListeners(this._eventListeners);
}
}
var EXPORTED_SYMBOLS = ['NetworkMonitor'];
this.NetworkMonitor = NetworkMonitor;

View file

@ -112,7 +112,7 @@ class FrameData {
}
class PageAgent {
constructor(messageManager, browserChannel, sessionId, frameTree, networkMonitor) {
constructor(messageManager, browserChannel, sessionId, frameTree) {
this._messageManager = messageManager;
this._browserChannel = browserChannel;
this._sessionId = sessionId;
@ -120,7 +120,6 @@ class PageAgent {
this._browserRuntime = browserChannel.connect(sessionId + 'runtime');
this._frameTree = frameTree;
this._runtime = frameTree.runtime();
this._networkMonitor = networkMonitor;
this._frameData = new Map();
this._workerData = new Map();
@ -146,7 +145,6 @@ class PageAgent {
navigate: this._navigate.bind(this),
reload: this._reload.bind(this),
removeScriptToEvaluateOnNewDocument: this._removeScriptToEvaluateOnNewDocument.bind(this),
requestDetails: this._requestDetails.bind(this),
screenshot: this._screenshot.bind(this),
scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
setCacheDisabled: this._setCacheDisabled.bind(this),
@ -170,10 +168,6 @@ class PageAgent {
this._dataTransfer = null;
}
_requestDetails({channelKey}) {
return this._networkMonitor.requestDetails(channelKey);
}
async _setEmulatedMedia({type, colorScheme}) {
const docShell = this._frameTree.mainFrame().docShell();
const cv = docShell.contentViewer;

View file

@ -5,19 +5,17 @@
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
const {NetworkMonitor} = ChromeUtils.import('chrome://juggler/content/content/NetworkMonitor.js');
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
let frameTree;
let networkMonitor;
const helper = new Helper();
const messageManager = this;
const sessions = new Map();
function createContentSession(channel, sessionId) {
const pageAgent = new PageAgent(messageManager, channel, sessionId, frameTree, networkMonitor);
const pageAgent = new PageAgent(messageManager, channel, sessionId, frameTree);
sessions.set(sessionId, [pageAgent]);
pageAgent.enable();
}
@ -118,7 +116,6 @@ function initialize() {
frameTree.addScriptToEvaluateOnNewDocument(script);
for (const { name, script } of bindings)
frameTree.addBinding(name, script);
networkMonitor = new NetworkMonitor(docShell, frameTree);
const channel = SimpleChannel.createForMessageManager('content::page', messageManager);
@ -180,7 +177,6 @@ function initialize() {
for (const sessionId of sessions.keys())
disposeContentSession(sessionId);
networkMonitor.dispose();
frameTree.dispose();
}),
];

View file

@ -18,7 +18,6 @@ juggler.jar:
content/protocol/AccessibilityHandler.js (protocol/AccessibilityHandler.js)
content/content/main.js (content/main.js)
content/content/FrameTree.js (content/FrameTree.js)
content/content/NetworkMonitor.js (content/NetworkMonitor.js)
content/content/PageAgent.js (content/PageAgent.js)
content/content/Runtime.js (content/Runtime.js)
content/content/WorkerMain.js (content/WorkerMain.js)

View file

@ -16,13 +16,9 @@ const helper = new Helper();
class NetworkHandler {
constructor(target, session, contentChannel) {
this._session = session;
this._contentPage = contentChannel.connect(session.sessionId() + 'page');
this._httpActivity = new Map();
this._enabled = false;
this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
this._requestInterception = false;
this._eventListeners = [];
this._pendingRequstWillBeSentEvents = new Set();
}
async enable() {
@ -51,9 +47,6 @@ class NetworkHandler {
this._pageNetwork.enableRequestInterception();
else
this._pageNetwork.disableRequestInterception();
// Right after we enable/disable request interception we need to await all pending
// requestWillBeSent events before successfully returning from the method.
await Promise.all(Array.from(this._pendingRequstWillBeSentEvents));
}
async resumeInterceptedRequest({requestId, method, headers, postData}) {
@ -69,89 +62,23 @@ class NetworkHandler {
}
dispose() {
this._contentPage.dispose();
helper.removeListeners(this._eventListeners);
}
_ensureHTTPActivity(requestId) {
let activity = this._httpActivity.get(requestId);
if (!activity) {
activity = {
_id: requestId,
_lastSentEvent: null,
request: null,
response: null,
complete: null,
failed: null,
};
this._httpActivity.set(requestId, activity);
}
return activity;
}
_reportHTTPAcitivityEvents(activity) {
// State machine - sending network events.
if (!activity._lastSentEvent && activity.request) {
this._session.emitEvent('Network.requestWillBeSent', activity.request);
activity._lastSentEvent = 'requestWillBeSent';
}
if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) {
this._session.emitEvent('Network.responseReceived', activity.response);
activity._lastSentEvent = 'responseReceived';
}
if (activity._lastSentEvent === 'responseReceived' && activity.complete) {
this._session.emitEvent('Network.requestFinished', activity.complete);
activity._lastSentEvent = 'requestFinished';
}
if (activity._lastSentEvent && activity.failed) {
this._session.emitEvent('Network.requestFailed', activity.failed);
activity._lastSentEvent = 'requestFailed';
}
// Clean up if request lifecycle is over.
if (activity._lastSentEvent === 'requestFinished' || activity._lastSentEvent === 'requestFailed')
this._httpActivity.delete(activity._id);
}
async _onRequest(eventDetails, channelKey) {
let pendingRequestCallback;
let pendingRequestPromise = new Promise(x => pendingRequestCallback = x);
this._pendingRequstWillBeSentEvents.add(pendingRequestPromise);
let details = null;
try {
details = await this._contentPage.send('requestDetails', {channelKey});
} catch (e) {
pendingRequestCallback();
this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
return;
}
const frameId = details ? details.frameId : undefined;
const activity = this._ensureHTTPActivity(eventDetails.requestId);
activity.request = {
frameId,
...eventDetails,
};
this._reportHTTPAcitivityEvents(activity);
pendingRequestCallback();
this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
this._session.emitEvent('Network.requestWillBeSent', eventDetails);
}
async _onResponse(eventDetails) {
const activity = this._ensureHTTPActivity(eventDetails.requestId);
activity.response = eventDetails;
this._reportHTTPAcitivityEvents(activity);
this._session.emitEvent('Network.responseReceived', eventDetails);
}
async _onRequestFinished(eventDetails) {
const activity = this._ensureHTTPActivity(eventDetails.requestId);
activity.complete = eventDetails;
this._reportHTTPAcitivityEvents(activity);
this._session.emitEvent('Network.requestFinished', eventDetails);
}
async _onRequestFailed(eventDetails) {
const activity = this._ensureHTTPActivity(eventDetails.requestId);
activity.failed = eventDetails;
this._reportHTTPAcitivityEvents(activity);
this._session.emitEvent('Network.requestFailed', eventDetails);
}
}