# HG changeset patch # User Jarda Snajdr Bug 1134073 - Part 2: collect information about request cause and stacktrace in network monitor diff --git a/devtools/server/actors/webconsole.js b/devtools/server/actors/webconsole.js index cb4743d..41126c0 100644 --- a/devtools/server/actors/webconsole.js +++ b/devtools/server/actors/webconsole.js @@ -597,26 +597,28 @@ WebConsoleActor.prototype = startedListeners.push(listener); break; case "NetworkActivity": if (!this.networkMonitor) { if (appId || messageManager) { // Start a network monitor in the parent process to listen to // most requests than happen in parent this.networkMonitor = - new NetworkMonitorChild(appId, messageManager, + new NetworkMonitorChild(window, appId, messageManager, this.parentActor.actorID, this); this.networkMonitor.init(); // Spawn also one in the child to listen to service workers this.networkMonitorChild = new NetworkMonitor({ window: window }, this); this.networkMonitorChild.init(); } else { - this.networkMonitor = new NetworkMonitor({ window: window }, this); + this.networkMonitor = new NetworkMonitor({ + window: window, + }, this, true); this.networkMonitor.init(); } } startedListeners.push(listener); break; case "FileActivity": if (this.window instanceof Ci.nsIDOMWindow) { if (!this.consoleProgressListener) { @@ -1823,16 +1825,17 @@ NetworkEventActor.prototype = { return { actor: this.actorID, startedDateTime: this._startedDateTime, timeStamp: Date.parse(this._startedDateTime), url: this._request.url, method: this._request.method, isXHR: this._isXHR, + cause: this._cause, fromCache: this._fromCache, fromServiceWorker: this._fromServiceWorker, private: this._private, }; }, /** * Releases this actor from the pool. @@ -1868,16 +1871,17 @@ NetworkEventActor.prototype = * * @param object aNetworkEvent * The network event associated with this actor. */ init: function NEA_init(aNetworkEvent) { this._startedDateTime = aNetworkEvent.startedDateTime; this._isXHR = aNetworkEvent.isXHR; + this._cause = aNetworkEvent.cause; this._fromCache = aNetworkEvent.fromCache; this._fromServiceWorker = aNetworkEvent.fromServiceWorker; for (let prop of ['method', 'url', 'httpVersion', 'headersSize']) { this._request[prop] = aNetworkEvent[prop]; } this._discardRequestBody = aNetworkEvent.discardRequestBody; diff --git a/devtools/shared/webconsole/client.js b/devtools/shared/webconsole/client.js index 39d9c7f..841d608 100644 --- a/devtools/shared/webconsole/client.js +++ b/devtools/shared/webconsole/client.js @@ -95,16 +95,17 @@ WebConsoleClient.prototype = { discardRequestBody: true, discardResponseBody: true, startedDateTime: actor.startedDateTime, request: { url: actor.url, method: actor.method, }, isXHR: actor.isXHR, + cause: actor.cause, response: {}, timings: {}, // track the list of network event updates updates: [], private: actor.private, fromCache: actor.fromCache, fromServiceWorker: actor.fromServiceWorker }; diff --git a/devtools/shared/webconsole/network-monitor.js b/devtools/shared/webconsole/network-monitor.js index 3a60d78..2708515 100644 --- a/devtools/shared/webconsole/network-monitor.js +++ b/devtools/shared/webconsole/network-monitor.js @@ -1,17 +1,17 @@ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ft= javascript ts=2 et sw=2 tw=80: */ /* 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 {Cc, Ci, Cu, Cr} = require("chrome"); +const {Cc, Ci, Cu, Cr, components} = require("chrome"); const Services = require("Services"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); loader.lazyRequireGetter(this, "NetworkHelper", "devtools/shared/webconsole/network-helper"); loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils"); @@ -32,16 +32,164 @@ const HTTP_MOVED_PERMANENTLY = 301; const HTTP_FOUND = 302; const HTTP_SEE_OTHER = 303; const HTTP_TEMPORARY_REDIRECT = 307; // The maximum number of bytes a NetworkResponseListener can hold: 1 MB const RESPONSE_BODY_LIMIT = 1048576; /** + * Check if a given network request should be logged by a network monitor + * based on the specified filters. + * + * @param nsIHttpChannel channel + * Request to check. + * @param filters + * NetworkMonitor filters to match against. + * @return boolean + * True if the network request should be logged, false otherwise. + */ +function matchRequest(channel, filters) { + // Log everything if no filter is specified + if (!filters.topFrame && !filters.window && !filters.appId) { + return true; + } + + // Ignore requests from chrome or add-on code when we are monitoring + // content. + // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs + // the DevToolsUtils.testing check. We will move to a better way to serve + // its needs in bug 1167188, where this check should be removed. + if (!DevToolsUtils.testing && channel.loadInfo && + channel.loadInfo.loadingDocument === null && + channel.loadInfo.loadingPrincipal === + Services.scriptSecurityManager.getSystemPrincipal()) { + return false; + } + + if (filters.window) { + // Since frames support, this.window may not be the top level content + // frame, so that we can't only compare with win.top. + let win = NetworkHelper.getWindowForRequest(channel); + while (win) { + if (win == filters.window) { + return true; + } + if (win.parent == win) { + break; + } + win = win.parent; + } + } + + if (filters.topFrame) { + let topFrame = NetworkHelper.getTopFrameForRequest(channel); + if (topFrame && topFrame === filters.topFrame) { + return true; + } + } + + if (filters.appId) { + let appId = NetworkHelper.getAppIdForRequest(channel); + if (appId && appId == filters.appId) { + return true; + } + } + + // The following check is necessary because beacon channels don't come + // associated with a load group. Bug 1160837 will hopefully introduce a + // platform fix that will render the following code entirely useless. + if (channel.loadInfo && + channel.loadInfo.externalContentPolicyType == + Ci.nsIContentPolicy.TYPE_BEACON) { + let nonE10sMatch = filters.window && + channel.loadInfo.loadingDocument === filters.window.document; + const loadingPrincipal = channel.loadInfo.loadingPrincipal; + let e10sMatch = filters.topFrame && + filters.topFrame.contentPrincipal && + filters.topFrame.contentPrincipal.equals(loadingPrincipal) && + filters.topFrame.contentPrincipal.URI.spec == channel.referrer.spec; + let b2gMatch = filters.appId && loadingPrincipal.appId === filters.appId; + if (nonE10sMatch || e10sMatch || b2gMatch) { + return true; + } + } + + return false; +} + +function StackTraceCollector(filters) { + this.filters = filters; + this.stacktraces = []; + this._onOpeningRequest = this._onOpeningRequest.bind(this); +} + +StackTraceCollector.prototype = { + init() { + Services.obs.addObserver(this._onOpeningRequest, + "http-on-opening-request", false); + console.log("Added http-on-opening-request observer"); + }, + + destroy() { + Services.obs.removeObserver(this._onOpeningRequest, + "http-on-opening-request"); + }, + + _onOpeningRequest(subject) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + + if (!matchRequest(channel, this.filters)) { + return; + } + + let uri = channel.URI.spec; + let frame = components.stack; + let stacktrace = []; + if (frame && frame.caller) { + frame = frame.caller; + while (frame) { + stacktrace.push({ + filename: frame.filename, + lineNumber: frame.lineNumber, + columnNumber: frame.columnNumber, + functionName: frame.name, + asyncCause: frame.asyncCause, + }); + if (frame.asyncCaller) { + frame = frame.asyncCaller; + } else { + frame = frame.caller; + } + } + } + + console.log("Queued stacktrace:", this.stacktraces.length, uri); + this.stacktraces.push({ uri, stacktrace }); + }, + + getStackTrace(uri) { + let trace = this.stacktraces.shift(); + if (!trace) { + console.error(`Stacktrace for ${uri} not found in queue`); + return null; + } + + if (uri !== trace.uri) { + console.error( + `Stacktrace for ${uri} is not next in queue (got ${trace.uri})`); + return null; + } + + console.log("Retrieved stacktrace:", this.stacktraces.length, uri); + return trace.stacktrace; + } +}; + +/** * The network response listener implements the nsIStreamListener and * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature * to get the response body of the request. * * The code is mostly based on code listings from: * * http://www.softwareishard.com/blog/firebug/ * nsitraceablechannel-intercept-http-traffic/ @@ -473,40 +621,41 @@ NetworkResponseListener.prototype = { * - onNetworkEvent(requestInfo, channel, networkMonitor). * This method is invoked once for every new network request and it is * given the following arguments: the initial network request * information, and the channel. The third argument is the NetworkMonitor * instance. * onNetworkEvent() must return an object which holds several add*() * methods which are used to add further network request/response * information. + * @param boolean collectStackTraces + * Should this network monitor be collecting stack traces? Should + * happen only in non-e10s where there is only one NetworkMonitor and + * there is no proxying between parent and child. In e10s, the + * NetworkMonitorChild takes care of the stack trace collection. */ -function NetworkMonitor(filters, owner) { - if (filters) { - this.window = filters.window; - this.appId = filters.appId; - this.topFrame = filters.topFrame; - } - if (!this.window && !this.appId && !this.topFrame) { - this._logEverything = true; - } +function NetworkMonitor(filters, owner, collectStackTraces) { + this.filters = filters; this.owner = owner; this.openRequests = {}; this.openResponses = {}; this._httpResponseExaminer = DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this); this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this); + + if (collectStackTraces) { + this.stackTraceCollector = new StackTraceCollector(this.filters); + this.stackTraceCollector.init(); + } } + exports.NetworkMonitor = NetworkMonitor; NetworkMonitor.prototype = { - _logEverything: false, - window: null, - appId: null, - topFrame: null, + filters: null, httpTransactionCodes: { 0x5001: "REQUEST_HEADER", 0x5002: "REQUEST_BODY_SENT", 0x5003: "RESPONSE_START", 0x5004: "RESPONSE_HEADER", 0x5005: "RESPONSE_COMPLETE", 0x5006: "TRANSACTION_CLOSE", @@ -547,16 +696,17 @@ NetworkMonitor.prototype = { */ init: function () { this.responsePipeSegmentSize = Services.prefs .getIntPref("network.buffer.cache.size"); this.interceptedChannels = new Set(); if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { gActivityDistributor.addObserver(this); + console.log("Added observer to activity distributor"); Services.obs.addObserver(this._httpResponseExaminer, "http-on-examine-response", false); Services.obs.addObserver(this._httpResponseExaminer, "http-on-examine-cached-response", false); } // In child processes, only watch for service worker requests // everything else only happens in the parent process Services.obs.addObserver(this._serviceWorkerRequest, @@ -597,17 +747,17 @@ NetworkMonitor.prototype = { (topic != "http-on-examine-response" && topic != "http-on-examine-cached-response") || !(subject instanceof Ci.nsIHttpChannel)) { return; } let channel = subject.QueryInterface(Ci.nsIHttpChannel); - if (!this._matchRequest(channel)) { + if (!matchRequest(channel, this.filters)) { return; } let response = { id: gSequenceId(), channel: channel, headers: [], cookies: [], @@ -649,16 +799,19 @@ NetworkMonitor.prototype = { this.openResponses[response.id] = response; if (topic === "http-on-examine-cached-response") { // Service worker requests emits cached-reponse notification on non-e10s, // and we fake one on e10s. let fromServiceWorker = this.interceptedChannels.has(channel); this.interceptedChannels.delete(channel); + console.log("Observer for http-on-cached-response fired:", + channel.URI.spec); + // If this is a cached response, there never was a request event // so we need to construct one here so the frontend gets all the // expected events. let httpActivity = this._createNetworkEvent(channel, { fromCache: !fromServiceWorker, fromServiceWorker: fromServiceWorker }); httpActivity.owner.addResponseStart({ @@ -752,94 +905,16 @@ NetworkMonitor.prototype = { this._onTransactionClose(httpActivity); break; default: break; } }), /** - * Check if a given network request should be logged by this network monitor - * instance based on the current filters. - * - * @private - * @param nsIHttpChannel channel - * Request to check. - * @return boolean - * True if the network request should be logged, false otherwise. - */ - _matchRequest: function (channel) { - if (this._logEverything) { - return true; - } - - // Ignore requests from chrome or add-on code when we are monitoring - // content. - // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs - // the DevToolsUtils.testing check. We will move to a better way to serve - // its needs in bug 1167188, where this check should be removed. - if (!DevToolsUtils.testing && channel.loadInfo && - channel.loadInfo.loadingDocument === null && - channel.loadInfo.loadingPrincipal === - Services.scriptSecurityManager.getSystemPrincipal()) { - return false; - } - - if (this.window) { - // Since frames support, this.window may not be the top level content - // frame, so that we can't only compare with win.top. - let win = NetworkHelper.getWindowForRequest(channel); - while (win) { - if (win == this.window) { - return true; - } - if (win.parent == win) { - break; - } - win = win.parent; - } - } - - if (this.topFrame) { - let topFrame = NetworkHelper.getTopFrameForRequest(channel); - if (topFrame && topFrame === this.topFrame) { - return true; - } - } - - if (this.appId) { - let appId = NetworkHelper.getAppIdForRequest(channel); - if (appId && appId == this.appId) { - return true; - } - } - - // The following check is necessary because beacon channels don't come - // associated with a load group. Bug 1160837 will hopefully introduce a - // platform fix that will render the following code entirely useless. - if (channel.loadInfo && - channel.loadInfo.externalContentPolicyType == - Ci.nsIContentPolicy.TYPE_BEACON) { - let nonE10sMatch = this.window && - channel.loadInfo.loadingDocument === this.window.document; - const loadingPrincipal = channel.loadInfo.loadingPrincipal; - let e10sMatch = this.topFrame && - this.topFrame.contentPrincipal && - this.topFrame.contentPrincipal.equals(loadingPrincipal) && - this.topFrame.contentPrincipal.URI.spec == channel.referrer.spec; - let b2gMatch = this.appId && loadingPrincipal.appId === this.appId; - if (nonE10sMatch || e10sMatch || b2gMatch) { - return true; - } - } - - return false; - }, - - /** * */ _createNetworkEvent: function (channel, { timestamp, extraStringData, fromCache, fromServiceWorker }) { let win = NetworkHelper.getWindowForRequest(channel); let httpActivity = this.createActivityObject(channel); // see _onRequestBodySent() @@ -866,22 +941,33 @@ NetworkMonitor.prototype = { event.fromCache = fromCache; event.fromServiceWorker = fromServiceWorker; httpActivity.fromServiceWorker = fromServiceWorker; if (extraStringData) { event.headersSize = extraStringData.length; } - // Determine if this is an XHR request. + // Determine the cause and if this is an XHR request. + let causeType = channel.loadInfo.externalContentPolicyType; + let loadingPrincipal = channel.loadInfo.loadingPrincipal; + let causeUri = loadingPrincipal ? loadingPrincipal.URI : null; + event.cause = { + type: causeType, + uri: causeUri ? causeUri.spec : null + }; + + if (this.stackTraceCollector) { + event.cause.stacktrace = + this.stackTraceCollector.getStackTrace(event.url); + } + httpActivity.isXHR = event.isXHR = - (channel.loadInfo.externalContentPolicyType === - Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST || - channel.loadInfo.externalContentPolicyType === - Ci.nsIContentPolicy.TYPE_FETCH); + (causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST || + causeType === Ci.nsIContentPolicy.TYPE_FETCH); // Determine the HTTP version. let httpVersionMaj = {}; let httpVersionMin = {}; channel.QueryInterface(Ci.nsIHttpChannelInternal); channel.getRequestVersion(httpVersionMaj, httpVersionMin); event.httpVersion = "HTTP/" + httpVersionMaj.value + "." + @@ -927,20 +1013,23 @@ NetworkMonitor.prototype = { * * @private * @param nsIHttpChannel channel * @param number timestamp * @param string extraStringData * @return void */ _onRequestHeader: function (channel, timestamp, extraStringData) { - if (!this._matchRequest(channel)) { + if (!matchRequest(channel, this.filters)) { return; } + console.log("Observer for activity-request-header fired:", + channel.URI.spec); + this._createNetworkEvent(channel, { timestamp: timestamp, extraStringData: extraStringData }); }, /** * Create the empty HTTP activity object. This object is used for storing all * the request and response information. * @@ -1218,22 +1307,25 @@ NetworkMonitor.prototype = { "http-on-examine-response"); Services.obs.removeObserver(this._httpResponseExaminer, "http-on-examine-cached-response"); } Services.obs.removeObserver(this._serviceWorkerRequest, "service-worker-synthesized-response"); + if (this.stackTraceCollector) { + this.stackTraceCollector.destroy(); + } + this.interceptedChannels.clear(); this.openRequests = {}; this.openResponses = {}; this.owner = null; - this.window = null; - this.topFrame = null; + this.filters = null; }, }; /** * The NetworkMonitorChild is used to proxy all of the network activity of the * child app process from the main process. The child WebConsoleActor creates an * instance of this object. * @@ -1250,24 +1342,31 @@ NetworkMonitor.prototype = { * The web appId of the child process. * @param nsIMessageManager messageManager * The nsIMessageManager to use to communicate with the parent process. * @param string connID * The connection ID to use for send messages to the parent process. * @param object owner * The WebConsoleActor that is listening for the network requests. */ -function NetworkMonitorChild(appId, messageManager, connID, owner) { +function NetworkMonitorChild(window, appId, messageManager, connID, owner) { + this.window = window; this.appId = appId; this.connID = connID; this.owner = owner; this._messageManager = messageManager; this._onNewEvent = this._onNewEvent.bind(this); this._onUpdateEvent = this._onUpdateEvent.bind(this); this._netEvents = new Map(); + + this.stackTraceCollector = new StackTraceCollector({ + window: this.window, + appId: this.appId + }); + this.stackTraceCollector.init(); } exports.NetworkMonitorChild = NetworkMonitorChild; NetworkMonitorChild.prototype = { appId: null, owner: null, _netEvents: null, _saveRequestAndResponseBodies: true, @@ -1297,16 +1396,19 @@ NetworkMonitorChild.prototype = { mm.sendAsyncMessage("debug:netmonitor:" + this.connID, { appId: this.appId, action: "start", }); }, _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) { let {id, event} = msg.data; + + event.cause.stacktrace = this.stackTraceCollector.getStackTrace(event.url); + let actor = this.owner.onNetworkEvent(event); this._netEvents.set(id, Cu.getWeakReference(actor)); }), _onUpdateEvent: DevToolsUtils.makeInfallible(function _onUpdateEvent(msg) { let {id, method, args} = msg.data; let weakActor = this._netEvents.get(id); let actor = weakActor ? weakActor.get() : null; @@ -1319,16 +1421,18 @@ NetworkMonitorChild.prototype = { Cu.reportError("Received debug:netmonitor:updateEvent unsupported " + "method: " + method); return; } actor[method].apply(actor, args); }), destroy: function () { + this.stackTraceCollector.destroy(); + let mm = this._messageManager; try { mm.removeMessageListener("debug:netmonitor:" + this.connID + ":newEvent", this._onNewEvent); mm.removeMessageListener("debug:netmonitor:" + this.connID + ":updateEvent", this._onUpdateEvent); } catch (e) {