# HG changeset patch # User Jarda Snajdr Bug 1134073 - Part 2: Show network request cause and stacktrace in netmonitor UI r=ochameau diff --git a/devtools/client/locales/en-US/netmonitor.dtd b/devtools/client/locales/en-US/netmonitor.dtd index d09eaaf..0d5230c 100644 --- a/devtools/client/locales/en-US/netmonitor.dtd +++ b/devtools/client/locales/en-US/netmonitor.dtd @@ -34,16 +34,20 @@ + + + diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js index 88ba1de..68a6e18 100644 --- a/devtools/client/netmonitor/netmonitor-controller.js +++ b/devtools/client/netmonitor/netmonitor-controller.js @@ -428,16 +428,23 @@ var NetMonitorController = { /** * Getter that tells if the server can do network performance statistics. * @type boolean */ 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); } }; /** * Functions handling target-related lifetime events. */ function TargetEventsHandler() { this._onTabNavigated = this._onTabNavigated.bind(this); @@ -624,22 +631,24 @@ NetworkEventsHandler.prototype = { * @param object networkInfo * The network request information. */ _onNetworkEvent: function (type, networkInfo) { let { actor, 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); }, /** * The "networkEventUpdate" message type handler. * * @param string type diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js index 4ac98b7..cf6ad65 100644 --- a/devtools/client/netmonitor/netmonitor-view.js +++ b/devtools/client/netmonitor/netmonitor-view.js @@ -56,16 +56,18 @@ const HTML_NS = "http://www.w3.org/1999/xhtml"; const EPSILON = 0.001; // 100 KB in bytes const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; // ms 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 const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // ms const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // px const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; @@ -97,16 +99,41 @@ const CONTENT_MIME_TYPE_MAPPINGS = { "/xml": Editor.modes.html, "/atom": Editor.modes.html, "/soap": Editor.modes.html, "/vnd.mpeg.dash.mpd": Editor.modes.html, "/rdf": Editor.modes.css, "/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, lineNumbers: true }; const GENERIC_VARIABLES_VIEW_SETTINGS = { lazyEmpty: true, // ms @@ -426,16 +453,30 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { this.widget = new SideMenuWidget($("#requests-menu-contents")); this._splitter = $("#network-inspector-view-splitter"); this._summary = $("#requests-menu-network-summary-button"); this._summary.setAttribute("label", L10N.getStr("networkMenu.empty")); 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); this.allowFocusOnRightClick = true; this.maintainSelectionVisible = true; this.widget.addEventListener("select", this._onSelect, false); this.widget.addEventListener("swap", this._onSwap, false); @@ -632,25 +673,30 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { * A string representation of when the request was started, which * can be parsed by Date (for example "2012-09-17T19:50:03.699Z"). * @param string method * Specifies the request method (e.g. "GET", "POST", etc.) * @param string url * 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) { return void this._flushRequests(); } this._flushRequestsTask.arm(); return undefined; @@ -880,17 +926,18 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { /** * Create a new custom request form populated with the data from * the currently selected request. */ cloneSelectedRequest: function () { 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], { attachment: Object.create(selected, { isCustom: { value: true } }) }); @@ -1447,29 +1494,16 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { } else { requestTarget.setAttribute("odd", ""); requestTarget.removeAttribute("even"); } } }, /** - * 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. * * @param object item * The network request item to attach the listener to. */ attachSecurityIconClickListener: function ({ target }) { let icon = $(".requests-security-state-icon", target); icon.addEventListener("click", this._onSecurityIconClick); @@ -1505,52 +1539,42 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { // Prevent displaying any updates received after the target closed. if (NetMonitorView._isDestroyed) { return; } 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); this._registerLastRequestEnd(unixTime); // Append a network request item to this container. let requestItem = this.push([menuView, id], { attachment: { startedDeltaMillis: unixTime - this._firstRequestStartedMillis, startedMillis: unixTime, 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; } window.emit(EVENTS.REQUEST_ADDED, id); } if (isScrolledToBottom && this._addQueue.length) { @@ -1749,31 +1773,36 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { /** * Customization function for creating an item's UI. * * @param string method * 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; }, /** * Get a human-readable string from a number of bytes, with the B, KB, MB, or * GB value. Note that the transition between abbreviations is by 1000 rather * than 1024 in order to keep the displayed digits smaller as "1016 KB" is * more awkward than 0.99 MB" @@ -1895,16 +1924,30 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { codeNode.setAttribute("value", value.status); break; } case "statusText": { let node = $(".requests-menu-status", target); 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); let text = this.getFormattedSize(value); node.setAttribute("value", text); node.setAttribute("tooltiptext", text); break; @@ -2225,21 +2268,16 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { } }, /** * The swap listener for this container. * 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); }, /** * The predicate used when deciding whether a popup should be shown * over a request item or not. @@ -2247,35 +2285,79 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { * @param nsIDOMNode target * The element node currently being hovered. * @param object tooltip * The current tooltip instance. * @return {Promise} */ _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", requestItem.target)) { + let { mimeType, text, encoding } = hovered.responseContent.content; + + if (mimeType && mimeType.includes("image/")) { + 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; + } + } else if (hovered.cause && + target.closest(".requests-menu-cause-stack", requestItem.target)) { + let stack = hovered.cause.stacktrace; + if (stack && stack.length > 0) { + let doc = tooltip.doc; + let el = doc.createElement("vbox"); + el.className = "requests-menu-stack-trace"; + + for (let f of stack) { + 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 || ""); + frameEl.appendChild(funcEl); + + let fileEl = doc.createElement("label"); + fileEl.className = "requests-menu-stack-frame-file-name"; + fileEl.setAttribute("value", filename.split(" -> ").pop()); + 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", () => { + NetMonitorController.viewSourceInDebugger(filename, lineNumber); + }, false); + + el.appendChild(frameEl); + } + + tooltip.content = el; + + return true; + } } + return false; }), /** * A handler that opens the security tab in the details view if secure or * broken security indicator is clicked. */ _onSecurityIconClick: function (e) { diff --git a/devtools/client/netmonitor/netmonitor.xul b/devtools/client/netmonitor/netmonitor.xul index e48909d..ba6ec56 100644 --- a/devtools/client/netmonitor/netmonitor.xul +++ b/devtools/client/netmonitor/netmonitor.xul @@ -216,16 +216,27 @@ + + +