performance-statistics-view.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. /* import-globals-from ./netmonitor-controller.js */
  6. /* globals $ */
  7. "use strict";
  8. const {PluralForm} = require("devtools/shared/plural-form");
  9. const {Filters} = require("./filter-predicates");
  10. const {L10N} = require("./l10n");
  11. const Actions = require("./actions/index");
  12. const REQUEST_TIME_DECIMALS = 2;
  13. const CONTENT_SIZE_DECIMALS = 2;
  14. // px
  15. const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
  16. /**
  17. * Functions handling the performance statistics view.
  18. */
  19. function PerformanceStatisticsView() {
  20. }
  21. PerformanceStatisticsView.prototype = {
  22. /**
  23. * Initialization function, called when the debugger is started.
  24. */
  25. initialize: function (store) {
  26. this.store = store;
  27. },
  28. /**
  29. * Initializes and displays empty charts in this container.
  30. */
  31. displayPlaceholderCharts: function () {
  32. this._createChart({
  33. id: "#primed-cache-chart",
  34. title: "charts.cacheEnabled"
  35. });
  36. this._createChart({
  37. id: "#empty-cache-chart",
  38. title: "charts.cacheDisabled"
  39. });
  40. window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED);
  41. },
  42. /**
  43. * Populates and displays the primed cache chart in this container.
  44. *
  45. * @param array items
  46. * @see this._sanitizeChartDataSource
  47. */
  48. createPrimedCacheChart: function (items) {
  49. this._createChart({
  50. id: "#primed-cache-chart",
  51. title: "charts.cacheEnabled",
  52. data: this._sanitizeChartDataSource(items),
  53. strings: this._commonChartStrings,
  54. totals: this._commonChartTotals,
  55. sorted: true
  56. });
  57. window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED);
  58. },
  59. /**
  60. * Populates and displays the empty cache chart in this container.
  61. *
  62. * @param array items
  63. * @see this._sanitizeChartDataSource
  64. */
  65. createEmptyCacheChart: function (items) {
  66. this._createChart({
  67. id: "#empty-cache-chart",
  68. title: "charts.cacheDisabled",
  69. data: this._sanitizeChartDataSource(items, true),
  70. strings: this._commonChartStrings,
  71. totals: this._commonChartTotals,
  72. sorted: true
  73. });
  74. window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED);
  75. },
  76. /**
  77. * Common stringifier predicates used for items and totals in both the
  78. * "primed" and "empty" cache charts.
  79. */
  80. _commonChartStrings: {
  81. size: value => {
  82. let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS);
  83. return L10N.getFormatStr("charts.sizeKB", string);
  84. },
  85. transferredSize: value => {
  86. let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS);
  87. return L10N.getFormatStr("charts.transferredSizeKB", string);
  88. },
  89. time: value => {
  90. let string = L10N.numberWithDecimals(value / 1000, REQUEST_TIME_DECIMALS);
  91. return L10N.getFormatStr("charts.totalS", string);
  92. }
  93. },
  94. _commonChartTotals: {
  95. cached: total => {
  96. return L10N.getFormatStr("charts.totalCached", total);
  97. },
  98. count: total => {
  99. return L10N.getFormatStr("charts.totalCount", total);
  100. },
  101. size: total => {
  102. let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS);
  103. return L10N.getFormatStr("charts.totalSize", string);
  104. },
  105. transferredSize: total => {
  106. let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS);
  107. return L10N.getFormatStr("charts.totalTransferredSize", string);
  108. },
  109. time: total => {
  110. let seconds = total / 1000;
  111. let string = L10N.numberWithDecimals(seconds, REQUEST_TIME_DECIMALS);
  112. return PluralForm.get(seconds,
  113. L10N.getStr("charts.totalSeconds")).replace("#1", string);
  114. }
  115. },
  116. /**
  117. * Adds a specific chart to this container.
  118. *
  119. * @param object
  120. * An object containing all or some the following properties:
  121. * - id: either "#primed-cache-chart" or "#empty-cache-chart"
  122. * - title/data/strings/totals/sorted: @see Chart.jsm for details
  123. */
  124. _createChart: function ({ id, title, data, strings, totals, sorted }) {
  125. let container = $(id);
  126. // Nuke all existing charts of the specified type.
  127. while (container.hasChildNodes()) {
  128. container.firstChild.remove();
  129. }
  130. // Create a new chart.
  131. let chart = Chart.PieTable(document, {
  132. diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
  133. title: L10N.getStr(title),
  134. header: {
  135. cached: "",
  136. count: "",
  137. label: L10N.getStr("charts.type"),
  138. size: L10N.getStr("charts.size"),
  139. transferredSize: L10N.getStr("charts.transferred"),
  140. time: L10N.getStr("charts.time")
  141. },
  142. data: data,
  143. strings: strings,
  144. totals: totals,
  145. sorted: sorted
  146. });
  147. chart.on("click", (_, item) => {
  148. // Reset FilterButtons and enable one filter exclusively
  149. this.store.dispatch(Actions.enableFilterTypeOnly(item.label));
  150. NetMonitorView.showNetworkInspectorView();
  151. });
  152. container.appendChild(chart.node);
  153. },
  154. /**
  155. * Sanitizes the data source used for creating charts, to follow the
  156. * data format spec defined in Chart.jsm.
  157. *
  158. * @param array items
  159. * A collection of request items used as the data source for the chart.
  160. * @param boolean emptyCache
  161. * True if the cache is considered enabled, false for disabled.
  162. */
  163. _sanitizeChartDataSource: function (items, emptyCache) {
  164. const data = [
  165. "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
  166. ].map((type) => ({
  167. cached: 0,
  168. count: 0,
  169. label: type,
  170. size: 0,
  171. transferredSize: 0,
  172. time: 0
  173. }));
  174. for (let requestItem of items) {
  175. let details = requestItem.attachment;
  176. let type;
  177. if (Filters.html(details)) {
  178. // "html"
  179. type = 0;
  180. } else if (Filters.css(details)) {
  181. // "css"
  182. type = 1;
  183. } else if (Filters.js(details)) {
  184. // "js"
  185. type = 2;
  186. } else if (Filters.fonts(details)) {
  187. // "fonts"
  188. type = 4;
  189. } else if (Filters.images(details)) {
  190. // "images"
  191. type = 5;
  192. } else if (Filters.media(details)) {
  193. // "media"
  194. type = 6;
  195. } else if (Filters.flash(details)) {
  196. // "flash"
  197. type = 7;
  198. } else if (Filters.ws(details)) {
  199. // "ws"
  200. type = 8;
  201. } else if (Filters.xhr(details)) {
  202. // Verify XHR last, to categorize other mime types in their own blobs.
  203. // "xhr"
  204. type = 3;
  205. } else {
  206. // "other"
  207. type = 9;
  208. }
  209. if (emptyCache || !responseIsFresh(details)) {
  210. data[type].time += details.totalTime || 0;
  211. data[type].size += details.contentSize || 0;
  212. data[type].transferredSize += details.transferredSize || 0;
  213. } else {
  214. data[type].cached++;
  215. }
  216. data[type].count++;
  217. }
  218. return data.filter(e => e.count > 0);
  219. },
  220. };
  221. /**
  222. * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
  223. * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
  224. *
  225. * @param object
  226. * An object containing the { responseHeaders, status } properties.
  227. * @return boolean
  228. * True if the response is fresh and loaded from cache.
  229. */
  230. function responseIsFresh({ responseHeaders, status }) {
  231. // Check for a "304 Not Modified" status and response headers availability.
  232. if (status != 304 || !responseHeaders) {
  233. return false;
  234. }
  235. let list = responseHeaders.headers;
  236. let cacheControl = list.filter(e => {
  237. return e.name.toLowerCase() == "cache-control";
  238. })[0];
  239. let expires = list.filter(e => e.name.toLowerCase() == "expires")[0];
  240. // Check the "Cache-Control" header for a maximum age value.
  241. if (cacheControl) {
  242. let maxAgeMatch =
  243. cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
  244. cacheControl.value.match(/max-age\s*=\s*(\d+)/);
  245. if (maxAgeMatch && maxAgeMatch.pop() > 0) {
  246. return true;
  247. }
  248. }
  249. // Check the "Expires" header for a valid date.
  250. if (expires && Date.parse(expires.value)) {
  251. return true;
  252. }
  253. return false;
  254. }
  255. exports.PerformanceStatisticsView = PerformanceStatisticsView;