merge m-c into fx-team

This commit is contained in:
Gavin Sharp 2014-01-31 22:42:02 -08:00
commit e19ed01397
24 changed files with 1956 additions and 134 deletions

View File

@ -1141,6 +1141,7 @@ pref("devtools.netmonitor.enabled", true);
// The default Network Monitor UI settings
pref("devtools.netmonitor.panes-network-details-width", 450);
pref("devtools.netmonitor.panes-network-details-height", 450);
pref("devtools.netmonitor.statistics", true);
// Enable the Tilt inspector
pref("devtools.tilt.enabled", true);

View File

@ -57,35 +57,64 @@ const EVENTS = {
// When the response body is displayed in the UI.
RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable",
// When `onTabSelect` is fired and subsequently rendered
// When `onTabSelect` is fired and subsequently rendered.
TAB_UPDATED: "NetMonitor:TabUpdated",
// Fired when Sidebar is finished being populated
// Fired when Sidebar has finished being populated.
SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated",
// Fired when NetworkDetailsView is finished being populated
// Fired when NetworkDetailsView has finished being populated.
NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated",
// Fired when NetworkDetailsView is finished being populated
CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated"
// Fired when CustomRequestView has finished being populated.
CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated",
// Fired when charts have been displayed in the PerformanceStatisticsView.
PLACEHOLDER_CHARTS_DISPLAYED: "NetMonitor:PlaceholderChartsDisplayed",
PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed",
EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed"
};
// Descriptions for what this frontend is currently doing.
const ACTIVITY_TYPE = {
// Standing by and handling requests normally.
NONE: 0,
// Forcing the target to reload with cache enabled or disabled.
RELOAD: {
WITH_CACHE_ENABLED: 1,
WITH_CACHE_DISABLED: 2
},
// Enabling or disabling the cache without triggering a reload.
ENABLE_CACHE: 3,
DISABLE_CACHE: 4
};
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/VariablesView.jsm");
Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const EventEmitter = require("devtools/shared/event-emitter");
const Editor = require("devtools/sourceeditor/editor");
XPCOMUtils.defineLazyModuleGetter(this, "Chart",
"resource:///modules/devtools/Chart.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
"resource://gre/modules/devtools/DevToolsUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "devtools",
"resource://gre/modules/devtools/Loader.jsm");
@ -255,9 +284,81 @@ let NetMonitorController = {
});
},
/**
* Gets the activity currently performed by the frontend.
* @return number
*/
getCurrentActivity: function() {
return this._currentActivity || ACTIVITY_TYPE.NONE;
},
/**
* Triggers a specific "activity" to be performed by the frontend. This can be,
* for example, triggering reloads or enabling/disabling cache.
*
* @param number aType
* The activity type. See the ACTIVITY_TYPE const.
* @return object
* A promise resolved once the activity finishes and the frontend
* is back into "standby" mode.
*/
triggerActivity: function(aType) {
// Puts the frontend into "standby" (when there's no particular activity).
let standBy = () => {
this._currentActivity = ACTIVITY_TYPE.NONE;
};
// Waits for a series of "navigation start" and "navigation stop" events.
let waitForNavigation = () => {
let deferred = promise.defer();
this._target.once("will-navigate", () => {
this._target.once("navigate", () => {
deferred.resolve();
});
});
return deferred.promise;
};
// Reconfigures the tab, optionally triggering a reload.
let reconfigureTab = aOptions => {
let deferred = promise.defer();
this._target.activeTab.reconfigure(aOptions, deferred.resolve);
return deferred.promise;
};
// Reconfigures the tab and waits for the target to finish navigating.
let reconfigureTabAndWaitForNavigation = aOptions => {
aOptions.performReload = true;
let navigationFinished = waitForNavigation();
return reconfigureTab(aOptions).then(() => navigationFinished);
}
if (aType == ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED) {
this._currentActivity = ACTIVITY_TYPE.ENABLE_CACHE;
this._target.once("will-navigate", () => this._currentActivity = aType);
return reconfigureTabAndWaitForNavigation({ cacheEnabled: true }).then(standBy);
}
if (aType == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED) {
this._currentActivity = ACTIVITY_TYPE.DISABLE_CACHE;
this._target.once("will-navigate", () => this._currentActivity = aType);
return reconfigureTabAndWaitForNavigation({ cacheEnabled: false }).then(standBy);
}
if (aType == ACTIVITY_TYPE.ENABLE_CACHE) {
this._currentActivity = aType;
return reconfigureTab({ cacheEnabled: true, performReload: false }).then(standBy);
}
if (aType == ACTIVITY_TYPE.DISABLE_CACHE) {
this._currentActivity = aType;
return reconfigureTab({ cacheEnabled: false, performReload: false }).then(standBy);
}
this._currentActivity = ACTIVITY_TYPE.NONE;
return promise.reject(new Error("Invalid activity type"));
},
_startup: null,
_shutdown: null,
_connection: null,
_currentActivity: null,
client: null,
tabClient: null,
webConsoleClient: null
@ -314,6 +415,11 @@ TargetEventsHandler.prototype = {
NetMonitorView.Sidebar.reset();
NetMonitorView.NetworkDetails.reset();
// Switch to the default network traffic inspector view.
if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) {
NetMonitorView.showNetworkInspectorView();
}
window.emit(EVENTS.TARGET_WILL_NAVIGATE);
break;
}
@ -383,7 +489,6 @@ NetworkEventsHandler.prototype = {
_onNetworkEvent: function(aType, aPacket) {
let { actor, startedDateTime, method, url, isXHR } = aPacket.eventActor;
NetMonitorView.RequestsMenu.addRequest(actor, startedDateTime, method, url, isXHR);
window.emit(EVENTS.NETWORK_EVENT);
},
@ -585,7 +690,8 @@ let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
*/
let Prefs = new ViewHelpers.Prefs("devtools.netmonitor", {
networkDetailsWidth: ["Int", "panes-network-details-width"],
networkDetailsHeight: ["Int", "panes-network-details-height"]
networkDetailsHeight: ["Int", "panes-network-details-height"],
statistics: ["Bool", "statistics"]
});
/**
@ -616,6 +722,39 @@ Object.defineProperties(window, {
}
});
/**
* Makes sure certain properties are available on all objects in a data store.
*
* @param array aDataStore
* A list of objects for which to check the availability of properties.
* @param array aMandatoryFields
* A list of strings representing properties of objects in aDataStore.
* @return object
* A promise resolved when all objects in aDataStore contain the
* properties defined in aMandatoryFields.
*/
function whenDataAvailable(aDataStore, aMandatoryFields) {
let deferred = promise.defer();
let interval = setInterval(() => {
if (aDataStore.every(item => aMandatoryFields.every(field => field in item))) {
clearInterval(interval);
clearTimeout(timer);
deferred.resolve();
}
}, WDA_DEFAULT_VERIFY_INTERVAL);
let timer = setTimeout(() => {
clearInterval(interval);
deferred.reject(new Error("Timed out while waiting for data"));
}, WDA_DEFAULT_GIVE_UP_TIMEOUT);
return deferred.promise;
};
const WDA_DEFAULT_VERIFY_INTERVAL = 50; // ms
const WDA_DEFAULT_GIVE_UP_TIMEOUT = 2000; // ms
/**
* Helper method for debugging.
* @param string

View File

@ -21,6 +21,7 @@ const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
const DEFAULT_HTTP_VERSION = "HTTP/1.1";
const REQUEST_TIME_DECIMALS = 2;
const HEADERS_SIZE_DECIMALS = 3;
const CONTENT_SIZE_DECIMALS = 2;
const CONTENT_MIME_TYPE_ABBREVIATIONS = {
@ -57,6 +58,7 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = {
eval: () => {},
switch: () => {}
};
const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px
/**
* Object defining the network monitor view components.
@ -102,6 +104,14 @@ let NetMonitorView = {
this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
this.toggleDetailsPane({ visible: false });
// Disable the performance statistics mode.
if (!Prefs.statistics) {
$("#request-menu-context-perf").hidden = true;
$("#notice-perf-message").hidden = true;
$("#requests-menu-network-summary-button").hidden = true;
$("#requests-menu-network-summary-label").hidden = true;
}
},
/**
@ -121,8 +131,9 @@ let NetMonitorView = {
* Gets the visibility state of the network details pane.
* @return boolean
*/
get detailsPaneHidden()
this._detailsPane.hasAttribute("pane-collapsed"),
get detailsPaneHidden() {
return this._detailsPane.hasAttribute("pane-collapsed");
},
/**
* Sets the network details pane hidden or visible.
@ -157,6 +168,66 @@ let NetMonitorView = {
}
},
/**
* Gets the current mode for this tool.
* @return string (e.g, "network-inspector-view" or "network-statistics-view")
*/
get currentFrontendMode() {
return this._body.selectedPanel.id;
},
/**
* Toggles between the frontend view modes ("Inspector" vs. "Statistics").
*/
toggleFrontendMode: function() {
if (this.currentFrontendMode != "network-inspector-view") {
this.showNetworkInspectorView();
} else {
this.showNetworkStatisticsView();
}
},
/**
* Switches to the "Inspector" frontend view mode.
*/
showNetworkInspectorView: function() {
this._body.selectedPanel = $("#network-inspector-view");
this.RequestsMenu._flushWaterfallViews(true);
},
/**
* Switches to the "Statistics" frontend view mode.
*/
showNetworkStatisticsView: function() {
this._body.selectedPanel = $("#network-statistics-view");
let controller = NetMonitorController;
let requestsView = this.RequestsMenu;
let statisticsView = this.PerformanceStatistics;
Task.spawn(function() {
statisticsView.displayPlaceholderCharts();
yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
try {
// • The response headers and status code are required for determining
// whether a response is "fresh" (cacheable).
// • The response content size and request total time are necessary for
// populating the statistics view.
// • The response mime type is used for categorization.
yield whenDataAvailable(requestsView.attachments, [
"responseHeaders", "status", "contentSize", "mimeType", "totalTime"
]);
} catch (ex) {
// Timed out while waiting for data. Continue with what we have.
DevToolsUtils.reportException("showNetworkStatisticsView", ex);
}
statisticsView.createPrimedCacheChart(requestsView.items);
statisticsView.createEmptyCacheChart(requestsView.items);
});
},
/**
* Lazily initializes and returns a promise for a Editor instance.
*
@ -263,8 +334,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
dumpn("Initializing the RequestsMenuView");
this.widget = new SideMenuWidget($("#requests-menu-contents"));
this._splitter = $('#splitter');
this._summary = $("#request-menu-network-summary");
this._splitter = $("#network-inspector-view-splitter");
this._summary = $("#requests-menu-network-summary-label");
this._summary.setAttribute("value", L10N.getStr("networkMenu.empty"));
this.allowFocusOnRightClick = true;
this.widget.maintainSelectionVisible = false;
@ -276,11 +348,12 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
this.requestsMenuFilterEvent = getKeyWithEvent(this.filterOn.bind(this));
this.clearEvent = this.clear.bind(this);
this.reqeustsMenuClearEvent = this.clear.bind(this);
this._onContextShowing = this._onContextShowing.bind(this);
this._onContextNewTabCommand = this.openRequestInTab.bind(this);
this._onContextCopyUrlCommand = this.copyUrl.bind(this);
this._onContextResendCommand = this.cloneSelectedRequest.bind(this);
this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
@ -288,11 +361,16 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
$("#toolbar-labels").addEventListener("click", this.requestsMenuSortEvent, false);
$("#requests-menu-footer").addEventListener("click", this.requestsMenuFilterEvent, false);
$("#requests-menu-clear-button").addEventListener("click", this.clearEvent, false);
$("#requests-menu-clear-button").addEventListener("click", this.reqeustsMenuClearEvent, false);
$("#network-request-popup").addEventListener("popupshowing", this._onContextShowing, false);
$("#request-menu-context-newtab").addEventListener("command", this._onContextNewTabCommand, false);
$("#request-menu-context-copy-url").addEventListener("command", this._onContextCopyUrlCommand, false);
$("#request-menu-context-resend").addEventListener("command", this._onContextResendCommand, false);
$("#request-menu-context-perf").addEventListener("command", this._onContextPerfCommand, false);
$("#requests-menu-perf-notice-button").addEventListener("command", this._onContextPerfCommand, false);
$("#requests-menu-network-summary-button").addEventListener("command", this._onContextPerfCommand, false);
$("#requests-menu-network-summary-label").addEventListener("click", this._onContextPerfCommand, false);
$("#custom-request-send-button").addEventListener("click", this.sendCustomRequestEvent, false);
$("#custom-request-close-button").addEventListener("click", this.closeCustomRequestEvent, false);
@ -311,11 +389,16 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
$("#toolbar-labels").removeEventListener("click", this.requestsMenuSortEvent, false);
$("#requests-menu-footer").removeEventListener("click", this.requestsMenuFilterEvent, false);
$("#requests-menu-clear-button").removeEventListener("click", this.clearEvent, false);
$("#requests-menu-clear-button").removeEventListener("click", this.reqeustsMenuClearEvent, false);
$("#network-request-popup").removeEventListener("popupshowing", this._onContextShowing, false);
$("#request-menu-context-newtab").removeEventListener("command", this._onContextNewTabCommand, false);
$("#request-menu-context-copy-url").removeEventListener("command", this._onContextCopyUrlCommand, false);
$("#request-menu-context-resend").removeEventListener("command", this._onContextResendCommand, false);
$("#request-menu-context-perf").removeEventListener("command", this._onContextPerfCommand, false);
$("#requests-menu-perf-notice-button").removeEventListener("command", this._onContextPerfCommand, false);
$("#requests-menu-network-summary-button").removeEventListener("command", this._onContextPerfCommand, false);
$("#requests-menu-network-summary-label").removeEventListener("click", this._onContextPerfCommand, false);
$("#custom-request-send-button").removeEventListener("click", this.sendCustomRequestEvent, false);
$("#custom-request-close-button").removeEventListener("click", this.closeCustomRequestEvent, false);
@ -327,6 +410,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
*/
reset: function() {
this.empty();
this.filterOn("all");
this._firstRequestStartedMillis = -1;
this._lastRequestEndedMillis = -1;
},
@ -394,6 +478,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
// Create the element node for the network request item.
let menuView = this._createMenuView(selected.method, selected.url);
// Append a network request item to this container.
let newItem = this.push([menuView], {
attachment: Object.create(selected, {
isCustom: { value: true }
@ -457,7 +542,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
*
* @param string aType
* Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
* or "flash".
* "flash" or "other".
*/
filterOn: function(aType = "all") {
let target = $("#requests-menu-filter-" + aType + "-button");
@ -477,28 +562,31 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.filterContents(() => true);
break;
case "html":
this.filterContents(this._onHtml);
this.filterContents(e => this.isHtml(e));
break;
case "css":
this.filterContents(this._onCss);
this.filterContents(e => this.isCss(e));
break;
case "js":
this.filterContents(this._onJs);
this.filterContents(e => this.isJs(e));
break;
case "xhr":
this.filterContents(this._onXhr);
this.filterContents(e => this.isXHR(e));
break;
case "fonts":
this.filterContents(this._onFonts);
this.filterContents(e => this.isFont(e));
break;
case "images":
this.filterContents(this._onImages);
this.filterContents(e => this.isImage(e));
break;
case "media":
this.filterContents(this._onMedia);
this.filterContents(e => this.isMedia(e));
break;
case "flash":
this.filterContents(this._onFlash);
this.filterContents(e => this.isFlash(e));
break;
case "other":
this.filterContents(e => this.isOther(e));
break;
}
@ -611,22 +699,22 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
* @return boolean
* True if the item should be visible, false otherwise.
*/
_onHtml: function({ attachment: { mimeType } })
isHtml: function({ attachment: { mimeType } })
mimeType && mimeType.contains("/html"),
_onCss: function({ attachment: { mimeType } })
isCss: function({ attachment: { mimeType } })
mimeType && mimeType.contains("/css"),
_onJs: function({ attachment: { mimeType } })
isJs: function({ attachment: { mimeType } })
mimeType && (
mimeType.contains("/ecmascript") ||
mimeType.contains("/javascript") ||
mimeType.contains("/x-javascript")),
_onXhr: function({ attachment: { isXHR } })
isXHR: function({ attachment: { isXHR } })
isXHR,
_onFonts: function({ attachment: { url, mimeType } }) // Fonts are a mess.
isFont: function({ attachment: { url, mimeType } }) // Fonts are a mess.
(mimeType && (
mimeType.contains("font/") ||
mimeType.contains("/font"))) ||
@ -635,22 +723,26 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
url.contains(".otf") ||
url.contains(".woff"),
_onImages: function({ attachment: { mimeType } })
isImage: function({ attachment: { mimeType } })
mimeType && mimeType.contains("image/"),
_onMedia: function({ attachment: { mimeType } }) // Not including images.
isMedia: function({ attachment: { mimeType } }) // Not including images.
mimeType && (
mimeType.contains("audio/") ||
mimeType.contains("video/") ||
mimeType.contains("model/")),
_onFlash: function({ attachment: { url, mimeType } }) // Flash is a mess.
isFlash: function({ attachment: { url, mimeType } }) // Flash is a mess.
(mimeType && (
mimeType.contains("/x-flv") ||
mimeType.contains("/x-shockwave-flash"))) ||
url.contains(".swf") ||
url.contains(".flv"),
isOther: function(e)
!this.isHtml(e) && !this.isCss(e) && !this.isJs(e) && !this.isXHR(e) &&
!this.isFont(e) && !this.isImage(e) && !this.isMedia(e) && !this.isFlash(e),
/**
* Predicates used when sorting items.
*
@ -724,8 +816,8 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
let str = PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary"));
this._summary.setAttribute("value", str
.replace("#1", visibleRequestsCount)
.replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2))
.replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2))
.replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, CONTENT_SIZE_DECIMALS))
.replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, REQUEST_TIME_DECIMALS))
);
},
@ -838,6 +930,12 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
case "responseContent":
requestItem.attachment.responseContent = value;
// If there's no mime type available when the response content
// is received, assume text/plain as a fallback.
if (!requestItem.attachment.mimeType) {
requestItem.attachment.mimeType = "text/plain";
this.updateMenuView(requestItem, "mimeType", "text/plain");
}
break;
case "totalTime":
requestItem.attachment.totalTime = value;
@ -1021,6 +1119,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
startCapNode.hidden = false;
endCapNode.hidden = false;
// Don't paint things while the waterfall view isn't even visible.
if (NetMonitorView.currentFrontendMode != "network-inspector-view") {
return;
}
// Rescale all the waterfalls so that everything is visible at once.
this._flushWaterfallViews();
},
@ -1134,7 +1237,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
if (divisionScale == "millisecond") {
normalizedTime |= 0;
} else {
normalizedTime = L10N.numberWithDecimals(normalizedTime, 2);
normalizedTime = L10N.numberWithDecimals(normalizedTime, REQUEST_TIME_DECIMALS);
}
let node = document.createElement("label");
@ -1263,6 +1366,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
* The resize listener for this container's window.
*/
_onResize: function(e) {
// Don't paint things while the waterfall view isn't even visible.
if (NetMonitorView.currentFrontendMode != "network-inspector-view") {
return;
}
// Allow requests to settle down first.
setNamedTimeout(
"resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
@ -1453,7 +1561,7 @@ SidebarView.prototype = {
return view.populate(aData).then(() => {
$("#details-pane").selectedIndex = isCustom ? 0 : 1
window.emit(EVENTS.SIDEBAR_POPULATED)
window.emit(EVENTS.SIDEBAR_POPULATED);
});
},
@ -1480,7 +1588,6 @@ CustomRequestView.prototype = {
dumpn("Initializing the CustomRequestView");
this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this));
$("#custom-pane").addEventListener("input", this.updateCustomRequestEvent, false);
},
@ -1555,18 +1662,12 @@ CustomRequestView.prototype = {
break;
case 'body':
value = $("#custom-postdata-value").value;
selectedItem.attachment.requestPostData = {
postData: {
text: value
}
};
selectedItem.attachment.requestPostData = { postData: { text: value } };
break;
case 'headers':
let headersText = $("#custom-headers-value").value;
value = parseHeaderText(headersText);
selectedItem.attachment.requestHeaders = {
headers: value
};
selectedItem.attachment.requestHeaders = { headers: value };
break;
}
@ -2172,6 +2273,170 @@ NetworkDetailsView.prototype = {
_responseCookies: ""
};
/**
* Functions handling the performance statistics view.
*/
function PerformanceStatisticsView() {
}
PerformanceStatisticsView.prototype = {
/**
* Initializes and displays empty charts in this container.
*/
displayPlaceholderCharts: function() {
this._createChart({
id: "#primed-cache-chart",
title: "charts.cacheEnabled"
});
this._createChart({
id: "#empty-cache-chart",
title: "charts.cacheDisabled"
});
window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
},
/**
* Populates and displays the primed cache chart in this container.
*
* @param array aItems
* @see this._sanitizeChartDataSource
*/
createPrimedCacheChart: function(aItems) {
this._createChart({
id: "#primed-cache-chart",
title: "charts.cacheEnabled",
data: this._sanitizeChartDataSource(aItems),
sorted: true,
totals: {
size: L10N.getStr("charts.totalSize"),
time: L10N.getStr("charts.totalTime"),
cached: L10N.getStr("charts.totalCached"),
count: L10N.getStr("charts.totalCount")
}
});
window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED);
},
/**
* Populates and displays the empty cache chart in this container.
*
* @param array aItems
* @see this._sanitizeChartDataSource
*/
createEmptyCacheChart: function(aItems) {
this._createChart({
id: "#empty-cache-chart",
title: "charts.cacheDisabled",
data: this._sanitizeChartDataSource(aItems, true),
sorted: true,
totals: {
size: L10N.getStr("charts.totalSize"),
time: L10N.getStr("charts.totalTime"),
cached: L10N.getStr("charts.totalCached"),
count: L10N.getStr("charts.totalCount")
}
});
window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED);
},
/**
* Adds a specific chart to this container.
*
* @param object
* An object containing all or some the following properties:
* - id: either "#primed-cache-chart" or "#empty-cache-chart"
* - title/data/sorted/totals: @see Chart.jsm for details
*/
_createChart: function({ id, title, data, sorted, totals }) {
let container = $(id);
// Nuke all existing charts of the specified type.
while (container.hasChildNodes()) {
container.firstChild.remove();
}
// Create a new chart.
let chart = Chart.PieTable(document, {
diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
title: L10N.getStr(title),
data: data,
sorted: sorted,
totals: totals
});
chart.on("click", (_, item) => {
NetMonitorView.RequestsMenu.filterOn(item.label);
NetMonitorView.showNetworkInspectorView();
});
container.appendChild(chart.node);
},
/**
* Sanitizes the data source used for creating charts, to follow the
* data format spec defined in Chart.jsm.
*
* @param array aItems
* A collection of request items used as the data source for the chart.
* @param boolean aEmptyCache
* True if the cache is considered enabled, false for disabled.
*/
_sanitizeChartDataSource: function(aItems, aEmptyCache) {
let data = [
"html", "css", "js", "xhr", "fonts", "images", "media", "flash", "other"
].map(e => ({
cached: 0,
count: 0,
label: e,
size: 0,
time: 0
}));
for (let requestItem of aItems) {
let details = requestItem.attachment;
let type;
if (RequestsMenuView.prototype.isHtml(requestItem)) {
type = 0; // "html"
} else if (RequestsMenuView.prototype.isCss(requestItem)) {
type = 1; // "css"
} else if (RequestsMenuView.prototype.isJs(requestItem)) {
type = 2; // "js"
} else if (RequestsMenuView.prototype.isFont(requestItem)) {
type = 4; // "fonts"
} else if (RequestsMenuView.prototype.isImage(requestItem)) {
type = 5; // "images"
} else if (RequestsMenuView.prototype.isMedia(requestItem)) {
type = 6; // "media"
} else if (RequestsMenuView.prototype.isFlash(requestItem)) {
type = 7; // "flash"
} else if (RequestsMenuView.prototype.isXHR(requestItem)) {
// Verify XHR last, to categorize other mime types in their own blobs.
type = 3; // "xhr"
} else {
type = 8; // "other"
}
if (aEmptyCache || !responseIsFresh(details)) {
data[type].time += details.totalTime || 0;
data[type].size += details.contentSize || 0;
} else {
data[type].cached++;
}
data[type].count++;
}
for (let chartItem of data) {
let size = L10N.numberWithDecimals(chartItem.size / 1024, CONTENT_SIZE_DECIMALS);
let time = L10N.numberWithDecimals(chartItem.time / 1000, REQUEST_TIME_DECIMALS);
chartItem.size = L10N.getFormatStr("charts.sizeKB", size);
chartItem.time = L10N.getFormatStr("charts.totalMS", time);
}
return data.filter(e => e.count > 0);
},
};
/**
* DOM query helper.
*/
@ -2194,8 +2459,8 @@ nsIURL.store = new Map();
/**
* Parse a url's query string into its components
*
* @param string aQueryString
* The query part of a url
* @param string aQueryString
* The query part of a url
* @return array
* Array of query params {name, value}
*/
@ -2216,8 +2481,8 @@ function parseQueryString(aQueryString) {
/**
* Parse text representation of HTTP headers.
*
* @param string aText
* Text of headers
* @param string aText
* Text of headers
* @return array
* Array of headers info {name, value}
*/
@ -2228,8 +2493,8 @@ function parseHeaderText(aText) {
/**
* Parse readable text list of a query string.
*
* @param string aText
* Text of query string represetation
* @param string aText
* Text of query string represetation
* @return array
* Array of query params {name, value}
*/
@ -2241,8 +2506,8 @@ function parseQueryText(aText) {
* Parse a text representation of a name:value list with
* the given name:value divider character.
*
* @param string aText
* Text of list
* @param string aText
* Text of list
* @return array
* Array of headers info {name, value}
*/
@ -2295,6 +2560,44 @@ function writeQueryString(aParams) {
return [(name + "=" + value) for ({name, value} of aParams)].join("&");
}
/**
* Checks if the "Expiration Calculations" defined in section 13.2.4 of the
* "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
*
* @param object
* An object containing the { responseHeaders, status } properties.
* @return boolean
* True if the response is fresh and loaded from cache.
*/
function responseIsFresh({ responseHeaders, status }) {
// Check for a "304 Not Modified" status and response headers availability.
if (status != 304 || !responseHeaders) {
return false;
}
let list = responseHeaders.headers;
let cacheControl = list.filter(e => e.name.toLowerCase() == "cache-control")[0];
let expires = list.filter(e => e.name.toLowerCase() == "expires")[0];
// Check the "Cache-Control" header for a maximum age value.
if (cacheControl) {
let maxAgeMatch =
cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
cacheControl.value.match(/max-age\s*=\s*(\d+)/);
if (maxAgeMatch && maxAgeMatch.pop() > 0) {
return true;
}
}
// Check the "Expires" header for a valid date.
if (expires && Date.parse(expires.value)) {
return true;
}
return false;
}
/**
* Helper method to get a wrapped function which can be bound to as an event listener directly and is executed only when data-key is present in event.target.
*
@ -2320,3 +2623,4 @@ NetMonitorView.RequestsMenu = new RequestsMenuView();
NetMonitorView.Sidebar = new SidebarView();
NetMonitorView.CustomRequest = new CustomRequestView();
NetMonitorView.NetworkDetails = new NetworkDetailsView();
NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();

View File

@ -12,11 +12,11 @@
visibility: hidden;
}
#response-content-image-box {
#custom-pane {
overflow: auto;
}
#custom-pane {
#response-content-image-box {
overflow: auto;
}
@ -24,6 +24,10 @@
display: none; /* This doesn't work yet. */
}
#network-statistics-charts {
overflow: auto;
}
/* Responsive sidebar */
@media (max-width: 700px) {
#toolbar-spacer,
@ -35,18 +39,14 @@
}
}
@media (min-width: 701px) and (max-width: 1024px) {
#body:not([pane-collapsed]) .requests-menu-footer-button,
@media (min-width: 701px) and (max-width: 1280px) {
#body:not([pane-collapsed]) .requests-menu-filter-button,
#body:not([pane-collapsed]) .requests-menu-footer-spacer {
display: none;
}
}
@media (min-width: 701px) {
#requests-menu-spacer-start {
display: none;
}
#network-table[waterfall-overflows] .requests-menu-waterfall {
display: none;
}

View File

@ -31,12 +31,16 @@
<menuitem id="request-menu-context-resend"
label="&netmonitorUI.summary.editAndResend;"
accesskey="&netmonitorUI.summary.editAndResend.accesskey;"/>
<menuseparator/>
<menuitem id="request-menu-context-perf"
label="&netmonitorUI.context.perfTools;"
accesskey="&netmonitorUI.context.perfTools.accesskey;"/>
</menupopup>
</popupset>
<box id="body"
class="devtools-responsive-container theme-sidebar"
flex="1">
<deck id="body" class="theme-sidebar" flex="1">
<box id="network-inspector-view" class="devtools-responsive-container">
<vbox id="network-table" flex="1">
<toolbar id="requests-menu-toolbar"
class="devtools-toolbar"
@ -118,9 +122,20 @@
disabled="true"
tabindex="0"/>
</toolbar>
<label id="requests-menu-empty-notice"
class="side-menu-widget-empty-text"
value="&netmonitorUI.emptyNotice2;"/>
<vbox id="requests-menu-empty-notice"
class="side-menu-widget-empty-text">
<hbox id="notice-perf-message" align="center">
<label value="&netmonitorUI.perfNotice1;"/>
<button id="requests-menu-perf-notice-button"
class="devtools-toolbarbutton"/>
<label value="&netmonitorUI.perfNotice2;"/>
</hbox>
<hbox id="notice-reload-message" align="center">
<label value="&netmonitorUI.emptyNotice3;"/>
</hbox>
</vbox>
<vbox id="requests-menu-contents" flex="1" context="network-request-popup">
<hbox id="requests-menu-item-template" hidden="true">
<hbox class="requests-menu-subitem requests-menu-status-and-method"
@ -151,70 +166,70 @@
</hbox>
</vbox>
<hbox id="requests-menu-footer">
<spacer id="requests-menu-spacer-start"
class="requests-menu-footer-spacer"
flex="100"/>
<button id="requests-menu-filter-all-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
checked="true"
data-key="all"
label="&netmonitorUI.footer.filterAll;">
</button>
<button id="requests-menu-filter-html-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
data-key="html"
label="&netmonitorUI.footer.filterHTML;">
</button>
<button id="requests-menu-filter-css-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
data-key="css"
label="&netmonitorUI.footer.filterCSS;">
</button>
<button id="requests-menu-filter-js-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
data-key="js"
label="&netmonitorUI.footer.filterJS;">
</button>
<button id="requests-menu-filter-xhr-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
data-key="xhr"
label="&netmonitorUI.footer.filterXHR;">
</button>
<button id="requests-menu-filter-fonts-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
data-key="fonts"
label="&netmonitorUI.footer.filterFonts;">
</button>
<button id="requests-menu-filter-images-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
data-key="images"
label="&netmonitorUI.footer.filterImages;">
</button>
<button id="requests-menu-filter-media-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
data-key="media"
label="&netmonitorUI.footer.filterMedia;">
</button>
<button id="requests-menu-filter-flash-button"
class="requests-menu-footer-button"
class="requests-menu-filter-button requests-menu-footer-button"
data-key="flash"
label="&netmonitorUI.footer.filterFlash;">
</button>
<spacer id="requests-menu-spacer-end"
<spacer id="requests-menu-spacer"
class="requests-menu-footer-spacer"
flex="100"/>
<label id="request-menu-network-summary"
<button id="requests-menu-network-summary-button"
class="requests-menu-footer-button"
tooltiptext="&netmonitorUI.footer.perf;"/>
<label id="requests-menu-network-summary-label"
class="plain requests-menu-footer-label"
flex="1"
crop="end"/>
crop="end"
tooltiptext="&netmonitorUI.footer.perf;"/>
<button id="requests-menu-clear-button"
class="requests-menu-footer-button"
label="&netmonitorUI.footer.clear;">
</button>
class="requests-menu-footer-button"
label="&netmonitorUI.footer.clear;"/>
</hbox>
</vbox>
<splitter id="splitter" class="devtools-side-splitter"/>
<splitter id="network-inspector-view-splitter"
class="devtools-side-splitter"/>
<deck id="details-pane"
hidden="true">
@ -460,4 +475,24 @@
</deck>
</box>
<box id="network-statistics-view">
<toolbar id="network-statistics-toolbar"
class="devtools-toolbar">
<button id="network-statistics-back-button"
class="devtools-toolbarbutton"
onclick="NetMonitorView.toggleFrontendMode()"
label="&netmonitorUI.backButton;"/>
</toolbar>
<box id="network-statistics-charts"
class="devtools-responsive-container"
flex="1">
<vbox id="primed-cache-chart" pack="center" flex="1"/>
<splitter id="network-statistics-view-splitter"
class="devtools-side-splitter"/>
<vbox id="empty-cache-chart" pack="center" flex="1"/>
</box>
</box>
</deck>
</window>

View File

@ -15,6 +15,7 @@ support-files =
html_post-raw-test-page.html
html_simple-test-page.html
html_sorting-test-page.html
html_statistics-test-page.html
html_status-codes-test-page.html
sjs_content-type-test-server.sjs
sjs_simple-test-server.sjs
@ -26,6 +27,11 @@ support-files =
[browser_net_accessibility-01.js]
[browser_net_accessibility-02.js]
[browser_net_autoscroll.js]
[browser_net_charts-01.js]
[browser_net_charts-02.js]
[browser_net_charts-03.js]
[browser_net_charts-04.js]
[browser_net_charts-05.js]
[browser_net_clear.js]
[browser_net_content-type.js]
[browser_net_copy_url.js]
@ -57,6 +63,8 @@ support-files =
[browser_net_sort-01.js]
[browser_net_sort-02.js]
[browser_net_sort-03.js]
[browser_net_statistics-01.js]
[browser_net_statistics-02.js]
[browser_net_status-codes.js]
[browser_net_timeline_ticks.js]
[browser_net_timing-division.js]

View File

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Makes sure Pie Charts have the right internal structure.
*/
function test() {
initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { document, Chart } = aMonitor.panelWin;
let container = document.createElement("box");
let pie = Chart.Pie(document, {
width: 100,
height: 100,
data: [{
size: 1,
label: "foo"
}, {
size: 2,
label: "bar"
}, {
size: 3,
label: "baz"
}]
});
let node = pie.node;
let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
let labels = node.querySelectorAll(".pie-chart-label");
ok(node.classList.contains("pie-chart-container") &&
node.classList.contains("generic-chart-container"),
"A pie chart container was created successfully.");
is(slices.length, 3,
"There should be 3 pie chart slices created.");
ok(slices[0].getAttribute("d").match(/\s*M 50,50 L 49\.\d+,97\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,2\.5\d* Z/),
"The first slice has the correct data.");
ok(slices[1].getAttribute("d").match(/\s*M 50,50 L 91\.\d+,26\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,97\.\d+ Z/),
"The second slice has the correct data.");
ok(slices[2].getAttribute("d").match(/\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 0 1 91\.\d+,26\.\d+ Z/),
"The third slice has the correct data.");
ok(slices[0].hasAttribute("largest"),
"The first slice should be the largest one.");
ok(slices[2].hasAttribute("smallest"),
"The third slice should be the smallest one.");
ok(slices[0].getAttribute("name"), "baz",
"The first slice's name is correct.");
ok(slices[1].getAttribute("name"), "bar",
"The first slice's name is correct.");
ok(slices[2].getAttribute("name"), "foo",
"The first slice's name is correct.");
is(labels.length, 3,
"There should be 3 pie chart labels created.");
is(labels[0].textContent, "baz",
"The first label's text is correct.");
is(labels[1].textContent, "bar",
"The first label's text is correct.");
is(labels[2].textContent, "foo",
"The first label's text is correct.");
teardown(aMonitor).then(finish);
});
}

View File

@ -0,0 +1,49 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Makes sure Pie Charts have the right internal structure when
* initialized with empty data.
*/
function test() {
initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { document, L10N, Chart } = aMonitor.panelWin;
let container = document.createElement("box");
let pie = Chart.Pie(document, {
data: null,
width: 100,
height: 100
});
let node = pie.node;
let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
let labels = node.querySelectorAll(".pie-chart-label");
ok(node.classList.contains("pie-chart-container") &&
node.classList.contains("generic-chart-container"),
"A pie chart container was created successfully.");
is(slices.length, 1,
"There should be 1 pie chart slice created.");
ok(slices[0].getAttribute("d").match(/\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/),
"The first slice has the correct data.");
ok(slices[0].hasAttribute("largest"),
"The first slice should be the largest one.");
ok(slices[0].hasAttribute("smallest"),
"The first slice should also be the smallest one.");
ok(slices[0].getAttribute("name"), L10N.getStr("pieChart.empty"),
"The first slice's name is correct.");
is(labels.length, 1,
"There should be 1 pie chart label created.");
is(labels[0].textContent, "Loading",
"The first label's text is correct.");
teardown(aMonitor).then(finish);
});
}

View File

@ -0,0 +1,100 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Makes sure Table Charts have the right internal structure.
*/
function test() {
initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { document, Chart } = aMonitor.panelWin;
let container = document.createElement("box");
let table = Chart.Table(document, {
title: "Table title",
data: [{
label1: 1,
label2: "11.1foo"
}, {
label1: 2,
label2: "12.2bar"
}, {
label1: 3,
label2: "13.3baz"
}],
totals: {
label1: "Hello %S",
label2: "World %S"
}
});
let node = table.node;
let title = node.querySelector(".table-chart-title");
let grid = node.querySelector(".table-chart-grid");
let totals = node.querySelector(".table-chart-totals");
let rows = grid.querySelectorAll(".table-chart-row");
let sums = node.querySelectorAll(".table-chart-summary-label");
ok(node.classList.contains("table-chart-container") &&
node.classList.contains("generic-chart-container"),
"A table chart container was created successfully.");
ok(title,
"A title node was created successfully.");
is(title.getAttribute("value"), "Table title",
"The title node displays the correct text.");
is(rows.length, 3,
"There should be 3 table chart rows created.");
ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
"A colored blob exists for the firt row.");
is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "label1",
"The first column of the first row exists.");
is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label2",
"The second column of the first row exists.");
is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "1",
"The first column of the first row displays the correct text.");
is(rows[0].querySelectorAll("label")[1].getAttribute("value"), "11.1foo",
"The second column of the first row displays the correct text.");
ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"),
"A colored blob exists for the second row.");
is(rows[1].querySelectorAll("label")[0].getAttribute("name"), "label1",
"The first column of the second row exists.");
is(rows[1].querySelectorAll("label")[1].getAttribute("name"), "label2",
"The second column of the second row exists.");
is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "2",
"The first column of the second row displays the correct text.");
is(rows[1].querySelectorAll("label")[1].getAttribute("value"), "12.2bar",
"The second column of the first row displays the correct text.");
ok(rows[2].querySelector(".table-chart-row-box.chart-colored-blob"),
"A colored blob exists for the third row.");
is(rows[2].querySelectorAll("label")[0].getAttribute("name"), "label1",
"The first column of the third row exists.");
is(rows[2].querySelectorAll("label")[1].getAttribute("name"), "label2",
"The second column of the third row exists.");
is(rows[2].querySelectorAll("label")[0].getAttribute("value"), "3",
"The first column of the third row displays the correct text.");
is(rows[2].querySelectorAll("label")[1].getAttribute("value"), "13.3baz",
"The second column of the third row displays the correct text.");
is(sums.length, 2,
"There should be 2 total summaries created.");
is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"), "label1",
"The first sum's type is correct.");
is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"), "Hello 6",
"The first sum's value is correct.");
is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"), "label2",
"The second sum's type is correct.");
is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"), "World 36.60",
"The second sum's value is correct.");
teardown(aMonitor).then(finish);
});
}

View File

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Makes sure Pie Charts have the right internal structure when
* initialized with empty data.
*/
function test() {
initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { document, L10N, Chart } = aMonitor.panelWin;
let container = document.createElement("box");
let table = Chart.Table(document, {
title: "Table title",
data: null,
totals: {
label1: "Hello %S",
label2: "World %S"
}
});
let node = table.node;
let title = node.querySelector(".table-chart-title");
let grid = node.querySelector(".table-chart-grid");
let totals = node.querySelector(".table-chart-totals");
let rows = grid.querySelectorAll(".table-chart-row");
let sums = node.querySelectorAll(".table-chart-summary-label");
ok(node.classList.contains("table-chart-container") &&
node.classList.contains("generic-chart-container"),
"A table chart container was created successfully.");
ok(title,
"A title node was created successfully.");
is(title.getAttribute("value"), "Table title",
"The title node displays the correct text.");
is(rows.length, 1,
"There should be 1 table chart row created.");
ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"),
"A colored blob exists for the firt row.");
is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size",
"The first column of the first row exists.");
is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label",
"The second column of the first row exists.");
is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "",
"The first column of the first row displays the correct text.");
is(rows[0].querySelectorAll("label")[1].getAttribute("value"), L10N.getStr("tableChart.empty"),
"The second column of the first row displays the correct text.");
is(sums.length, 2,
"There should be 2 total summaries created.");
is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"), "label1",
"The first sum's type is correct.");
is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"), "Hello 0",
"The first sum's value is correct.");
is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"), "label2",
"The second sum's type is correct.");
is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"), "World 0",
"The second sum's value is correct.");
teardown(aMonitor).then(finish);
});
}

View File

@ -0,0 +1,60 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Makes sure Pie+Table Charts have the right internal structure.
*/
function test() {
initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { document, Chart } = aMonitor.panelWin;
let container = document.createElement("box");
let chart = Chart.PieTable(document, {
title: "Table title",
data: [{
size: 1,
label: "11.1foo"
}, {
size: 2,
label: "12.2bar"
}, {
size: 3,
label: "13.3baz"
}],
totals: {
size: "Hello %S",
label: "World %S"
}
});
ok(chart.pie, "The pie chart proxy is accessible.");
ok(chart.table, "The table chart proxy is accessible.");
let node = chart.node;
let slices = node.querySelectorAll(".pie-chart-slice");
let rows = node.querySelectorAll(".table-chart-row");
let sums = node.querySelectorAll(".table-chart-summary-label");
ok(node.classList.contains("pie-table-chart-container"),
"A pie+table chart container was created successfully.");
ok(node.querySelector(".table-chart-title"),
"A title node was created successfully.");
ok(node.querySelector(".pie-chart-container"),
"A pie chart was created successfully.");
ok(node.querySelector(".table-chart-container"),
"A table chart was created successfully.");
is(rows.length, 3,
"There should be 3 pie chart slices created.");
is(rows.length, 3,
"There should be 3 table chart rows created.");
is(sums.length, 2,
"There should be 2 total summaries created.");
teardown(aMonitor).then(finish);
});
}

View File

@ -80,7 +80,7 @@ function test() {
})
function testStatus() {
let summary = $("#request-menu-network-summary");
let summary = $("#requests-menu-network-summary-label");
let value = summary.getAttribute("value");
info("Current summary: " + value);
@ -89,13 +89,7 @@ function test() {
let totalRequestsCount = RequestsMenu.itemCount;
info("Current requests: " + visibleRequestsCount + " of " + totalRequestsCount + ".");
if (!totalRequestsCount) {
is(value, "",
"The current summary text is incorrect, expected an empty string.");
return;
}
if (!visibleRequestsCount) {
if (!totalRequestsCount || !visibleRequestsCount) {
is(value, L10N.getStr("networkMenu.empty"),
"The current summary text is incorrect, expected an 'empty' label.");
return;

View File

@ -0,0 +1,57 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the statistics view is populated correctly.
*/
function test() {
initNetMonitor(STATISTICS_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let panel = aMonitor.panelWin;
let { document, $, $all, EVENTS, NetMonitorView } = panel;
is(NetMonitorView.currentFrontendMode, "network-inspector-view",
"The initial frontend mode is correct.");
is($("#primed-cache-chart").childNodes.length, 0,
"There should be no primed cache chart created yet.");
is($("#empty-cache-chart").childNodes.length, 0,
"There should be no empty cache chart created yet.");
waitFor(panel, EVENTS.PLACEHOLDER_CHARTS_DISPLAYED).then(() => {
is($("#primed-cache-chart").childNodes.length, 1,
"There should be a placeholder primed cache chart created now.");
is($("#empty-cache-chart").childNodes.length, 1,
"There should be a placeholder empty cache chart created now.");
is($all(".pie-chart-container[placeholder=true]").length, 2,
"Two placeholder pie chart appear to be rendered correctly.");
is($all(".table-chart-container[placeholder=true]").length, 2,
"Two placeholder table chart appear to be rendered correctly.");
promise.all([
waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
]).then(() => {
is($("#primed-cache-chart").childNodes.length, 1,
"There should be a real primed cache chart created now.");
is($("#empty-cache-chart").childNodes.length, 1,
"There should be a real empty cache chart created now.");
is($all(".pie-chart-container:not([placeholder=true])").length, 2,
"Two real pie chart appear to be rendered correctly.");
is($all(".table-chart-container:not([placeholder=true])").length, 2,
"Two real table chart appear to be rendered correctly.");
teardown(aMonitor).then(finish);
});
});
NetMonitorView.toggleFrontendMode();
is(NetMonitorView.currentFrontendMode, "network-statistics-view",
"The current frontend mode is correct.");
});
}

View File

@ -0,0 +1,43 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the network inspector view is shown when the target navigates
* away while in the statistics view.
*/
function test() {
initNetMonitor(STATISTICS_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let panel = aMonitor.panelWin;
let { document, EVENTS, NetMonitorView } = panel;
is(NetMonitorView.currentFrontendMode, "network-inspector-view",
"The initial frontend mode is correct.");
promise.all([
waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED),
waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED)
]).then(() => {
is(NetMonitorView.currentFrontendMode, "network-statistics-view",
"The frontend mode is currently in the statistics view.");
waitFor(panel, EVENTS.TARGET_WILL_NAVIGATE).then(() => {
is(NetMonitorView.currentFrontendMode, "network-inspector-view",
"The frontend mode switched back to the inspector view.");
waitFor(panel, EVENTS.TARGET_DID_NAVIGATE).then(() => {
is(NetMonitorView.currentFrontendMode, "network-inspector-view",
"The frontend mode is still in the inspector view.");
teardown(aMonitor).then(finish);
});
});
aDebuggee.location.reload();
});
NetMonitorView.toggleFrontendMode();
});
}

View File

@ -29,6 +29,7 @@ const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";

View File

@ -0,0 +1,37 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Network Monitor test page</title>
</head>
<body>
<p>Statistics test</p>
<script type="text/javascript">
function get(aAddress) {
var xhr = new XMLHttpRequest();
xhr.open("GET", aAddress, true);
xhr.send(null);
}
get("sjs_content-type-test-server.sjs?sts=304&fmt=txt");
get("sjs_content-type-test-server.sjs?sts=304&fmt=xml");
get("sjs_content-type-test-server.sjs?sts=304&fmt=html");
get("sjs_content-type-test-server.sjs?sts=304&fmt=css");
get("sjs_content-type-test-server.sjs?sts=304&fmt=js");
get("sjs_content-type-test-server.sjs?sts=304&fmt=json");
get("sjs_content-type-test-server.sjs?sts=304&fmt=jsonp");
get("sjs_content-type-test-server.sjs?sts=304&fmt=font");
get("sjs_content-type-test-server.sjs?sts=304&fmt=image");
get("sjs_content-type-test-server.sjs?sts=304&fmt=audio");
get("sjs_content-type-test-server.sjs?sts=304&fmt=video");
get("sjs_content-type-test-server.sjs?sts=304&fmt=flash");
get("test-image.png");
</script>
</body>
</html>

View File

@ -7,118 +7,151 @@ function handleRequest(request, response) {
response.processAsync();
let params = request.queryString.split("&");
let format = params.filter((s) => s.contains("fmt="))[0].split("=")[1];
let format = (params.filter((s) => s.contains("fmt="))[0] || "").split("=")[1];
let status = (params.filter((s) => s.contains("sts="))[0] || "").split("=")[1] || 200;
let cachedCount = 0;
let cacheExpire = 60; // seconds
function maybeMakeCached() {
if (status != 304) {
return;
}
// Spice things up a little!
if (cachedCount % 2) {
response.setHeader("Cache-Control", "max-age=" + cacheExpire, false);
} else {
response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false);
}
cachedCount++;
}
Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
switch (format) {
case "txt": {
response.setStatusLine(request.httpVersion, 200, "DA DA DA");
response.setStatusLine(request.httpVersion, status, "DA DA DA");
response.setHeader("Content-Type", "text/plain", false);
maybeMakeCached();
response.write("Братан, ты вообще качаешься?");
response.finish();
break;
}
case "xml": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
maybeMakeCached();
response.write("<label value='greeting'>Hello XML!</label>");
response.finish();
break;
}
case "html": {
let content = params.filter((s) => s.contains("res="))[0].split("=")[1];
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "text/html; charset=utf-8", false);
maybeMakeCached();
response.write(content || "<p>Hello HTML!</p>");
response.finish();
break;
}
case "html-long": {
let str = new Array(102400 /* 100 KB in bytes */).join(".");
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "text/html; charset=utf-8", false);
maybeMakeCached();
response.write("<p>" + str + "</p>");
response.finish();
break;
}
case "css": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "text/css; charset=utf-8", false);
maybeMakeCached();
response.write("body:pre { content: 'Hello CSS!' }");
response.finish();
break;
}
case "js": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
maybeMakeCached();
response.write("function() { return 'Hello JS!'; }");
response.finish();
break;
}
case "json": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "application/json; charset=utf-8", false);
maybeMakeCached();
response.write("{ \"greeting\": \"Hello JSON!\" }");
response.finish();
break;
}
case "jsonp": {
let fun = params.filter((s) => s.contains("jsonp="))[0].split("=")[1];
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "text/json; charset=utf-8", false);
maybeMakeCached();
response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })");
response.finish();
break;
}
case "json-long": {
let str = "{ \"greeting\": \"Hello long string JSON!\" },";
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "text/json; charset=utf-8", false);
maybeMakeCached();
response.write("[" + new Array(2048).join(str).slice(0, -1) + "]");
response.finish();
break;
}
case "json-malformed": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "text/json; charset=utf-8", false);
maybeMakeCached();
response.write("{ \"greeting\": \"Hello malformed JSON!\" },");
response.finish();
break;
}
case "json-custom-mime": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "text/x-bigcorp-json; charset=utf-8", false);
maybeMakeCached();
response.write("{ \"greeting\": \"Hello oddly-named JSON!\" }");
response.finish();
break;
}
case "font": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "font/woff", false);
maybeMakeCached();
response.finish();
break;
}
case "image": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "image/png", false);
maybeMakeCached();
response.finish();
break;
}
case "audio": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "audio/ogg", false);
maybeMakeCached();
response.finish();
break;
}
case "video": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "video/webm", false);
maybeMakeCached();
response.finish();
break;
}
case "flash": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setStatusLine(request.httpVersion, status, "OK");
response.setHeader("Content-Type", "application/x-shockwave-flash", false);
maybeMakeCached();
response.finish();
break;
}

View File

@ -0,0 +1,422 @@
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Ci = Components.interfaces;
const Cu = Components.utils;
const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties";
const SVG_NS = "http://www.w3.org/2000/svg";
const PI = Math.PI;
const TAU = PI * 2;
const EPSILON = 0.0000001;
const NAMED_SLICE_MIN_ANGLE = TAU / 8;
const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9;
const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
this.EXPORTED_SYMBOLS = ["Chart"];
/**
* Localization convenience methods.
*/
let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
/**
* A factory for creating charts.
* Example usage: let myChart = Chart.Pie(document, { ... });
*/
let Chart = {
Pie: createPieChart,
Table: createTableChart,
PieTable: createPieTableChart
};
/**
* A simple pie chart proxy for the underlying view.
* Each item in the `slices` property represents a [data, node] pair containing
* the data used to create the slice and the nsIDOMNode displaying it.
*
* @param nsIDOMNode node
* The node representing the view for this chart.
*/
function PieChart(node) {
this.node = node;
this.slices = new WeakMap();
EventEmitter.decorate(this);
}
/**
* A simple table chart proxy for the underlying view.
* Each item in the `rows` property represents a [data, node] pair containing
* the data used to create the row and the nsIDOMNode displaying it.
*
* @param nsIDOMNode node
* The node representing the view for this chart.
*/
function TableChart(node) {
this.node = node;
this.rows = new WeakMap();
EventEmitter.decorate(this);
}
/**
* A simple pie+table chart proxy for the underlying view.
*
* @param nsIDOMNode node
* The node representing the view for this chart.
* @param PieChart pie
* The pie chart proxy.
* @param TableChart table
* The table chart proxy.
*/
function PieTableChart(node, pie, table) {
this.node = node;
this.pie = pie;
this.table = table;
EventEmitter.decorate(this);
}
/**
* Creates the DOM for a pie+table chart.
*
* @param nsIDocument document
* The document responsible with creating the DOM.
* @param object
* An object containing all or some of the following properties:
* - title: a string displayed as the table chart's (description)/local
* - diameter: the diameter of the pie chart, in pixels
* - data: an array of items used to display each slice in the pie
* and each row in the table;
* @see `createPieChart` and `createTableChart` for details.
* - sorted: a flag specifying if the `data` should be sorted
* ascending by `size`.
* - totals: @see `createTableChart` for details.
* @return PieTableChart
* A pie+table chart proxy instance, which emits the following events:
* - "mouseenter", when the mouse enters a slice or a row
* - "mouseleave", when the mouse leaves a slice or a row
* - "click", when the mouse enters a slice or a row
*/
function createPieTableChart(document, { sorted, title, diameter, data, totals }) {
if (sorted) {
data = data.slice().sort((a, b) => +(parseFloat(a.size) < parseFloat(b.size)));
}
let pie = Chart.Pie(document, {
width: diameter,
data: data
});
let table = Chart.Table(document, {
title: title,
data: data,
totals: totals
});
let container = document.createElement("hbox");
container.className = "pie-table-chart-container";
container.appendChild(pie.node);
container.appendChild(table.node);
let proxy = new PieTableChart(container, pie, table);
pie.on("click", (event, item) => {
proxy.emit(event, item)
});
table.on("click", (event, item) => {
proxy.emit(event, item)
});
pie.on("mouseenter", (event, item) => {
proxy.emit(event, item);
if (table.rows.has(item)) {
table.rows.get(item).setAttribute("focused", "");
}
});
pie.on("mouseleave", (event, item) => {
proxy.emit(event, item);
if (table.rows.has(item)) {
table.rows.get(item).removeAttribute("focused");
}
});
table.on("mouseenter", (event, item) => {
proxy.emit(event, item);
if (pie.slices.has(item)) {
pie.slices.get(item).setAttribute("focused", "");
}
});
table.on("mouseleave", (event, item) => {
proxy.emit(event, item);
if (pie.slices.has(item)) {
pie.slices.get(item).removeAttribute("focused");
}
});
return proxy;
}
/**
* Creates the DOM for a pie chart based on the specified properties.
*
* @param nsIDocument document
* The document responsible with creating the DOM.
* @param object
* An object containing all or some of the following properties:
* - data: an array of items used to display each slice; all the items
* should be objects containing a `size` and a `label` property.
* e.g: [{
* size: 1,
* label: "foo"
* }, {
* size: 2,
* label: "bar"
* }];
* - width: the width of the chart, in pixels
* - height: optional, the height of the chart, in pixels.
* - centerX: optional, the X-axis center of the chart, in pixels.
* - centerY: optional, the Y-axis center of the chart, in pixels.
* - radius: optional, the radius of the chart, in pixels.
* @return PieChart
* A pie chart proxy instance, which emits the following events:
* - "mouseenter", when the mouse enters a slice
* - "mouseleave", when the mouse leaves a slice
* - "click", when the mouse clicks a slice
*/
function createPieChart(document, { data, width, height, centerX, centerY, radius }) {
height = height || width;
centerX = centerX || width / 2;
centerY = centerY || height / 2;
radius = radius || (width + height) / 4;
let isPlaceholder = false;
// Filter out very small sizes, as they'll just render invisible slices.
data = data ? data.filter(e => parseFloat(e.size) > EPSILON) : null;
// If there's no data available, display an empty placeholder.
if (!data || !data.length) {
data = emptyPieChartData;
isPlaceholder = true;
}
let container = document.createElementNS(SVG_NS, "svg");
container.setAttribute("class", "generic-chart-container pie-chart-container");
container.setAttribute("pack", "center");
container.setAttribute("flex", "1");
container.setAttribute("width", width);
container.setAttribute("height", height);
container.setAttribute("viewBox", "0 0 " + width + " " + height);
container.setAttribute("slices", data.length);
container.setAttribute("placeholder", isPlaceholder);
let proxy = new PieChart(container);
let total = data.reduce((acc, e) => acc + parseFloat(e.size), 0);
let angles = data.map(e => parseFloat(e.size) / total * (TAU - EPSILON));
let largest = data.reduce((a, b) => parseFloat(a.size) > parseFloat(b.size) ? a : b);
let smallest = data.reduce((a, b) => parseFloat(a.size) < parseFloat(b.size) ? a : b);
let textDistance = radius / NAMED_SLICE_TEXT_DISTANCE_RATIO;
let translateDistance = radius / HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO;
let startAngle = TAU;
let endAngle = 0;
let midAngle = 0;
radius -= translateDistance;
for (let i = data.length - 1; i >= 0; i--) {
let sliceInfo = data[i];
let sliceAngle = angles[i];
if (!sliceInfo.size || sliceAngle < EPSILON) {
continue;
}
endAngle = startAngle - sliceAngle;
midAngle = (startAngle + endAngle) / 2;
let x1 = centerX + radius * Math.sin(startAngle);
let y1 = centerY - radius * Math.cos(startAngle);
let x2 = centerX + radius * Math.sin(endAngle);
let y2 = centerY - radius * Math.cos(endAngle);
let largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0;
let pathNode = document.createElementNS(SVG_NS, "path");
pathNode.setAttribute("class", "pie-chart-slice chart-colored-blob");
pathNode.setAttribute("name", sliceInfo.label);
pathNode.setAttribute("d",
" M " + centerX + "," + centerY +
" L " + x2 + "," + y2 +
" A " + radius + "," + radius +
" 0 " + largeArcFlag +
" 1 " + x1 + "," + y1 +
" Z");
if (sliceInfo == largest) {
pathNode.setAttribute("largest", "");
}
if (sliceInfo == smallest) {
pathNode.setAttribute("smallest", "");
}
let hoverX = translateDistance * Math.sin(midAngle);
let hoverY = -translateDistance * Math.cos(midAngle);
let hoverTransform = "transform: translate(" + hoverX + "px, " + hoverY + "px)";
pathNode.setAttribute("style", hoverTransform);
proxy.slices.set(sliceInfo, pathNode);
delegate(proxy, ["click", "mouseenter", "mouseleave"], pathNode, sliceInfo);
container.appendChild(pathNode);
if (sliceInfo.label && sliceAngle > NAMED_SLICE_MIN_ANGLE) {
let textX = centerX + textDistance * Math.sin(midAngle);
let textY = centerY - textDistance * Math.cos(midAngle);
let label = document.createElementNS(SVG_NS, "text");
label.appendChild(document.createTextNode(sliceInfo.label));
label.setAttribute("class", "pie-chart-label");
label.setAttribute("style", data.length > 1 ? hoverTransform : "");
label.setAttribute("x", data.length > 1 ? textX : centerX);
label.setAttribute("y", data.length > 1 ? textY : centerY);
container.appendChild(label);
}
startAngle = endAngle;
}
return proxy;
}
/**
* Creates the DOM for a table chart based on the specified properties.
*
* @param nsIDocument document
* The document responsible with creating the DOM.
* @param object
* An object containing all or some of the following properties:
* - title: a string displayed as the chart's (description)/local
* - data: an array of items used to display each row; all the items
* should be objects representing columns, for which the
* properties' values will be displayed in each cell of a row.
* e.g: [{
* size: 1,
* label2: "1foo",
* label3: "2yolo"
* }, {
* size: 2,
* label2: "3bar",
* label3: "4swag"
* }];
* - totals: an object specifying for which rows in the `data` array
* the sum of their cells is to be displayed in the chart;
* e.g: {
* label1: "Total size: %S",
* label3: "Total lolz: %S"
* }
* @return TableChart
* A table chart proxy instance, which emits the following events:
* - "mouseenter", when the mouse enters a row
* - "mouseleave", when the mouse leaves a row
* - "click", when the mouse clicks a row
*/
function createTableChart(document, { data, totals, title }) {
let isPlaceholder = false;
// If there's no data available, display an empty placeholder.
if (!data || !data.length) {
data = emptyTableChartData;
isPlaceholder = true;
}
let container = document.createElement("vbox");
container.className = "generic-chart-container table-chart-container";
container.setAttribute("pack", "center");
container.setAttribute("flex", "1");
container.setAttribute("rows", data.length);
container.setAttribute("placeholder", isPlaceholder);
let proxy = new TableChart(container);
let titleNode = document.createElement("label");
titleNode.className = "plain table-chart-title";
titleNode.setAttribute("value", title);
container.appendChild(titleNode);
let tableNode = document.createElement("vbox");
tableNode.className = "plain table-chart-grid";
container.appendChild(tableNode);
for (let rowInfo of data) {
let rowNode = document.createElement("hbox");
rowNode.className = "table-chart-row";
rowNode.setAttribute("align", "center");
let boxNode = document.createElement("hbox");
boxNode.className = "table-chart-row-box chart-colored-blob";
boxNode.setAttribute("name", rowInfo.label);
rowNode.appendChild(boxNode);
for (let [key, value] in Iterator(rowInfo)) {
let labelNode = document.createElement("label");
labelNode.className = "plain table-chart-row-label";
labelNode.setAttribute("name", key);
labelNode.setAttribute("value", value);
rowNode.appendChild(labelNode);
}
proxy.rows.set(rowInfo, rowNode);
delegate(proxy, ["click", "mouseenter", "mouseleave"], rowNode, rowInfo);
tableNode.appendChild(rowNode);
}
let totalsNode = document.createElement("vbox");
totalsNode.className = "table-chart-totals";
for (let [key, value] in Iterator(totals || {})) {
let total = data.reduce((acc, e) => acc + parseFloat(e[key]), 0);
let formatted = !isNaN(total) ? L10N.numberWithDecimals(total, 2) : 0;
let labelNode = document.createElement("label");
labelNode.className = "plain table-chart-summary-label";
labelNode.setAttribute("name", key);
labelNode.setAttribute("value", value.replace("%S", formatted));
totalsNode.appendChild(labelNode);
}
container.appendChild(totalsNode);
return proxy;
}
XPCOMUtils.defineLazyGetter(this, "emptyPieChartData", () => {
return [{ size: 1, label: L10N.getStr("pieChart.empty") }];
});
XPCOMUtils.defineLazyGetter(this, "emptyTableChartData", () => {
return [{ size: "", label: L10N.getStr("tableChart.empty") }];
});
/**
* Delegates DOM events emitted by an nsIDOMNode to an EventEmitter proxy.
*
* @param EventEmitter emitter
* The event emitter proxy instance.
* @param array events
* An array of events, e.g. ["mouseenter", "mouseleave"].
* @param nsIDOMNode node
* The element firing the DOM events.
* @param any args
* The arguments passed when emitting events through the proxy.
*/
function delegate(emitter, events, node, args) {
for (let event of events) {
node.addEventListener(event, emitter.emit.bind(emitter, event, args));
}
}

View File

@ -11,9 +11,14 @@
- A good criteria is the language in which you'd find the best
- documentation on web development on the web. -->
<!-- LOCALIZATION NOTE (netmonitorUI.emptyNotice2): This is the label displayed
<!-- LOCALIZATION NOTE (netmonitorUI.perfNotice1/2): These are the labels displayed
- in the network table when empty to start performance analysis. -->
<!ENTITY netmonitorUI.perfNotice1 "• Click on the">
<!ENTITY netmonitorUI.perfNotice2 "button to start performance analysis.">
<!-- LOCALIZATION NOTE (netmonitorUI.emptyNotice3): This is the label displayed
- in the network table when empty. -->
<!ENTITY netmonitorUI.emptyNotice2 "Perform a request or reload the page to see detailed information about network activity.">
<!ENTITY netmonitorUI.emptyNotice3 "• Perform a request or reload the page to see detailed information about network activity.">
<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.status2): This is the label displayed
- in the network table toolbar, above the "status" column. -->
@ -99,10 +104,18 @@
- in the network details footer for the "Flash" filtering button. -->
<!ENTITY netmonitorUI.footer.filterFlash "Flash">
<!-- LOCALIZATION NOTE (debuggerUI.footer.filterOther): This is the label displayed
- in the network details footer for the "Other" filtering button. -->
<!ENTITY netmonitorUI.footer.filterOther "Other">
<!-- LOCALIZATION NOTE (debuggerUI.footer.clear): This is the label displayed
- in the network details footer for the "Clear" button. -->
<!ENTITY netmonitorUI.footer.clear "Clear">
<!-- LOCALIZATION NOTE (debuggerUI.footer.clear): This is the label displayed
- in the network details footer for the performance analysis button. -->
<!ENTITY netmonitorUI.footer.perf "Toggle performance analysis...">
<!-- LOCALIZATION NOTE (debuggerUI.panesButton.tooltip): This is the tooltip for
- the button that toggles the panes visible or hidden in the netmonitor UI. -->
<!ENTITY netmonitorUI.panesButton.tooltip "Toggle network info">
@ -173,22 +186,30 @@
- in a "receive" state. -->
<!ENTITY netmonitorUI.timings.receive "Receiving:">
<!-- LOCALIZATION NOTE (netmonitorUI.context.perfTools): This is the label displayed
- on the context menu that shows the performance analysis tools -->
<!ENTITY netmonitorUI.context.perfTools "Start Performance Analysis...">
<!-- LOCALIZATION NOTE (netmonitorUI.context.perfTools.accesskey): This is the access key
- for the performance analysis menu item displayed in the context menu for a request -->
<!ENTITY netmonitorUI.context.perfTools.accesskey "S">
<!-- LOCALIZATION NOTE (netmonitorUI.context.copyUrl): This is the label displayed
- on the context menu that copies the selected request's url -->
<!ENTITY netmonitorUI.context.copyUrl "Copy URL">
<!ENTITY netmonitorUI.context.copyUrl "Copy URL">
<!-- LOCALIZATION NOTE (netmonitorUI.context.copyUrl.accesskey): This is the access key
- for the Copy URL menu item displayed in the context menu for a request -->
<!ENTITY netmonitorUI.context.copyUrl.accesskey "C">
<!ENTITY netmonitorUI.context.copyUrl.accesskey "C">
<!-- LOCALIZATION NOTE (debuggerUI.summary.editAndResend): This is the label displayed
- on the button in the headers tab that opens a form to edit and resend the currently
displayed request -->
<!ENTITY netmonitorUI.summary.editAndResend "Edit and Resend">
<!ENTITY netmonitorUI.summary.editAndResend "Edit and Resend">
<!-- LOCALIZATION NOTE (debuggerUI.summary.editAndResend.accesskey): This is the access key
- for the "Edit and Resend" menu item displayed in the context menu for a request -->
<!ENTITY netmonitorUI.summary.editAndResend.accesskey "R">
<!ENTITY netmonitorUI.summary.editAndResend.accesskey "R">
<!-- LOCALIZATION NOTE (netmonitorUI.context.newTab): This is the label
- for the Open in New Tab menu item displayed in the context menu of the
@ -198,7 +219,7 @@
<!-- LOCALIZATION NOTE (netmonitorUI.context.newTab.accesskey): This is the access key
- for the Open in New Tab menu item displayed in the context menu of the
- network container -->
<!ENTITY netmonitorUI.context.newTab.accesskey "O">
<!ENTITY netmonitorUI.context.newTab.accesskey "O">
<!-- LOCALIZATION NOTE (debuggerUI.custom.newRequest): This is the label displayed
- as the title of the new custom request form -->
@ -223,3 +244,7 @@
<!-- LOCALIZATION NOTE (debuggerUI.custom.cancel): This is the label displayed
- on the button which cancels and closes the custom request form -->
<!ENTITY netmonitorUI.custom.cancel "Cancel">
<!-- LOCALIZATION NOTE (debuggerUI.backButton): This is the label displayed
- on the button which exists the performance statistics view -->
<!ENTITY netmonitorUI.backButton "Back">

View File

@ -135,3 +135,49 @@ networkMenu.second=%S s
# LOCALIZATION NOTE (networkMenu.minute): This is the label displayed
# in the network menu specifying timing interval divisions (in minutes).
networkMenu.minute=%S min
# LOCALIZATION NOTE (networkMenu.minute): This is the label displayed
# in the network menu specifying timing interval divisions (in minutes).
networkMenu.minute=%S min
# LOCALIZATION NOTE (pieChart.empty): This is the label displayed
# for pie charts (e.g., in the performance analysis view) when there is
# no data available yet.
pieChart.empty=Loading
# LOCALIZATION NOTE (tableChart.empty): This is the label displayed
# for table charts (e.g., in the performance analysis view) when there is
# no data available yet.
tableChart.empty=Please wait…
# LOCALIZATION NOTE (charts.sizeKB): This is the label displayed
# in pie or table charts specifying the size of a request (in kilobytes).
charts.sizeKB=%S KB
# LOCALIZATION NOTE (charts.totalMS): This is the label displayed
# in pie or table charts specifying the time for a request to finish (in milliseconds).
charts.totalMS=%S ms
# LOCALIZATION NOTE (charts.cacheEnabled): This is the label displayed
# in the performance analysis view for "cache enabled" charts.
charts.cacheEnabled=Primed cache
# LOCALIZATION NOTE (charts.cacheDisabled): This is the label displayed
# in the performance analysis view for "cache disabled" charts.
charts.cacheDisabled=Empty cache
# LOCALIZATION NOTE (charts.totalSize): This is the label displayed
# in the performance analysis view for total requests size, in kilobytes.
charts.totalSize=Size: %S KB
# LOCALIZATION NOTE (charts.totalTime): This is the label displayed
# in the performance analysis view for total requests time, in milliseconds.
charts.totalTime=Time: %S ms
# LOCALIZATION NOTE (charts.totalCached): This is the label displayed
# in the performance analysis view for total cached responses.
charts.totalCached=Cached responses: %S
# LOCALIZATION NOTE (charts.totalCount): This is the label displayed
# in the performance analysis view for total requests.
charts.totalCount=Total requests: %S

View File

@ -6,7 +6,7 @@
#requests-menu-empty-notice {
margin: 0;
padding: 12px;
font-size: 110%;
font-size: 120%;
}
.theme-dark #requests-menu-empty-notice {
@ -17,6 +17,18 @@
color: #585959; /* Grey foreground text */
}
#requests-menu-perf-notice-button {
min-width: 30px;
min-height: 28px;
margin: 0;
list-style-image: url(profiler-stopwatch.png);
-moz-image-region: rect(0px,16px,16px,0px);
}
#requests-menu-perf-notice-button .button-text {
display: none;
}
%filter substitution
%define table_itemDarkStartBorder rgba(0,0,0,0.2)
%define table_itemDarkEndBorder rgba(128,128,128,0.15)
@ -475,12 +487,11 @@ box.requests-menu-status {
/* Network request details tabpanels */
.theme-dark .tabpanel-content {
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
color: #f5f7fa; /* Light foreground text */
}
.theme-dark .tabpanel-summary-label {
color: #f5f7fa; /* Dark foreground text */
}
/* Summary tabpanel */
.tabpanel-summary-container {
padding: 1px;
@ -578,7 +589,7 @@ box.requests-menu-status {
min-width: 1em;
margin: 0;
border: none;
padding: 2px 1.5vw;
padding: 2px 0.75vw;
}
.theme-dark .requests-menu-footer-button,
@ -595,14 +606,14 @@ box.requests-menu-status {
min-width: 2px;
}
.theme-dark .requests-menu-footer-spacer:not(:first-of-type),
.theme-dark .requests-menu-footer-button:not(:first-of-type) {
.theme-dark .requests-menu-footer-spacer:not(:first-child),
.theme-dark .requests-menu-footer-button:not(:first-child) {
-moz-border-start: 1px solid @table_itemDarkStartBorder@;
box-shadow: -1px 0 0 @table_itemDarkEndBorder@;
}
.theme-light .requests-menu-footer-spacer:not(:first-of-type),
.theme-light .requests-menu-footer-button:not(:first-of-type) {
.theme-light .requests-menu-footer-spacer:not(:first-child),
.theme-light .requests-menu-footer-button:not(:first-child) {
-moz-border-start: 1px solid @table_itemLightStartBorder@;
box-shadow: -1px 0 0 @table_itemLightEndBorder@;
}
@ -628,10 +639,179 @@ box.requests-menu-status {
}
.requests-menu-footer-label {
padding-top: 2px;
padding-top: 3px;
font-weight: 600;
}
/* Performance analysis buttons */
#requests-menu-network-summary-button {
background: none;
box-shadow: none;
border-color: transparent;
list-style-image: url(profiler-stopwatch.png);
-moz-image-region: rect(0px,16px,16px,0px);
-moz-padding-end: 0;
cursor: pointer;
}
#requests-menu-network-summary-label {
-moz-padding-start: 0;
cursor: pointer;
}
#requests-menu-network-summary-label:hover {
text-decoration: underline;
}
/* Performance analysis view */
#network-statistics-toolbar {
border: none;
margin: 0;
padding: 0;
}
#network-statistics-back-button {
min-width: 4em;
min-height: 100vh;
margin: 0;
padding: 0;
border-radius: 0;
border-top: none;
border-bottom: none;
-moz-border-start: none;
}
#network-statistics-view-splitter {
border-color: rgba(0,0,0,0.2);
cursor: default;
pointer-events: none;
}
#network-statistics-charts {
min-height: 1px;
}
.theme-dark #network-statistics-charts {
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
}
.theme-light #network-statistics-charts {
background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
}
#network-statistics-charts .pie-chart-container {
-moz-margin-start: 3vw;
-moz-margin-end: 1vw;
}
#network-statistics-charts .table-chart-container {
-moz-margin-start: 1vw;
-moz-margin-end: 3vw;
}
.theme-dark .chart-colored-blob[name=html] {
fill: #5e88b0; /* Blue-Grey highlight */
background: #5e88b0;
}
.theme-light .chart-colored-blob[name=html] {
fill: #5f88b0; /* Blue-Grey highlight */
background: #5f88b0;
}
.theme-dark .chart-colored-blob[name=css] {
fill: #46afe3; /* Blue highlight */
background: #46afe3;
}
.theme-light .chart-colored-blob[name=css] {
fill: #0088cc; /* Blue highlight */
background: #0088cc;
}
.theme-dark .chart-colored-blob[name=js] {
fill: #d99b28; /* Light Orange highlight */
background: #d99b28;
}
.theme-light .chart-colored-blob[name=js] {
fill: #d97e00; /* Light Orange highlight */
background: #d97e00;
}
.theme-dark .chart-colored-blob[name=xhr] {
fill: #d96629; /* Orange highlight */
background: #d96629;
}
.theme-light .chart-colored-blob[name=xhr] {
fill: #f13c00; /* Orange highlight */
background: #f13c00;
}
.theme-dark .chart-colored-blob[name=fonts] {
fill: #6b7abb; /* Purple highlight */
background: #6b7abb;
}
.theme-light .chart-colored-blob[name=fonts] {
fill: #5b5fff; /* Purple highlight */
background: #5b5fff;
}
.theme-dark .chart-colored-blob[name=images] {
fill: #df80ff; /* Pink highlight */
background: #df80ff;
}
.theme-light .chart-colored-blob[name=images] {
fill: #b82ee5; /* Pink highlight */
background: #b82ee5;
}
.theme-dark .chart-colored-blob[name=media] {
fill: #70bf53; /* Green highlight */
background: #70bf53;
}
.theme-light .chart-colored-blob[name=media] {
fill: #2cbb0f; /* Green highlight */
background: #2cbb0f;
}
.theme-dark .chart-colored-blob[name=flash] {
fill: #eb5368; /* Red highlight */
background: #eb5368;
}
.theme-light .chart-colored-blob[name=flash] {
fill: #ed2655; /* Red highlight */
background: #ed2655;
}
.table-chart-row-label[name=cached] {
display: none;
}
.table-chart-row-label[name=count] {
width: 3em;
text-align: end;
}
.table-chart-row-label[name=label] {
width: 7em;
}
.table-chart-row-label[name=size] {
width: 7em;
}
.table-chart-row-label[name=time] {
width: 7em;
}
/* Responsive sidebar */
@media (max-width: 700px) {
#requests-menu-toolbar {
@ -644,7 +824,7 @@ box.requests-menu-status {
.requests-menu-footer-button,
.requests-menu-footer-label {
padding: 2px 2vw;
padding: 2px 1vw;
}
#details-pane {

View File

@ -723,7 +723,9 @@
.theme-light #breadcrumb-separator-normal,
.theme-light .scrollbutton-up > .toolbarbutton-icon,
.theme-light .scrollbutton-down > .toolbarbutton-icon,
.theme-light #black-boxed-message-button .button-icon {
.theme-light #black-boxed-message-button .button-icon,
.theme-light #requests-menu-perf-notice-button .button-icon,
.theme-light #requests-menu-network-summary-button .button-icon {
filter: url(filters.svg#invert);
}

View File

@ -792,4 +792,145 @@
visibility: hidden;
}
/* Charts */
.generic-chart-container {
/* Hack: force hardware acceleration */
transform: translateZ(1px);
}
.theme-dark .generic-chart-container {
color: #f5f7fa; /* Light foreground text */
}
.theme-light .generic-chart-container {
color: #585959; /* Grey foreground text */
}
.theme-dark .chart-colored-blob {
fill: #b8c8d9; /* Light content text */
background: #b8c8d9;
}
.theme-light .chart-colored-blob {
fill: #8fa1b2; /* Grey content text */
background: #8fa1b2;
}
/* Charts: Pie */
.pie-chart-slice {
stroke-width: 1px;
cursor: pointer;
}
.theme-dark .pie-chart-slice {
stroke: rgba(0,0,0,0.2);
}
.theme-light .pie-chart-slice {
stroke: rgba(255,255,255,0.8);
}
.theme-dark .pie-chart-slice[largest] {
stroke-width: 2px;
stroke: #fff;
}
.theme-light .pie-chart-slice[largest] {
stroke: #000;
}
.pie-chart-label {
text-anchor: middle;
dominant-baseline: middle;
pointer-events: none;
}
.theme-dark .pie-chart-label {
fill: #000;
}
.theme-light .pie-chart-label {
fill: #fff;
}
.pie-chart-container[slices="1"] > .pie-chart-slice {
stroke-width: 0px;
}
.pie-chart-slice,
.pie-chart-label {
transition: all 0.1s ease-out;
}
.pie-chart-slice:not(:hover):not([focused]),
.pie-chart-slice:not(:hover):not([focused]) + .pie-chart-label {
transform: none !important;
}
/* Charts: Table */
.table-chart-title {
padding-bottom: 10px;
font-size: 120%;
font-weight: 600;
}
.table-chart-row {
margin-top: 1px;
cursor: pointer;
}
.table-chart-grid:hover > .table-chart-row {
transition: opacity 0.1s ease-in-out;
}
.table-chart-grid:not(:hover) > .table-chart-row {
transition: opacity 0.2s ease-in-out;
}
.generic-chart-container:hover > .table-chart-grid:hover > .table-chart-row:not(:hover),
.generic-chart-container:hover ~ .table-chart-container > .table-chart-grid > .table-chart-row:not([focused]) {
opacity: 0.4;
}
.table-chart-row-box {
width: 8px;
height: 1.5em;
-moz-margin-end: 10px;
}
.table-chart-row-label {
width: 8em;
-moz-padding-end: 6px;
cursor: inherit;
}
.table-chart-totals {
margin-top: 8px;
padding-top: 6px;
}
.theme-dark .table-chart-totals {
border-top: 1px solid #b6babf; /* Grey foreground text */
}
.theme-light .table-chart-totals {
border-top: 1px solid #585959; /* Grey foreground text */
}
.table-chart-summary-label {
font-weight: 600;
padding: 1px 0px;
}
.theme-dark .table-chart-summary-label {
color: #f5f7fa; /* Light foreground text */
}
.theme-light .table-chart-summary-label {
color: #18191a; /* Dark foreground text */
}
%include ../../shared/devtools/app-manager/manifest-editor.inc.css

View File

@ -781,7 +781,12 @@ BrowserTabActor.prototype = {
reload = true;
}
if (reload) {
// Reload if:
// - there's an explicit `performReload` flag and it's true
// - there's no `performReload` flag, but it makes sense to do so
let hasExplicitReloadFlag = "performReload" in options;
if ((hasExplicitReloadFlag && options.performReload) ||
(!hasExplicitReloadFlag && reload)) {
this.onReload();
}
},