Bug 764958 - Show cached network requests in the net monitor. r=jsantell

This commit is contained in:
James Long 2015-04-24 14:57:00 -04:00
parent 0e90ef4932
commit cc0aeb3f6e
11 changed files with 314 additions and 69 deletions

View File

@ -558,8 +558,10 @@ NetworkEventsHandler.prototype = {
return;
}
let { actor, startedDateTime, method, url, isXHR } = aPacket.eventActor;
NetMonitorView.RequestsMenu.addRequest(actor, startedDateTime, method, url, isXHR);
let { actor, startedDateTime, method, url, isXHR, fromCache } = aPacket.eventActor;
NetMonitorView.RequestsMenu.addRequest(
actor, startedDateTime, method, url, isXHR, fromCache
);
window.emit(EVENTS.NETWORK_EVENT, actor);
},

View File

@ -502,8 +502,10 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
* Specifies the request's url.
* @param boolean aIsXHR
* True if this request was initiated via XHR.
* @param boolean aFromCache
* Indicates if the result came from the browser cache
*/
addRequest: function(aId, aStartedDateTime, aMethod, aUrl, aIsXHR) {
addRequest: function(aId, aStartedDateTime, aMethod, aUrl, aIsXHR, aFromCache) {
// Convert the received date/time string to a unix timestamp.
let unixTime = Date.parse(aStartedDateTime);
@ -521,7 +523,8 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
startedMillis: unixTime,
method: aMethod,
url: aUrl,
isXHR: aIsXHR
isXHR: aIsXHR,
fromCache: aFromCache
}
});
@ -1237,13 +1240,20 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
case "status":
requestItem.attachment.status = value;
this.updateMenuView(requestItem, key, value);
this.updateMenuView(requestItem, key, {
status: value,
cached: requestItem.attachment.fromCache
});
break;
case "statusText":
requestItem.attachment.statusText = value;
this.updateMenuView(requestItem, key,
requestItem.attachment.status + " " +
requestItem.attachment.statusText);
let text = (requestItem.attachment.status + " " +
requestItem.attachment.statusText);
if(requestItem.attachment.fromCache) {
text += " (cached)";
}
this.updateMenuView(requestItem, key, text);
break;
case "headersSize":
requestItem.attachment.headersSize = value;
@ -1253,8 +1263,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.updateMenuView(requestItem, key, value);
break;
case "transferredSize":
requestItem.attachment.transferredSize = value;
this.updateMenuView(requestItem, key, value);
if(requestItem.attachment.fromCache) {
requestItem.attachment.transferredSize = 0;
this.updateMenuView(requestItem, key, 'cached');
}
else {
requestItem.attachment.transferredSize = value;
this.updateMenuView(requestItem, key, value);
}
break;
case "mimeType":
requestItem.attachment.mimeType = value;
@ -1278,7 +1294,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
case "eventTimings":
requestItem.attachment.eventTimings = value;
this._createWaterfallView(requestItem, value.timings);
this._createWaterfallView(
requestItem, value.timings, requestItem.attachment.fromCache
);
break;
}
}
@ -1385,7 +1403,8 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
case "remoteAddress":
let domain = $(".requests-menu-domain", target);
let tooltip = domain.getAttribute("value") + " (" + aValue + ")";
let tooltip = (domain.getAttribute("value") +
(aValue ? " (" + aValue + ")" : ""));
domain.setAttribute("tooltiptext", tooltip);
break;
case "securityState": {
@ -1399,9 +1418,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
case "status": {
let node = $(".requests-menu-status", target);
node.setAttribute("code", aValue.cached ? "cached" : aValue.status);
let codeNode = $(".requests-menu-status-code", target);
codeNode.setAttribute("value", aValue);
node.setAttribute("code", aValue);
codeNode.setAttribute("value", aValue.status);
break;
}
case "statusText": {
@ -1419,15 +1438,22 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
}
case "transferredSize": {
let node = $(".requests-menu-transferred", target);
let text;
if (aValue === null) {
text = L10N.getStr("networkMenu.sizeUnavailable");
} else {
}
else if(aValue === "cached") {
text = aValue;
node.classList.add('theme-comment');
}
else {
let kb = aValue / 1024;
let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
text = L10N.getFormatStr("networkMenu.sizeKB", size);
}
let node = $(".requests-menu-transferred", target);
node.setAttribute("value", text);
node.setAttribute("tooltiptext", text);
break;
@ -1472,8 +1498,10 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
* The network request item in this container.
* @param object aTimings
* An object containing timing information.
* @param boolean aFromCache
* Indicates if the result came from the browser cache
*/
_createWaterfallView: function(aItem, aTimings) {
_createWaterfallView: function(aItem, aTimings, aFromCache) {
let { target, attachment } = aItem;
let sections = ["dns", "connect", "send", "wait", "receive"];
// Skipping "blocked" because it doesn't work yet.
@ -1481,6 +1509,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
let timingsNode = $(".requests-menu-timings", target);
let timingsTotal = $(".requests-menu-timings-total", timingsNode);
if(aFromCache) {
timingsTotal.style.display = 'none';
return;
}
// Add a set of boxes representing timing information.
for (let key of sections) {
let width = aTimings[key];
@ -2356,7 +2389,7 @@ NetworkDetailsView.prototype = {
}
if (aData.status) {
$("#headers-summary-status-circle").setAttribute("code", aData.status);
$("#headers-summary-status-circle").setAttribute("code", aData.fromCache ? "cached" : aData.status);
$("#headers-summary-status-value").setAttribute("value", aData.status + " " + aData.statusText);
$("#headers-summary-status").removeAttribute("hidden");
} else {
@ -2727,7 +2760,9 @@ NetworkDetailsView.prototype = {
let tabboxWidth = $("#details-pane").getAttribute("width");
let availableWidth = tabboxWidth / 2; // Other nodes also take some space.
let scale = Math.max(availableWidth / aResponse.totalTime, 0);
let scale = (aResponse.totalTime > 0 ?
Math.max(availableWidth / aResponse.totalTime, 0) :
0);
$("#timings-summary-blocked .requests-menu-timings-box")
.setAttribute("width", blocked * scale);

View File

@ -38,6 +38,7 @@ support-files =
[browser_net_accessibility-01.js]
[browser_net_accessibility-02.js]
[browser_net_autoscroll.js]
[browser_net_cached-status.js]
[browser_net_charts-01.js]
[browser_net_charts-02.js]
[browser_net_charts-03.js]

View File

@ -0,0 +1,105 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if cached requests have the correct status code
*/
let test = Task.async(function*() {
let [tab, debuggee, monitor] = yield initNetMonitor(STATUS_CODES_URL, null, true);
info("Starting test... ");
let { document, L10N, NetMonitorView } = monitor.panelWin;
let { RequestsMenu, NetworkDetails } = NetMonitorView;
RequestsMenu.lazyUpdate = false;
NetworkDetails._params.lazyEmpty = false;
const REQUEST_DATA = [
{
method: 'GET',
uri: STATUS_CODES_SJS + "?sts=ok&cached",
details: {
status: 200,
statusText: 'OK',
type: "plain",
fullMimeType: "text/plain; charset=utf-8"
}
},
{
method: 'GET',
uri: STATUS_CODES_SJS + "?sts=redirect&cached",
details: {
status: 301,
statusText: 'Moved Permanently',
type: "html",
fullMimeType: "text/html; charset=utf-8"
}
},
{
method: 'GET',
uri: 'http://example.com/redirected',
details: {
status: 404,
statusText: 'Not Found',
type: "html",
fullMimeType: "text/html; charset=utf-8"
}
},
{
method: 'GET',
uri: STATUS_CODES_SJS + "?sts=ok&cached",
details: {
status: 200,
statusText: "OK (cached)",
fromCache: true,
type: "plain",
fullMimeType: "text/plain; charset=utf-8"
}
},
{
method: 'GET',
uri: STATUS_CODES_SJS + "?sts=redirect&cached",
details: {
status: 301,
statusText: "Moved Permanently (cached)",
fromCache: true,
type: "html",
fullMimeType: "text/html; charset=utf-8"
}
},
{
method: 'GET',
uri: 'http://example.com/redirected',
details: {
status: 404,
statusText: 'Not Found',
type: "html",
fullMimeType: "text/html; charset=utf-8"
}
}
];
info("Performing requests #1...");
debuggee.performCachedRequests();
yield waitForNetworkEvents(monitor, 3);
info("Performing requests #2...");
debuggee.performCachedRequests();
yield waitForNetworkEvents(monitor, 3);
let index = 0;
for (let request of REQUEST_DATA) {
let item = RequestsMenu.getItemAtIndex(index);
info("Verifying request #" + index);
yield verifyRequestItemTarget(item, request.method, request.uri, request.details);
index++;
}
yield teardown(monitor);
finish();
});

View File

@ -132,7 +132,7 @@ function toggleCache(aTarget, aDisabled) {
return reconfigureTab(aTarget, options).then(() => navigationFinished);
}
function initNetMonitor(aUrl, aWindow) {
function initNetMonitor(aUrl, aWindow, aEnableCache) {
info("Initializing a network monitor pane.");
return Task.spawn(function*() {
@ -144,8 +144,10 @@ function initNetMonitor(aUrl, aWindow) {
yield target.makeRemote();
info("Target remoted.");
yield toggleCache(target, true);
info("Cache disabled when the current and all future toolboxes are open.");
if(!aEnableCache) {
yield toggleCache(target, true);
info("Cache disabled when the current and all future toolboxes are open.");
}
let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
info("Netork monitor pane shown successfully.");
@ -268,7 +270,8 @@ function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
info("Widget index of item: " + widgetIndex);
info("Visible index of item: " + visibleIndex);
let { fuzzyUrl, status, statusText, type, fullMimeType, transferred, size, time } = aData;
let { fuzzyUrl, status, statusText, type, fullMimeType,
transferred, size, time, fromCache } = aData;
let { attachment, target } = aRequestItem
let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
@ -314,7 +317,7 @@ function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
info("Displayed status: " + value);
info("Displayed code: " + codeValue);
info("Tooltip status: " + tooltip);
is(value, status, "The displayed status is correct.");
is(value, fromCache ? "cached" : status, "The displayed status is correct.");
is(codeValue, status, "The displayed status code is correct.");
is(tooltip, status + " " + statusText, "The tooltip status is correct.");
}

View File

@ -40,6 +40,15 @@
});
});
}
function performCachedRequests() {
get("sjs_status-codes-test-server.sjs?sts=ok&cached", function() {
get("sjs_status-codes-test-server.sjs?sts=redirect&cached", function() {
// Done.
});
});
}
</script>
</body>

View File

@ -7,7 +7,8 @@ function handleRequest(request, response) {
response.processAsync();
let params = request.queryString.split("&");
let status = params.filter((s) => s.contains("sts="))[0].split("=")[1];
let status = params.filter(s => s.contains("sts="))[0].split("=")[1];
let cached = params.filter(s => s === 'cached').length !== 0;
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(() => {
@ -29,11 +30,24 @@ function handleRequest(request, response) {
case "500":
response.setStatusLine(request.httpVersion, 501, "Not Implemented");
break;
case "ok":
response.setStatusLine(request.httpVersion, 200, "OK");
break;
case "redirect":
response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
response.setHeader("Location", "http://example.com/redirected");
break;
}
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
if(!cached) {
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
}
else {
response.setHeader("Cache-Control", "no-transform,public,max-age=300,s-maxage=900");
response.setHeader("Expires", "Thu, 01 Dec 2100 20:00:00 GMT");
}
response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
response.write("Hello status code " + status + "!");

View File

@ -26,12 +26,15 @@ add_task(function() {
info("Checking Netmonitor contents.");
let requestsForCss = 0;
let attachments = [];
for (let item of netmonitor._view.RequestsMenu) {
if (item.attachment.url.endsWith("doc_uncached.css")) {
requestsForCss++;
attachments.push(item.attachment);
}
}
is(requestsForCss, 1,
"Got one request for doc_uncached.css after Style Editor was loaded.");
is(attachments.length, 2,
"Got two requests for doc_uncached.css after Style Editor was loaded.");
ok(attachments[1].fromCache,
"Second request was loaded from browser cache");
});

View File

@ -228,6 +228,16 @@ label.requests-menu-status-code {
background-color: rgba(143, 161, 178, 1); /* grey */
}
.theme-dark box.requests-menu-status[code="cached"] {
border: 2px solid rgba(95, 115, 135, 1); /* dark grey */
background-color: transparent;
}
.theme-light box.requests-menu-status[code="cached"] {
border: 2px solid rgba(143, 161, 178, 1); /* grey */
background-color: transparent;
}
.theme-dark box.requests-menu-status[code^="1"] {
background-color: rgba(70, 175, 227, 1); /* light blue */
}

View File

@ -1317,7 +1317,7 @@ WebConsoleActor.prototype =
let packet = {
from: this.actorID,
type: "networkEvent",
eventActor: actor.grip(),
eventActor: actor.grip()
};
this.conn.send(packet);
@ -1694,6 +1694,7 @@ NetworkEventActor.prototype =
url: this._request.url,
method: this._request.method,
isXHR: this._isXHR,
fromCache: this._fromCache,
private: this._private,
};
},
@ -1737,6 +1738,7 @@ NetworkEventActor.prototype =
{
this._startedDateTime = aNetworkEvent.startedDateTime;
this._isXHR = aNetworkEvent.isXHR;
this._fromCache = aNetworkEvent.fromCache;
for (let prop of ['method', 'url', 'httpVersion', 'headersSize']) {
this._request[prop] = aNetworkEvent[prop];
@ -1862,7 +1864,7 @@ NetworkEventActor.prototype =
return {
from: this.actorID,
timings: this._timings,
totalTime: this._totalTime,
totalTime: this._totalTime
};
},
@ -1972,7 +1974,7 @@ NetworkEventActor.prototype =
from: this.actorID,
type: "networkEventUpdate",
updateType: "responseStart",
response: aInfo,
response: aInfo
};
this.conn.send(packet);
@ -2088,7 +2090,7 @@ NetworkEventActor.prototype =
from: this.actorID,
type: "networkEventUpdate",
updateType: "eventTimings",
totalTime: aTotal,
totalTime: aTotal
};
this.conn.send(packet);

View File

@ -353,8 +353,10 @@ NetworkResponseListener.prototype = {
this.receivedData = "";
this.httpActivity.owner.
addResponseContent(response, this.httpActivity.discardResponseBody);
this.httpActivity.owner.addResponseContent(
response,
this.httpActivity.discardResponseBody
);
this._wrappedNotificationCallbacks = null;
this.httpActivity.channel = null;
@ -507,6 +509,8 @@ NetworkMonitor.prototype = {
if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
Services.obs.addObserver(this._httpResponseExaminer,
"http-on-examine-response", false);
Services.obs.addObserver(this._httpResponseExaminer,
"http-on-examine-cached-response", false);
}
},
@ -526,7 +530,9 @@ NetworkMonitor.prototype = {
// NetworkResponseListener is responsible with updating the httpActivity
// object with the data from the new object in openResponses.
if (!this.owner || aTopic != "http-on-examine-response" ||
if (!this.owner ||
(aTopic != "http-on-examine-response" &&
aTopic != "http-on-examine-cached-response") ||
!(aSubject instanceof Ci.nsIHttpChannel)) {
return;
}
@ -577,6 +583,26 @@ NetworkMonitor.prototype = {
httpVersionMin.value;
this.openResponses[response.id] = response;
if(aTopic === "http-on-examine-cached-response") {
// 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: true });
httpActivity.owner.addResponseStart({
httpVersion: response.httpVersion,
remoteAddress: "",
remotePort: "",
status: response.status,
statusText: response.statusText,
headersSize: 0,
}, "", true);
// There also is never any timing events, so we can fire this
// event with zeroed out values.
let timings = this._setupHarTimings(httpActivity, true);
httpActivity.owner.addEventTimings(timings.total, timings.timings);
}
},
/**
@ -692,7 +718,7 @@ NetworkMonitor.prototype = {
return true;
}
}
if (this.topFrame) {
let topFrame = NetworkHelper.getTopFrameForRequest(aChannel);
if (topFrame && topFrame === this.topFrame) {
@ -711,24 +737,9 @@ NetworkMonitor.prototype = {
},
/**
* Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the
* headers are sent to the server. This method creates the |httpActivity|
* object where we store the request and response information that is
* collected through its lifetime.
*
* @private
* @param nsIHttpChannel aChannel
* @param number aTimestamp
* @param string aExtraStringData
* @return void
*/
_onRequestHeader:
function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData)
{
if (!this._matchRequest(aChannel)) {
return;
}
_createNetworkEvent: function(aChannel, { timestamp, extraStringData, fromCache }) {
let win = NetworkHelper.getWindowForRequest(aChannel);
let httpActivity = this.createActivityObject(aChannel);
@ -738,25 +749,32 @@ NetworkMonitor.prototype = {
aChannel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
httpActivity.private = aChannel.isChannelPrivate;
httpActivity.timings.REQUEST_HEADER = {
first: aTimestamp,
last: aTimestamp
};
if(timestamp) {
httpActivity.timings.REQUEST_HEADER = {
first: timestamp,
last: timestamp
};
}
let httpVersionMaj = {};
let httpVersionMin = {};
let event = {};
event.startedDateTime = new Date(Math.round(aTimestamp / 1000)).toISOString();
event.headersSize = aExtraStringData.length;
event.method = aChannel.requestMethod;
event.url = aChannel.URI.spec;
event.private = httpActivity.private;
event.headersSize = 0;
event.startedDateTime = (timestamp ? new Date(Math.round(timestamp / 1000)) : new Date()).toISOString();
event.fromCache = fromCache;
if(extraStringData) {
event.headersSize = extraStringData.length;
}
// Determine if this is an XHR request.
httpActivity.isXHR = event.isXHR =
(aChannel.loadInfo.contentPolicyType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST);
// Determine the HTTP version.
let httpVersionMaj = {};
let httpVersionMin = {};
aChannel.QueryInterface(Ci.nsIHttpChannelInternal);
aChannel.getRequestVersion(httpVersionMaj, httpVersionMin);
@ -785,14 +803,38 @@ NetworkMonitor.prototype = {
cookies = NetworkHelper.parseCookieHeader(cookieHeader);
}
httpActivity.owner = this.owner.onNetworkEvent(event, aChannel, this);
httpActivity.owner = this.owner.onNetworkEvent(event, aChannel);
this._setupResponseListener(httpActivity);
this.openRequests[httpActivity.id] = httpActivity;
httpActivity.owner.addRequestHeaders(headers, aExtraStringData);
httpActivity.owner.addRequestHeaders(headers, extraStringData);
httpActivity.owner.addRequestCookies(cookies);
this.openRequests[httpActivity.id] = httpActivity;
return httpActivity;
},
/**
* Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the
* headers are sent to the server. This method creates the |httpActivity|
* object where we store the request and response information that is
* collected through its lifetime.
*
* @private
* @param nsIHttpChannel aChannel
* @param number aTimestamp
* @param string aExtraStringData
* @return void
*/
_onRequestHeader:
function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData)
{
if (!this._matchRequest(aChannel)) {
return;
}
this._createNetworkEvent(aChannel, { timestamp: aTimestamp,
extraStringData: aExtraStringData });
},
/**
@ -975,17 +1017,36 @@ NetworkMonitor.prototype = {
* Update the HTTP activity object to include timing information as in the HAR
* spec. The HTTP activity object holds the raw timing information in
* |timings| - these are timings stored for each activity notification. The
* HAR timing information is constructed based on these lower level data.
* HAR timing information is constructed based on these lower level
* data.
*
* @param object aHttpActivity
* The HTTP activity object we are working with.
* @param boolean fromCache
* Indicates that the result was returned from the browser cache
* @return object
* This object holds two properties:
* - total - the total time for all of the request and response.
* - timings - the HAR timings object.
*/
_setupHarTimings: function NM__setupHarTimings(aHttpActivity)
_setupHarTimings: function NM__setupHarTimings(aHttpActivity, fromCache)
{
if(fromCache) {
// If it came from the browser cache, we have no timing
// information and these should all be 0
return {
total: 0,
timings: {
blocked: 0,
dns: 0,
connect: 0,
send: 0,
wait: 0,
receive: 0
}
};
}
let timings = aHttpActivity.timings;
let harTimings = {};