# HG changeset patch # User Jarda Snajdr Bug 1134073 - Part 4: show network request cause and stacktrace in netmonitor UI diff --git a/devtools/client/locales/en-US/netmonitor.dtd b/devtools/client/locales/en-US/netmonitor.dtd index 02bf4db..f2f1e9b 100644 --- a/devtools/client/locales/en-US/netmonitor.dtd +++ b/devtools/client/locales/en-US/netmonitor.dtd @@ -33,16 +33,20 @@ + + + diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js index dd642f4..54d2f31 100644 --- a/devtools/client/netmonitor/netmonitor-controller.js +++ b/devtools/client/netmonitor/netmonitor-controller.js @@ -430,16 +430,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); @@ -626,22 +633,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 b77a862..ad25c1a 100644 --- a/devtools/client/netmonitor/netmonitor-view.js +++ b/devtools/client/netmonitor/netmonitor-view.js @@ -57,16 +57,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_SHOW_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; @@ -98,16 +100,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 @@ -420,16 +447,28 @@ 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, + REQUESTS_TOOLTIP_SHOW_DELAY); + 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); @@ -626,25 +665,29 @@ 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 two properties: + * - type: nsContentPolicyType constant + * - uri: URI of the request origin * @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; @@ -874,17 +917,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 } }) }); @@ -1441,29 +1485,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); @@ -1499,52 +1530,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) { @@ -1743,31 +1764,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; }, /** * Updates the information displayed in a network request item view. * * @param object item * The network request item in this container. @@ -1866,16 +1892,25 @@ 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 node = $(".requests-menu-cause", target); + let text = LOAD_CAUSE_STRINGS[value.type] || "unknown"; + node.setAttribute("value", text); + if (value.uri) { + node.setAttribute("tooltiptext", value.uri); + } + break; + } case "contentSize": { let kb = value / 1024; let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS); let node = $(".requests-menu-size", target); let text = L10N.getFormatStr("networkMenu.sizeKB", size); node.setAttribute("value", text); node.setAttribute("tooltiptext", text); break; @@ -2198,58 +2233,81 @@ 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. * * @param nsIDOMNode target * The element node currently being hovered. * @param object tooltip * The current tooltip instance. */ _onHover: function (target, tooltip) { let requestItem = this.getItemForElement(target); - if (!requestItem || !requestItem.attachment.responseContent) { + if (!requestItem) { return null; } let hovered = requestItem.attachment; - let { mimeType, text, encoding } = hovered.responseContent.content; + if (hovered.responseContent && + target.closest(".requests-menu-icon-and-file", requestItem.target)) { + let { mimeType, text, encoding } = hovered.responseContent.content; - if (mimeType && mimeType.includes("image/") && ( - target.classList.contains("requests-menu-icon") || - target.classList.contains("requests-menu-file"))) { - return gNetwork.getString(text).then(string => { - let anchor = $(".requests-menu-icon", requestItem.target); - let src = formDataURI(mimeType, encoding, string); + if (mimeType && mimeType.includes("image/")) { + return gNetwork.getString(text).then(string => { + let anchor = $(".requests-menu-icon", requestItem.target); + let src = formDataURI(mimeType, encoding, string); - tooltip.setImageContent(src, { - maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM + tooltip.setImageContent(src, { + maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM + }); + + return anchor; }); + } + } else if (hovered.cause && + target.closest(".requests-menu-cause", requestItem.target)) { + let stack = hovered.cause.stacktrace; + if (stack && stack.length > 0) { + let doc = tooltip.doc; + let el = doc.createElement("vbox"); - return anchor; - }); + for (let s of stack) { + let { functionName, filename, lineNumber, columnNumber } = s; + functionName = functionName || "anonymous"; + + let sel = doc.createElement("label"); + sel.className = "devtools-monospace"; + sel.setAttribute("value", + `${functionName} @ ${filename}:${lineNumber}:${columnNumber}`); + sel.addEventListener("click", () => { + NetMonitorController.viewSourceInDebugger(filename, lineNumber); + }, false); + + el.appendChild(sel); + } + + tooltip.content = el; + + return true; + } } + return undefined; }, /** * 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 77c65a4..cac7734 100644 --- a/devtools/client/netmonitor/netmonitor.xul +++ b/devtools/client/netmonitor/netmonitor.xul @@ -216,16 +216,27 @@ + + +