Bug 1134073 - Part 2: Show network request cause and stacktrace in netmonitor UI. r=ochameau

--HG--
extra : rebase_source : 6efa4fdec35a7f9ca0acdafba3aba1886bac8602
This commit is contained in:
Jarda Snajdr 2016-06-03 16:26:35 +02:00
parent 8b53520b4b
commit ec1e427b5d
6 changed files with 241 additions and 55 deletions

View File

@ -39,6 +39,10 @@
- in the network table toolbar, above the "domain" column. -->
<!ENTITY netmonitorUI.toolbar.domain "Domain">
<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.cause): This is the label displayed
- in the network table toolbar, above the "cause" column. -->
<!ENTITY netmonitorUI.toolbar.cause "Cause">
<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.type): This is the label displayed
- in the network table toolbar, above the "type" column. -->
<!ENTITY netmonitorUI.toolbar.type "Type">

View File

@ -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);
},

View File

@ -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.

View File

@ -221,6 +221,17 @@
flex="1">
</button>
</hbox>
<hbox id="requests-menu-cause-header-box"
class="requests-menu-header requests-menu-cause"
align="center">
<button id="requests-menu-cause-button"
class="requests-menu-header-button requests-menu-cause"
data-key="cause"
label="&netmonitorUI.toolbar.cause;"
crop="end"
flex="1">
</button>
</hbox>
<hbox id="requests-menu-type-header-box"
class="requests-menu-header requests-menu-type"
align="center">
@ -323,6 +334,10 @@
crop="end"
flex="1"/>
</hbox>
<hbox class="requests-menu-subitem requests-menu-cause" align="center">
<label class="requests-menu-cause-stack" value="JS" hidden="true"/>
<label class="plain requests-menu-cause-label" flex="1" crop="end"/>
</hbox>
<label class="plain requests-menu-subitem requests-menu-type"
crop="end"/>
<label class="plain requests-menu-subitem requests-menu-transferred"

View File

@ -16,6 +16,7 @@ function NetMonitorPanel(iframeWindow, toolbox) {
this._view = this.panelWin.NetMonitorView;
this._controller = this.panelWin.NetMonitorController;
this._controller._target = this.target;
this._controller._toolbox = this._toolbox;
EventEmitter.decorate(this);
}

View File

@ -246,6 +246,25 @@
width: 8vw;
}
.requests-menu-cause {
max-width: 8em;
width: 8vw;
}
.requests-menu-cause-stack {
background-color: var(--theme-body-color-alt);
color: var(--theme-body-background);
font-size: 8px;
font-weight: bold;
line-height: 10px;
border-radius: 3px;
padding: 0 2px;
margin: 0;
margin-inline-end: 3px;
-moz-user-select: none;
cursor: pointer;
}
.requests-menu-transferred {
max-width: 8em;
text-align: center;
@ -676,6 +695,40 @@
color: var(--theme-selection-color);
}
/* Requests menu stacktrace tooltip */
.requests-menu-stack-trace {
max-height: 400px;
width: 586px;
overflow-y: auto;
}
.requests-menu-stack-frame {
color: var(--theme-body-color-alt);
cursor: pointer;
display: flex;
}
.requests-menu-stack-frame:hover {
background-color: var(--theme-selection-background-semitransparent);
}
.requests-menu-stack-frame-function-name {
color: var(--theme-highlight-blue);
cursor: inherit;
flex-grow: 1;
}
.requests-menu-stack-frame-file-name {
cursor: inherit;
margin-inline-end: 0;
}
.requests-menu-stack-frame-line {
color: var(--theme-highlight-orange);
cursor: inherit;
margin-inline-start: 0;
}
/* Performance analysis buttons */
#requests-menu-network-summary-button {