From e9eb52b1c2ff886eaec7b08e1776f774afa6980c Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Fri, 3 Jun 2016 03:24:00 +0200 Subject: [PATCH] Bug 1134073 - Part 2: Show network request cause and stacktrace in netmonitor UI r=ochameau MozReview-Commit-ID: GPm9u84SL1d --- devtools/client/locales/en-US/netmonitor.dtd | 4 + .../netmonitor/netmonitor-controller.js | 11 +- devtools/client/netmonitor/netmonitor-view.js | 212 +++++++++++++----- devtools/client/netmonitor/netmonitor.xul | 15 ++ devtools/client/netmonitor/panel.js | 1 + devtools/client/themes/netmonitor.css | 53 +++++ 6 files changed, 241 insertions(+), 55 deletions(-) diff --git a/devtools/client/locales/en-US/netmonitor.dtd b/devtools/client/locales/en-US/netmonitor.dtd index d09eaafb38d3..0d5230cfc529 100644 --- a/devtools/client/locales/en-US/netmonitor.dtd +++ b/devtools/client/locales/en-US/netmonitor.dtd @@ -39,6 +39,10 @@ - in the network table toolbar, above the "domain" column. --> + + + diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js index 88ba1dee716a..68a6e1837a27 100644 --- a/devtools/client/netmonitor/netmonitor-controller.js +++ b/devtools/client/netmonitor/netmonitor-controller.js @@ -433,6 +433,13 @@ var NetMonitorController = { get supportsPerfStats() { return this.tabClient && (this.tabClient.traits.reconfigure || !this._target.isApp); + }, + + /** + * Open a given source in Debugger + */ + viewSourceInDebugger(sourceURL, sourceLine) { + return this._toolbox.viewSourceInDebugger(sourceURL, sourceLine); } }; @@ -629,12 +636,14 @@ NetworkEventsHandler.prototype = { startedDateTime, request: { method, url }, isXHR, + cause, fromCache, fromServiceWorker } = networkInfo; NetMonitorView.RequestsMenu.addRequest( - actor, startedDateTime, method, url, isXHR, fromCache, fromServiceWorker + actor, startedDateTime, method, url, isXHR, cause, fromCache, + fromServiceWorker ); window.emit(EVENTS.NETWORK_EVENT, actor); }, diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js index 4ac98b74fda4..ff6f82ffe1b0 100644 --- a/devtools/client/netmonitor/netmonitor-view.js +++ b/devtools/client/netmonitor/netmonitor-view.js @@ -30,7 +30,9 @@ const {ViewHelpers, Heritage, WidgetMethods, setNamedTimeout} = * Localization convenience methods. */ const NET_STRINGS_URI = "chrome://devtools/locale/netmonitor.properties"; +const WEBCONSOLE_STRINGS_URI = "chrome://devtools/locale/webconsole.properties"; var L10N = new LocalizationHelper(NET_STRINGS_URI); +const WEBCONSOLE_L10N = new LocalizationHelper(WEBCONSOLE_STRINGS_URI); // ms const WDA_DEFAULT_VERIFY_INTERVAL = 50; @@ -61,6 +63,8 @@ const RESIZE_REFRESH_RATE = 50; // ms const REQUESTS_REFRESH_RATE = 50; const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft"; +// tooltip show/hide delay in ms +const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500; // px const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px @@ -102,6 +106,31 @@ const CONTENT_MIME_TYPE_MAPPINGS = { "/rss": Editor.modes.css, "/css": Editor.modes.css }; +const LOAD_CAUSE_STRINGS = { + [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid", + [Ci.nsIContentPolicy.TYPE_OTHER]: "other", + [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script", + [Ci.nsIContentPolicy.TYPE_IMAGE]: "img", + [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet", + [Ci.nsIContentPolicy.TYPE_OBJECT]: "object", + [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document", + [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument", + [Ci.nsIContentPolicy.TYPE_REFRESH]: "refresh", + [Ci.nsIContentPolicy.TYPE_XBL]: "xbl", + [Ci.nsIContentPolicy.TYPE_PING]: "ping", + [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr", + [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "objectSubdoc", + [Ci.nsIContentPolicy.TYPE_DTD]: "dtd", + [Ci.nsIContentPolicy.TYPE_FONT]: "font", + [Ci.nsIContentPolicy.TYPE_MEDIA]: "media", + [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket", + [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp", + [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt", + [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon", + [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch", + [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset", + [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest" +}; const DEFAULT_EDITOR_CONFIG = { mode: Editor.modes.text, readOnly: true, @@ -431,6 +460,20 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { this.userInputTimer = Cc["@mozilla.org/timer;1"] .createInstance(Ci.nsITimer); + // Create a tooltip for the newly appended network request item. + this.tooltip = new Tooltip(document, { + closeOnEvents: [{ + emitter: $("#requests-menu-contents"), + event: "scroll", + useCapture: true + }] + }); + this.tooltip.startTogglingOnHover(this.widget, this._onHover, { + toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY, + interactive: true + }); + this.tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION; + Prefs.filters.forEach(type => this.filterOn(type)); this.sortContents(this._byTiming); @@ -637,15 +680,20 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { * Specifies the request's url. * @param boolean isXHR * True if this request was initiated via XHR. + * @param object cause + * Specifies the request's cause. Has the following properties: + * - type: nsContentPolicyType constant + * - loadingDocumentUri: URI of the request origin + * - stacktrace: JS stacktrace of the request * @param boolean fromCache * Indicates if the result came from the browser cache * @param boolean fromServiceWorker * Indicates if the request has been intercepted by a Service Worker */ - addRequest: function (id, startedDateTime, method, url, isXHR, fromCache, - fromServiceWorker) { - this._addQueue.push([id, startedDateTime, method, url, isXHR, fromCache, - fromServiceWorker]); + addRequest: function (id, startedDateTime, method, url, isXHR, cause, + fromCache, fromServiceWorker) { + this._addQueue.push([id, startedDateTime, method, url, isXHR, cause, + fromCache, fromServiceWorker]); // Lazy updating is disabled in some tests. if (!this.lazyUpdate) { @@ -885,7 +933,8 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { let selected = this.selectedItem.attachment; // Create the element node for the network request item. - let menuView = this._createMenuView(selected.method, selected.url); + let menuView = this._createMenuView(selected.method, selected.url, + selected.cause); // Append a network request item to this container. let newItem = this.push([menuView], { @@ -1451,19 +1500,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { } }, - /** - * Refreshes the toggling anchor for the specified item's tooltip. - * - * @param object item - * The network request item in this container. - */ - refreshTooltip: function (item) { - let tooltip = item.attachment.tooltip; - tooltip.hide(); - tooltip.startTogglingOnHover(item.target, this._onHover); - tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION; - }, - /** * Attaches security icon click listener for the given request menu item. * @@ -1510,13 +1546,13 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { let widget = NetMonitorView.RequestsMenu.widget; let isScrolledToBottom = widget.isScrolledToBottom(); - for (let [id, startedDateTime, method, url, isXHR, fromCache, + for (let [id, startedDateTime, method, url, isXHR, cause, fromCache, fromServiceWorker] of this._addQueue) { // Convert the received date/time string to a unix timestamp. let unixTime = Date.parse(startedDateTime); // Create the element node for the network request item. - let menuView = this._createMenuView(method, url); + let menuView = this._createMenuView(method, url, cause); // Remember the first and last event boundaries. this._registerFirstRequestStart(unixTime); @@ -1530,22 +1566,12 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { method: method, url: url, isXHR: isXHR, + cause: cause, fromCache: fromCache, fromServiceWorker: fromServiceWorker } }); - // Create a tooltip for the newly appended network request item. - requestItem.attachment.tooltip = new Tooltip(document, { - closeOnEvents: [{ - emitter: $("#requests-menu-contents"), - event: "scroll", - useCapture: true - }] - }); - - this.refreshTooltip(requestItem); - if (id == this._preferredItemId) { this.selectedItem = requestItem; } @@ -1754,21 +1780,26 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { * Specifies the request method (e.g. "GET", "POST", etc.) * @param string url * Specifies the request's url. + * @param object cause + * Specifies the request's cause. Has two properties: + * - type: nsContentPolicyType constant + * - uri: URI of the request origin * @return nsIDOMNode * The network request view. */ - _createMenuView: function (method, url) { + _createMenuView: function (method, url, cause) { let template = $("#requests-menu-item-template"); let fragment = document.createDocumentFragment(); - this.updateMenuView(template, "method", method); - this.updateMenuView(template, "url", url); - // Flatten the DOM by removing one redundant box (the template container). for (let node of template.childNodes) { fragment.appendChild(node.cloneNode(true)); } + this.updateMenuView(fragment, "method", method); + this.updateMenuView(fragment, "url", url); + this.updateMenuView(fragment, "cause", cause); + return fragment; }, @@ -1900,6 +1931,20 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { node.setAttribute("tooltiptext", value); break; } + case "cause": { + let labelNode = $(".requests-menu-cause-label", target); + let text = LOAD_CAUSE_STRINGS[value.type] || "unknown"; + labelNode.setAttribute("value", text); + if (value.loadingDocumentUri) { + labelNode.setAttribute("tooltiptext", value.loadingDocumentUri); + } + + let stackNode = $(".requests-menu-cause-stack", target); + if (value.stacktrace && value.stacktrace.length > 0) { + stackNode.removeAttribute("hidden"); + } + break; + } case "contentSize": { let node = $(".requests-menu-size", target); @@ -2230,11 +2275,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { * Called when two items switch places, when the contents are sorted. */ _onSwap: function ({ detail: [firstItem, secondItem] }) { - // Sorting will create new anchor nodes for all the swapped request items - // in this container, so it's necessary to refresh the Tooltip instances. - this.refreshTooltip(firstItem); - this.refreshTooltip(secondItem); - // Reattach click listener to the security icons this.attachSecurityIconClickListener(firstItem); this.attachSecurityIconClickListener(secondItem); @@ -2252,28 +2292,92 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { */ _onHover: Task.async(function* (target, tooltip) { let requestItem = this.getItemForElement(target); - if (!requestItem || !requestItem.attachment.responseContent) { + if (!requestItem) { return false; } let hovered = requestItem.attachment; - let { mimeType, text, encoding } = hovered.responseContent.content; - - if (mimeType && mimeType.includes("image/") && ( - target.classList.contains("requests-menu-icon") || - target.classList.contains("requests-menu-file"))) { - let string = yield gNetwork.getString(text); - let anchor = $(".requests-menu-icon", requestItem.target); - let src = formDataURI(mimeType, encoding, string); - - tooltip.setImageContent(src, { - maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM - }); - return anchor; + if (hovered.responseContent && target.closest(".requests-menu-icon-and-file")) { + return this._setTooltipImageContent(tooltip, requestItem); + } else if (hovered.cause && target.closest(".requests-menu-cause-stack")) { + return this._setTooltipStackTraceContent(tooltip, requestItem); } + return false; }), + _setTooltipImageContent: Task.async(function* (tooltip, requestItem) { + let { mimeType, text, encoding } = requestItem.attachment.responseContent.content; + + if (!mimeType || !mimeType.includes("image/")) { + return false; + } + + let string = yield gNetwork.getString(text); + let anchor = $(".requests-menu-icon", requestItem.target); + let src = formDataURI(mimeType, encoding, string); + + tooltip.setImageContent(src, { + maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM + }); + + return anchor; + }), + + _setTooltipStackTraceContent: Task.async(function* (tooltip, requestItem) { + let {stacktrace} = requestItem.attachment.cause; + + if (!stacktrace || stacktrace.length == 0) { + return false; + } + + let doc = tooltip.doc; + let el = doc.createElement("vbox"); + el.className = "requests-menu-stack-trace"; + + for (let f of stacktrace) { + let { functionName, filename, lineNumber, columnNumber } = f; + + let frameEl = doc.createElement("hbox"); + frameEl.className = "requests-menu-stack-frame devtools-monospace"; + + let funcEl = doc.createElement("label"); + funcEl.className = "requests-menu-stack-frame-function-name"; + funcEl.setAttribute("value", + functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction")); + frameEl.appendChild(funcEl); + + let fileEl = doc.createElement("label"); + fileEl.className = "requests-menu-stack-frame-file-name"; + // Parse a stack frame in format "url -> url" + let sourceUrl = filename.split(" -> ").pop(); + fileEl.setAttribute("value", sourceUrl); + fileEl.setAttribute("tooltiptext", sourceUrl); + fileEl.setAttribute("crop", "start"); + frameEl.appendChild(fileEl); + + let lineEl = doc.createElement("label"); + lineEl.className = "requests-menu-stack-frame-line"; + lineEl.setAttribute("value", `:${lineNumber}:${columnNumber}`); + frameEl.appendChild(lineEl); + + frameEl.addEventListener("click", () => { + // avoid an ugly visual artefact when the view is switched to debugger and the + // tooltip is hidden only after a delay - the tooltip is moved outside the browser + // window. + tooltip.hide(); + NetMonitorController.viewSourceInDebugger(filename, lineNumber); + }, false); + + el.appendChild(frameEl); + } + + tooltip.content = el; + tooltip.panel.setAttribute("wide", ""); + + return true; + }), + /** * A handler that opens the security tab in the details view if secure or * broken security indicator is clicked. diff --git a/devtools/client/netmonitor/netmonitor.xul b/devtools/client/netmonitor/netmonitor.xul index e48909dba832..ba6ec56cdc41 100644 --- a/devtools/client/netmonitor/netmonitor.xul +++ b/devtools/client/netmonitor/netmonitor.xul @@ -221,6 +221,17 @@ flex="1"> + + + @@ -323,6 +334,10 @@ crop="end" flex="1"/> + +