author | Victor Porof <vporof@mozilla.com> |
Sat, 01 Feb 2014 08:37:53 +0200 | |
changeset 166447 | b3a41a5f1972abd1e0b0b1afb61759e811c25637 |
parent 166430 | c21d44250e0c3b35bf23abac778cc63207ccf89f |
child 166448 | b8b35ef1ea88e46d3b618f59657b3b005cf67c37 |
push id | 26127 |
push user | philringnalda@gmail.com |
push date | Sun, 02 Feb 2014 17:11:12 +0000 |
treeherder | mozilla-central@2918a9e625b4 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | rcampbell |
bugs | 946601 |
milestone | 29.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1136,16 +1136,17 @@ pref("devtools.profiler.enabled", true); pref("devtools.profiler.ui.show-platform-data", false); // Enable the Network Monitor 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); pref("devtools.tilt.intro_transition", true); pref("devtools.tilt.outro_transition", true); // Scratchpad settings // - recentFileMax: The maximum number of recently-opened files
--- a/browser/devtools/netmonitor/netmonitor-controller.js +++ b/browser/devtools/netmonitor/netmonitor-controller.js @@ -52,45 +52,74 @@ const EVENTS = { RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent", // When the request post params are displayed in the UI. REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable", // 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"); Object.defineProperty(this, "NetworkHelper", { get: function() { return devtools.require("devtools/toolkit/webconsole/network-helper"); }, configurable: true, @@ -250,19 +279,91 @@ let NetMonitorController = { if (aCallback) { aCallback(); } }); }); }, + /** + * 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 }; /** * Functions handling target-related lifetime events. */ @@ -309,16 +410,21 @@ TargetEventsHandler.prototype = { _onTabNavigated: function(aType, aPacket) { switch (aType) { case "will-navigate": { // Reset UI. NetMonitorView.RequestsMenu.reset(); 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; } case "navigate": { window.emit(EVENTS.TARGET_DID_NAVIGATE); break; } } @@ -378,17 +484,16 @@ NetworkEventsHandler.prototype = { * @param string aType * Message type. * @param object aPacket * The message received from the server. */ _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); }, /** * The "networkEventUpdate" message type handler. * * @param string aType * Message type. @@ -580,17 +685,18 @@ NetworkEventsHandler.prototype = { */ let L10N = new ViewHelpers.L10N(NET_STRINGS_URI); /** * Shortcuts for accessing various network monitor preferences. */ 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"] }); /** * Returns true if this is document is in RTL mode. * @return boolean */ XPCOMUtils.defineLazyGetter(window, "isRTL", function() { return window.getComputedStyle(document.documentElement, null).direction == "rtl"; @@ -612,16 +718,49 @@ NetMonitorController.NetworkEventsHandle */ Object.defineProperties(window, { "gNetwork": { get: function() NetMonitorController.NetworkEventsHandler } }); /** + * 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 */ function dumpn(str) { if (wantLogging) { dump("NET-FRONTEND: " + str + "\n"); } }
--- a/browser/devtools/netmonitor/netmonitor-view.js +++ b/browser/devtools/netmonitor/netmonitor-view.js @@ -16,16 +16,17 @@ const REQUESTS_WATERFALL_HEADER_TICKS_MU const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3; const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px 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 = { "ecmascript": "js", "javascript": "js", "x-javascript": "js" }; const CONTENT_MIME_TYPE_MAPPINGS = { @@ -52,16 +53,17 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = searchEnabled: true, editableValueTooltip: "", editableNameTooltip: "", preventDisableOnChange: true, preventDescriptorModifiers: true, eval: () => {}, switch: () => {} }; +const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px /** * Object defining the network monitor view components. */ let NetMonitorView = { /** * Initializes the network monitor view. */ @@ -97,16 +99,24 @@ let NetMonitorView = { this._detailsPaneToggleButton = $("#details-pane-toggle"); this._collapsePaneString = L10N.getStr("collapseDetailsPane"); this._expandPaneString = L10N.getStr("expandDetailsPane"); 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; + } }, /** * Destroys the UI for all the displayed panes. */ _destroyPanes: function() { dumpn("Destroying the NetMonitorView panes"); @@ -116,18 +126,19 @@ let NetMonitorView = { this._detailsPane = null; this._detailsPaneToggleButton = null; }, /** * 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. * * @param object aFlags * An object containing some of the following properties: * - visible: true if the pane should be shown, false to hide * - animated: true to display an animation on toggle @@ -153,16 +164,76 @@ let NetMonitorView = { } if (aTabIndex !== undefined) { $("#event-details-pane").selectedIndex = aTabIndex; } }, /** + * 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. * * @param string aId * The id of the editor placeholder node. * @return object * A promise that is resolved when the editor is available. */ editor: function(aId) { @@ -258,46 +329,53 @@ function RequestsMenuView() { RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { /** * Initialization function, called when the network monitor is started. */ initialize: function() { 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; this.widget.autoscrollWithAppendedItems = true; this.widget.addEventListener("select", this._onSelect, false); this._splitter.addEventListener("mousemove", this._onResize, false); window.addEventListener("resize", this._onResize, false); 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); this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this); $("#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); $("#headers-summary-resend").addEventListener("click", this.cloneSelectedRequestEvent, false); }, /** * Destruction function, called when the network monitor is closed. @@ -306,32 +384,38 @@ RequestsMenuView.prototype = Heritage.ex dumpn("Destroying the SourcesView"); this.widget.removeEventListener("select", this._onSelect, false); this._splitter.removeEventListener("mousemove", this._onResize, false); window.removeEventListener("resize", this._onResize, false); $("#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); $("#headers-summary-resend").removeEventListener("click", this.cloneSelectedRequestEvent, false); }, /** * Resets this container (removes all the networking information). */ reset: function() { this.empty(); + this.filterOn("all"); this._firstRequestStartedMillis = -1; this._lastRequestEndedMillis = -1; }, /** * Specifies if this view may be updated lazily. */ lazyUpdate: true, @@ -389,16 +473,17 @@ RequestsMenuView.prototype = Heritage.ex * the currently selected request. */ cloneSelectedRequest: function() { let selected = this.selectedItem.attachment; // Create the element node for the network request item. let menuView = this._createMenuView(selected.method, selected.url); + // Append a network request item to this container. let newItem = this.push([menuView], { attachment: Object.create(selected, { isCustom: { value: true } }) }); // Immediately switch to new request pane. this.selectedItem = newItem; @@ -452,17 +537,17 @@ RequestsMenuView.prototype = Heritage.ex NetMonitorView.Sidebar.toggle(false); }, /** * Filters all network requests in this container by a specified type. * * @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"); let buttons = document.querySelectorAll(".requests-menu-footer-button"); for (let button of buttons) { if (button != target) { button.removeAttribute("checked"); @@ -472,38 +557,41 @@ RequestsMenuView.prototype = Heritage.ex } // Filter on whatever was requested. switch (aType) { case "all": 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; } this.refreshSummary(); this.refreshZebra(); }, /** @@ -606,56 +694,60 @@ RequestsMenuView.prototype = Heritage.ex /** * Predicates used when filtering items. * * @param object aItem * The filtered item. * @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"))) || url.contains(".eot") || url.contains(".ttf") || 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. * * @param object aFirst * The first item used in the comparison. * @param object aSecond * The second item used in the comparison. * @return number @@ -719,18 +811,18 @@ RequestsMenuView.prototype = Heritage.ex let totalMillis = this._getNewestRequest(visibleItems).attachment.endedMillis - this._getOldestRequest(visibleItems).attachment.startedMillis; // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals 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)) ); }, /** * Adds odd/even attributes to all the visible items in this container. */ refreshZebra: function() { let visibleItems = this.visibleItems; @@ -833,16 +925,22 @@ RequestsMenuView.prototype = Heritage.ex this.updateMenuView(requestItem, key, value); break; case "mimeType": requestItem.attachment.mimeType = value; this.updateMenuView(requestItem, key, value); 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; requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value; this.updateMenuView(requestItem, key, value); this._registerLastRequestEnd(requestItem.attachment.endedMillis); break; case "eventTimings": @@ -1016,16 +1114,21 @@ RequestsMenuView.prototype = Heritage.ex } } // Since at least one timing box should've been rendered, unhide the // start and end timing cap nodes. 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(); }, /** * Rescales and redraws all the waterfall views in this container. * * @param boolean aReset @@ -1129,17 +1232,17 @@ RequestsMenuView.prototype = Heritage.ex normalizedTime /= 1000; divisionScale = "second"; } // Showing too many decimals is bad UX. if (divisionScale == "millisecond") { normalizedTime |= 0; } else { - normalizedTime = L10N.numberWithDecimals(normalizedTime, 2); + normalizedTime = L10N.numberWithDecimals(normalizedTime, REQUEST_TIME_DECIMALS); } let node = document.createElement("label"); let text = L10N.getFormatStr("networkMenu." + divisionScale, normalizedTime); node.className = "plain requests-menu-timings-division"; node.setAttribute("division-scale", divisionScale); node.style.transform = translateX; @@ -1258,16 +1361,21 @@ RequestsMenuView.prototype = Heritage.ex NetMonitorView.Sidebar.toggle(false); } }, /** * 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)); }, /** * Handle the context menu opening. Hide items if no request is selected. */ @@ -1448,17 +1556,17 @@ SidebarView.prototype = { populate: function(aData) { let isCustom = aData.isCustom; let view = isCustom ? NetMonitorView.CustomRequest : NetMonitorView.NetworkDetails; return view.populate(aData).then(() => { $("#details-pane").selectedIndex = isCustom ? 0 : 1 - window.emit(EVENTS.SIDEBAR_POPULATED) + window.emit(EVENTS.SIDEBAR_POPULATED); }); }, /** * Hides this container. */ reset: function() { this.toggle(false); @@ -1475,17 +1583,16 @@ function CustomRequestView() { CustomRequestView.prototype = { /** * Initialization function, called when the network monitor is started. */ initialize: function() { dumpn("Initializing the CustomRequestView"); this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this)); - $("#custom-pane").addEventListener("input", this.updateCustomRequestEvent, false); }, /** * Destruction function, called when the network monitor is closed. */ destroy: function() { dumpn("Destroying the CustomRequestView"); @@ -1550,28 +1657,22 @@ CustomRequestView.prototype = { let query = $("#custom-query-value").value; this.updateCustomUrl(query); field = 'url'; value = $("#custom-url-value").value selectedItem.attachment.url = value; 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; } NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value); }, /** * Update the query string field based on the url. @@ -2168,16 +2269,180 @@ NetworkDetailsView.prototype = { _paramsPostPayload: "", _requestHeaders: "", _responseHeaders: "", _requestCookies: "", _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. */ function $(aSelector, aTarget = document) aTarget.querySelector(aSelector); function $all(aSelector, aTarget = document) aTarget.querySelectorAll(aSelector); /** * Helper for getting an nsIURL instance out of a string. */ @@ -2189,18 +2454,18 @@ function nsIURL(aUrl, aStore = nsIURL.st aStore.set(aUrl, uri); return uri; } 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} */ function parseQueryString(aQueryString) { // Make sure there's at least one param available. if (!aQueryString || !aQueryString.contains("=")) { return; } @@ -2211,43 +2476,43 @@ function parseQueryString(aQueryString) value: NetworkHelper.convertToUnicode(unescape(param[1])) }); return paramsArray; } /** * 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} */ function parseHeaderText(aText) { return parseRequestText(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} */ function parseQueryText(aText) { return parseRequestText(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} */ function parseRequestText(aText, aDivider) { let regex = new RegExp("(.+?)\\" + aDivider + "\\s*(.+)"); let pairs = []; for (let line of aText.split("\n")) { let matches; @@ -2291,16 +2556,54 @@ function writeQueryText(aParams) { * @return string * Query string that can be appended to a url. */ 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. * * @param function callback * Function to execute execute when data-key is present in event.target. * @return function * Wrapped function with the target data-key as the first argument. */ function getKeyWithEvent(callback) { @@ -2315,8 +2618,9 @@ function getKeyWithEvent(callback) { /** * Preliminary setup for the NetMonitorView object. */ NetMonitorView.Toolbar = new ToolbarView(); NetMonitorView.RequestsMenu = new RequestsMenuView(); NetMonitorView.Sidebar = new SidebarView(); NetMonitorView.CustomRequest = new CustomRequestView(); NetMonitorView.NetworkDetails = new NetworkDetailsView(); +NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();
--- a/browser/devtools/netmonitor/netmonitor.css +++ b/browser/devtools/netmonitor/netmonitor.css @@ -7,51 +7,51 @@ overflow: hidden; } #details-pane-toggle[disabled] { /* Don't use display: none; to avoid collapsing #requests-menu-toolbar */ visibility: hidden; } -#response-content-image-box { +#custom-pane { overflow: auto; } -#custom-pane { +#response-content-image-box { overflow: auto; } #timings-summary-blocked { display: none; /* This doesn't work yet. */ } +#network-statistics-charts { + overflow: auto; +} + /* Responsive sidebar */ @media (max-width: 700px) { #toolbar-spacer, #details-pane-toggle, #details-pane[pane-collapsed], .requests-menu-waterfall, .requests-menu-footer-label { display: none; } } -@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; } #network-table[size-overflows] .requests-menu-size { display: none; }
--- a/browser/devtools/netmonitor/netmonitor.xul +++ b/browser/devtools/netmonitor/netmonitor.xul @@ -26,22 +26,26 @@ label="&netmonitorUI.context.newTab;" accesskey="&netmonitorUI.context.newTab.accesskey;"/> <menuitem id="request-menu-context-copy-url" label="&netmonitorUI.context.copyUrl;" accesskey="&netmonitorUI.context.copyUrl.accesskey;"/> <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" align="center"> <hbox id="toolbar-labels" flex="1"> <hbox id="requests-menu-status-and-method-header-box" class="requests-menu-header requests-menu-status-and-method" align="center"> @@ -113,19 +117,30 @@ </hbox> </hbox> <toolbarbutton id="details-pane-toggle" class="devtools-toolbarbutton" tooltiptext="&netmonitorUI.panesButton.tooltip;" 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" align="center"> <box class="requests-menu-status"/> <label class="plain requests-menu-method" crop="end" flex="1"/> @@ -146,80 +161,80 @@ <hbox class="start requests-menu-timings-cap" hidden="true"/> <hbox class="end requests-menu-timings-cap" hidden="true"/> <label class="plain requests-menu-timings-total"/> </hbox> </hbox> </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"> <vbox id="custom-pane" class="tabpanel-content"> <hbox align="baseline"> <label value="&netmonitorUI.custom.newRequest;" class="plain tabpanel-summary-label @@ -455,9 +470,29 @@ </hbox> </vbox> </tabpanel> </tabpanels> </tabbox> </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>
--- a/browser/devtools/netmonitor/test/browser.ini +++ b/browser/devtools/netmonitor/test/browser.ini @@ -10,27 +10,33 @@ support-files = html_json-long-test-page.html html_json-malformed-test-page.html html_jsonp-test-page.html html_navigate-test-page.html html_post-data-test-page.html 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 sjs_sorting-test-server.sjs sjs_status-codes-test-server.sjs test-image.png [browser_net_aaa_leaktest.js] [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] [browser_net_cyrillic-01.js] [browser_net_cyrillic-02.js] [browser_net_filter-01.js] [browser_net_filter-02.js] [browser_net_filter-03.js] @@ -52,11 +58,13 @@ support-files = [browser_net_resend.js] [browser_net_simple-init.js] [browser_net_simple-request-data.js] [browser_net_simple-request-details.js] [browser_net_simple-request.js] [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]
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-01.js @@ -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); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-02.js @@ -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); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-03.js @@ -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); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-04.js @@ -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); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-05.js @@ -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); + }); +}
--- a/browser/devtools/netmonitor/test/browser_net_footer-summary.js +++ b/browser/devtools/netmonitor/test/browser_net_footer-summary.js @@ -75,32 +75,26 @@ function test() { EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button")); testStatus(); teardown(aMonitor).then(finish); }) function testStatus() { - let summary = $("#request-menu-network-summary"); + let summary = $("#requests-menu-network-summary-label"); let value = summary.getAttribute("value"); info("Current summary: " + value); let visibleItems = RequestsMenu.visibleItems; let visibleRequestsCount = visibleItems.length; 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; } let totalBytes = RequestsMenu._getTotalBytesOfRequests(visibleItems); let totalMillis = RequestsMenu._getNewestRequest(visibleItems).attachment.endedMillis -
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_statistics-01.js @@ -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."); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_statistics-02.js @@ -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(); + }); +}
--- a/browser/devtools/netmonitor/test/head.js +++ b/browser/devtools/netmonitor/test/head.js @@ -24,16 +24,17 @@ const POST_RAW_URL = EXAMPLE_URL + "html const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html"; const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html"; const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html"; const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html"; 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"; const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs"; const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs"; const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/html_statistics-test-page.html @@ -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>
--- a/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs +++ b/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs @@ -2,128 +2,161 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ const { classes: Cc, interfaces: Ci } = Components; 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; } default: { response.setStatusLine(request.httpVersion, 404, "Not Found"); response.setHeader("Content-Type", "text/html; charset=utf-8", false); response.write("<blink>Not Found</blink>"); response.finish();
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/widgets/Chart.jsm @@ -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)); + } +}
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd @@ -6,19 +6,24 @@ <!-- LOCALIZATION NOTE : FILE Do not translate commandkey --> <!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to - keep it in English, or another language commonly spoken among web developers. - You want to make that choice consistent across the developer tools. - 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. --> <!ENTITY netmonitorUI.toolbar.status2 "✓"> <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.method): This is the label displayed - in the network table toolbar, above the "method" column. --> <!ENTITY netmonitorUI.toolbar.method "Method"> @@ -94,20 +99,28 @@ <!-- LOCALIZATION NOTE (debuggerUI.footer.filterMedia): This is the label displayed - in the network details footer for the "Media" filtering button. --> <!ENTITY netmonitorUI.footer.filterMedia "Media"> <!-- LOCALIZATION NOTE (debuggerUI.footer.filterFlash): This is the label displayed - 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"> <!-- LOCALIZATION NOTE (debuggerUI.summary.url): This is the label displayed - in the network details headers tab identifying the URL. --> <!ENTITY netmonitorUI.summary.url "Request URL:"> @@ -168,42 +181,50 @@ - in a "wait" state. --> <!ENTITY netmonitorUI.timings.wait "Waiting:"> <!-- LOCALIZATION NOTE (debuggerUI.timings.receive): This is the label displayed - in the network details timings tab identifying the amount of time spent - 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 - network container --> <!ENTITY netmonitorUI.context.newTab "Open in New Tab"> <!-- 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 --> <!ENTITY netmonitorUI.custom.newRequest "New Request"> <!-- LOCALIZATION NOTE (debuggerUI.custom.query): This is the label displayed - above the query string entry in the custom request form --> <!ENTITY netmonitorUI.custom.query "Query String:"> @@ -218,8 +239,12 @@ <!-- LOCALIZATION NOTE (debuggerUI.custom.send): This is the label displayed - on the button which sends the custom request --> <!ENTITY netmonitorUI.custom.send "Send"> <!-- 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">
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties +++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties @@ -130,8 +130,54 @@ networkMenu.millisecond=%S ms # LOCALIZATION NOTE (networkMenu.second): This is the label displayed # in the network menu specifying timing interval divisions (in seconds). 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
--- a/browser/themes/shared/devtools/netmonitor.inc.css +++ b/browser/themes/shared/devtools/netmonitor.inc.css @@ -1,27 +1,39 @@ /* vim:set ts=2 sw=2 sts=2 et: */ /* 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/. */ #requests-menu-empty-notice { margin: 0; padding: 12px; - font-size: 110%; + font-size: 120%; } .theme-dark #requests-menu-empty-notice { color: #f5f7fa; /* Light foreground text */ } .theme-light #requests-menu-empty-notice { 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) %define table_itemLightStartBorder rgba(128,128,128,0.25) %define table_itemLightEndBorder transparent /* Network requests table */ @@ -470,22 +482,21 @@ box.requests-menu-status { #details-pane-toggle:active { -moz-image-region: rect(0px,32px,16px,16px); } /* 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; } .tabpanel-summary-label { -moz-padding-start: 4px; -moz-padding-end: 3px; @@ -573,41 +584,41 @@ box.requests-menu-status { border-top: solid 1px hsla(210,5%,5%,.3); } .requests-menu-footer-button, .requests-menu-footer-label { min-width: 1em; margin: 0; border: none; - padding: 2px 1.5vw; + padding: 2px 0.75vw; } .theme-dark .requests-menu-footer-button, .theme-dark .requests-menu-footer-label { color: #f5f7fa; /* Light foreground text */ } .theme-light .requests-menu-footer-button, .theme-light .requests-menu-footer-label { color: #18191a; /* Dark foreground text */ } .requests-menu-footer-spacer { 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@; } .requests-menu-footer-button { -moz-appearance: none; background: rgba(0,0,0,0.025); } @@ -623,33 +634,202 @@ box.requests-menu-status { .requests-menu-footer-button:not(:active)[checked] { background-color: rgba(0,0,0,0.25); background-image: radial-gradient(farthest-side at center top, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3)); background-size: 100% 1px; background-repeat: no-repeat; } .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 { height: 22px; } .requests-menu-header-button { min-height: 20px; } .requests-menu-footer-button, .requests-menu-footer-label { - padding: 2px 2vw; + padding: 2px 1vw; } #details-pane { max-width: none; margin: 0 !important; /* To prevent all the margin hacks to hide the sidebar. */ }
--- a/browser/themes/shared/devtools/toolbars.inc.css +++ b/browser/themes/shared/devtools/toolbars.inc.css @@ -718,17 +718,19 @@ .theme-light .command-button > image, .theme-light .command-button:active > image, .theme-light .devtools-closebutton > image, .theme-light .devtools-toolbarbutton > image, .theme-light .devtools-option-toolbarbutton > image, .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); } /* Since selected backgrounds are blue, we want to use the normal * (light) icons. */ .theme-light .command-button[checked=true]:not(:active) > image, .theme-light .devtools-tab[selected] > image, .theme-light .devtools-tab[highlighted] > image,
--- a/browser/themes/shared/devtools/widgets.inc.css +++ b/browser/themes/shared/devtools/widgets.inc.css @@ -787,9 +787,150 @@ .arrow[open] { -moz-appearance: treetwistyopen; } .arrow[invisible] { 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
--- a/toolkit/devtools/server/actors/webbrowser.js +++ b/toolkit/devtools/server/actors/webbrowser.js @@ -776,17 +776,22 @@ BrowserTabActor.prototype = { reload = true; } if (typeof options.cacheEnabled !== "undefined" && options.cacheEnabled !== this._getCacheEnabled()) { this._setCacheEnabled(options.cacheEnabled); 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(); } }, /** * Disable or enable the cache via docShell. */ _setCacheEnabled: function(allow) {