requests-menu-view.js 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. /* globals document, window, dumpn, $, gNetwork, EVENTS, Prefs,
  5. NetMonitorController, NetMonitorView */
  6. "use strict";
  7. /* eslint-disable mozilla/reject-some-requires */
  8. const { Cu } = require("chrome");
  9. const {Task} = require("devtools/shared/task");
  10. const {DeferredTask} = Cu.import("resource://gre/modules/DeferredTask.jsm", {});
  11. /* eslint-disable mozilla/reject-some-requires */
  12. const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
  13. const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
  14. const {setImageTooltip, getImageDimensions} =
  15. require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
  16. const {Heritage, WidgetMethods, setNamedTimeout} =
  17. require("devtools/client/shared/widgets/view-helpers");
  18. const {CurlUtils} = require("devtools/client/shared/curl");
  19. const {Filters, isFreetextMatch} = require("./filter-predicates");
  20. const {Sorters} = require("./sort-predicates");
  21. const {L10N, WEBCONSOLE_L10N} = require("./l10n");
  22. const {formDataURI,
  23. writeHeaderText,
  24. getKeyWithEvent,
  25. getAbbreviatedMimeType,
  26. getUriNameWithQuery,
  27. getUriHostPort,
  28. getUriHost,
  29. loadCauseString} = require("./request-utils");
  30. const Actions = require("./actions/index");
  31. const RequestListContextMenu = require("./request-list-context-menu");
  32. loader.lazyRequireGetter(this, "NetworkHelper",
  33. "devtools/shared/webconsole/network-helper");
  34. const HTML_NS = "http://www.w3.org/1999/xhtml";
  35. const EPSILON = 0.001;
  36. // ms
  37. const RESIZE_REFRESH_RATE = 50;
  38. // ms
  39. const REQUESTS_REFRESH_RATE = 50;
  40. // tooltip show/hide delay in ms
  41. const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
  42. // px
  43. const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
  44. // px
  45. const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
  46. // px
  47. const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
  48. // ms
  49. const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
  50. // px
  51. const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
  52. // ms
  53. const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5;
  54. const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
  55. // px
  56. const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10;
  57. const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
  58. const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32;
  59. // byte
  60. const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32;
  61. const REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA = [255, 0, 0, 128];
  62. const REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA = [0, 0, 255, 128];
  63. // Constants for formatting bytes.
  64. const BYTES_IN_KB = 1024;
  65. const BYTES_IN_MB = Math.pow(BYTES_IN_KB, 2);
  66. const BYTES_IN_GB = Math.pow(BYTES_IN_KB, 3);
  67. const MAX_BYTES_SIZE = 1000;
  68. const MAX_KB_SIZE = 1000 * BYTES_IN_KB;
  69. const MAX_MB_SIZE = 1000 * BYTES_IN_MB;
  70. // TODO: duplicated from netmonitor-view.js. Move to a format-utils.js module.
  71. const REQUEST_TIME_DECIMALS = 2;
  72. const CONTENT_SIZE_DECIMALS = 2;
  73. const CONTENT_MIME_TYPE_ABBREVIATIONS = {
  74. "ecmascript": "js",
  75. "javascript": "js",
  76. "x-javascript": "js"
  77. };
  78. // A smart store watcher to notify store changes as necessary
  79. function storeWatcher(initialValue, reduceValue, onChange) {
  80. let currentValue = initialValue;
  81. return () => {
  82. const oldValue = currentValue;
  83. const newValue = reduceValue(currentValue);
  84. if (newValue !== oldValue) {
  85. currentValue = newValue;
  86. onChange(newValue, oldValue);
  87. }
  88. };
  89. }
  90. /**
  91. * Functions handling the requests menu (containing details about each request,
  92. * like status, method, file, domain, as well as a waterfall representing
  93. * timing imformation).
  94. */
  95. function RequestsMenuView() {
  96. dumpn("RequestsMenuView was instantiated");
  97. this._flushRequests = this._flushRequests.bind(this);
  98. this._onHover = this._onHover.bind(this);
  99. this._onSelect = this._onSelect.bind(this);
  100. this._onSwap = this._onSwap.bind(this);
  101. this._onResize = this._onResize.bind(this);
  102. this._onScroll = this._onScroll.bind(this);
  103. this._onSecurityIconClick = this._onSecurityIconClick.bind(this);
  104. }
  105. RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
  106. /**
  107. * Initialization function, called when the network monitor is started.
  108. */
  109. initialize: function (store) {
  110. dumpn("Initializing the RequestsMenuView");
  111. this.store = store;
  112. this.contextMenu = new RequestListContextMenu();
  113. let widgetParentEl = $("#requests-menu-contents");
  114. this.widget = new SideMenuWidget(widgetParentEl);
  115. this._splitter = $("#network-inspector-view-splitter");
  116. // Create a tooltip for the newly appended network request item.
  117. this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
  118. this.tooltip.startTogglingOnHover(widgetParentEl, this._onHover, {
  119. toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
  120. interactive: true
  121. });
  122. this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
  123. this.allowFocusOnRightClick = true;
  124. this.maintainSelectionVisible = true;
  125. this.widget.addEventListener("select", this._onSelect, false);
  126. this.widget.addEventListener("swap", this._onSwap, false);
  127. this._splitter.addEventListener("mousemove", this._onResize, false);
  128. window.addEventListener("resize", this._onResize, false);
  129. this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
  130. this.requestsMenuSortKeyboardEvent = getKeyWithEvent(this.sortBy.bind(this), true);
  131. this._onContextMenu = this._onContextMenu.bind(this);
  132. this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
  133. this._onReloadCommand = () => NetMonitorView.reloadPage();
  134. this._flushRequestsTask = new DeferredTask(this._flushRequests,
  135. REQUESTS_REFRESH_RATE);
  136. this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
  137. this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
  138. this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
  139. this.toggleRawHeadersEvent = this.toggleRawHeaders.bind(this);
  140. this.reFilterRequests = this.reFilterRequests.bind(this);
  141. $("#toolbar-labels").addEventListener("click",
  142. this.requestsMenuSortEvent, false);
  143. $("#toolbar-labels").addEventListener("keydown",
  144. this.requestsMenuSortKeyboardEvent, false);
  145. $("#toggle-raw-headers").addEventListener("click",
  146. this.toggleRawHeadersEvent, false);
  147. $("#requests-menu-contents").addEventListener("scroll", this._onScroll, true);
  148. $("#requests-menu-contents").addEventListener("contextmenu", this._onContextMenu);
  149. this.unsubscribeStore = store.subscribe(storeWatcher(
  150. null,
  151. () => store.getState().filters,
  152. (newFilters) => {
  153. this._activeFilters = newFilters.types
  154. .toSeq()
  155. .filter((checked, key) => checked)
  156. .keySeq()
  157. .toArray();
  158. this._currentFreetextFilter = newFilters.url;
  159. this.reFilterRequests();
  160. }
  161. ));
  162. Prefs.filters.forEach(type =>
  163. store.dispatch(Actions.toggleFilterType(type)));
  164. window.once("connected", this._onConnect.bind(this));
  165. },
  166. _onConnect: function () {
  167. $("#requests-menu-reload-notice-button").addEventListener("command",
  168. this._onReloadCommand, false);
  169. if (NetMonitorController.supportsCustomRequest) {
  170. $("#custom-request-send-button").addEventListener("click",
  171. this.sendCustomRequestEvent, false);
  172. $("#custom-request-close-button").addEventListener("click",
  173. this.closeCustomRequestEvent, false);
  174. $("#headers-summary-resend").addEventListener("click",
  175. this.cloneSelectedRequestEvent, false);
  176. } else {
  177. $("#headers-summary-resend").hidden = true;
  178. }
  179. if (NetMonitorController.supportsPerfStats) {
  180. $("#requests-menu-perf-notice-button").addEventListener("command",
  181. this._onContextPerfCommand, false);
  182. $("#network-statistics-back-button").addEventListener("command",
  183. this._onContextPerfCommand, false);
  184. } else {
  185. $("#notice-perf-message").hidden = true;
  186. }
  187. if (!NetMonitorController.supportsTransferredResponseSize) {
  188. $("#requests-menu-transferred-header-box").hidden = true;
  189. $("#requests-menu-item-template .requests-menu-transferred")
  190. .hidden = true;
  191. }
  192. },
  193. /**
  194. * Destruction function, called when the network monitor is closed.
  195. */
  196. destroy: function () {
  197. dumpn("Destroying the RequestsMenuView");
  198. Prefs.filters = this._activeFilters;
  199. /* Destroy the tooltip */
  200. this.tooltip.stopTogglingOnHover();
  201. this.tooltip.destroy();
  202. $("#requests-menu-contents").removeEventListener("scroll", this._onScroll, true);
  203. $("#requests-menu-contents").removeEventListener("contextmenu", this._onContextMenu);
  204. this.widget.removeEventListener("select", this._onSelect, false);
  205. this.widget.removeEventListener("swap", this._onSwap, false);
  206. this._splitter.removeEventListener("mousemove", this._onResize, false);
  207. window.removeEventListener("resize", this._onResize, false);
  208. $("#toolbar-labels").removeEventListener("click",
  209. this.requestsMenuSortEvent, false);
  210. $("#toolbar-labels").removeEventListener("keydown",
  211. this.requestsMenuSortKeyboardEvent, false);
  212. this._flushRequestsTask.disarm();
  213. $("#requests-menu-reload-notice-button").removeEventListener("command",
  214. this._onReloadCommand, false);
  215. $("#requests-menu-perf-notice-button").removeEventListener("command",
  216. this._onContextPerfCommand, false);
  217. $("#network-statistics-back-button").removeEventListener("command",
  218. this._onContextPerfCommand, false);
  219. $("#custom-request-send-button").removeEventListener("click",
  220. this.sendCustomRequestEvent, false);
  221. $("#custom-request-close-button").removeEventListener("click",
  222. this.closeCustomRequestEvent, false);
  223. $("#headers-summary-resend").removeEventListener("click",
  224. this.cloneSelectedRequestEvent, false);
  225. $("#toggle-raw-headers").removeEventListener("click",
  226. this.toggleRawHeadersEvent, false);
  227. this.unsubscribeStore();
  228. },
  229. /**
  230. * Resets this container (removes all the networking information).
  231. */
  232. reset: function () {
  233. this.empty();
  234. this._addQueue = [];
  235. this._updateQueue = [];
  236. this._firstRequestStartedMillis = -1;
  237. this._lastRequestEndedMillis = -1;
  238. this.resetNotPersistent();
  239. },
  240. /**
  241. * Reset informations that "devtools.webconsole.persistlog == true".
  242. */
  243. resetNotPersistent: function () {
  244. this._firstRequestStartedMillisNotPersistent = -1;
  245. },
  246. /**
  247. * Specifies if this view may be updated lazily.
  248. */
  249. _lazyUpdate: true,
  250. get lazyUpdate() {
  251. return this._lazyUpdate;
  252. },
  253. set lazyUpdate(value) {
  254. this._lazyUpdate = value;
  255. if (!value) {
  256. this._flushRequests();
  257. }
  258. },
  259. /**
  260. * Adds a network request to this container.
  261. *
  262. * @param string id
  263. * An identifier coming from the network monitor controller.
  264. * @param string startedDateTime
  265. * A string representation of when the request was started, which
  266. * can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
  267. * @param string method
  268. * Specifies the request method (e.g. "GET", "POST", etc.)
  269. * @param string url
  270. * Specifies the request's url.
  271. * @param boolean isXHR
  272. * True if this request was initiated via XHR.
  273. * @param object cause
  274. * Specifies the request's cause. Has the following properties:
  275. * - type: nsContentPolicyType constant
  276. * - loadingDocumentUri: URI of the request origin
  277. * - stacktrace: JS stacktrace of the request
  278. * @param boolean fromCache
  279. * Indicates if the result came from the browser cache
  280. * @param boolean fromServiceWorker
  281. * Indicates if the request has been intercepted by a Service Worker
  282. */
  283. addRequest: function (id, startedDateTime, method, url, isXHR, cause,
  284. fromCache, fromServiceWorker) {
  285. this._addQueue.push([id, startedDateTime, method, url, isXHR, cause,
  286. fromCache, fromServiceWorker]);
  287. // Lazy updating is disabled in some tests.
  288. if (!this.lazyUpdate) {
  289. return void this._flushRequests();
  290. }
  291. this._flushRequestsTask.arm();
  292. return undefined;
  293. },
  294. /**
  295. * Create a new custom request form populated with the data from
  296. * the currently selected request.
  297. */
  298. cloneSelectedRequest: function () {
  299. let selected = this.selectedItem.attachment;
  300. // Create the element node for the network request item.
  301. let menuView = this._createMenuView(selected.method, selected.url,
  302. selected.cause);
  303. // Append a network request item to this container.
  304. let newItem = this.push([menuView], {
  305. attachment: Object.create(selected, {
  306. isCustom: { value: true }
  307. })
  308. });
  309. // Immediately switch to new request pane.
  310. this.selectedItem = newItem;
  311. },
  312. /**
  313. * Send a new HTTP request using the data in the custom request form.
  314. */
  315. sendCustomRequest: function () {
  316. let selected = this.selectedItem.attachment;
  317. let data = {
  318. url: selected.url,
  319. method: selected.method,
  320. httpVersion: selected.httpVersion,
  321. };
  322. if (selected.requestHeaders) {
  323. data.headers = selected.requestHeaders.headers;
  324. }
  325. if (selected.requestPostData) {
  326. data.body = selected.requestPostData.postData.text;
  327. }
  328. NetMonitorController.webConsoleClient.sendHTTPRequest(data, response => {
  329. let id = response.eventActor.actor;
  330. this._preferredItemId = id;
  331. });
  332. this.closeCustomRequest();
  333. },
  334. /**
  335. * Remove the currently selected custom request.
  336. */
  337. closeCustomRequest: function () {
  338. this.remove(this.selectedItem);
  339. NetMonitorView.Sidebar.toggle(false);
  340. },
  341. /**
  342. * Shows raw request/response headers in textboxes.
  343. */
  344. toggleRawHeaders: function () {
  345. let requestTextarea = $("#raw-request-headers-textarea");
  346. let responseTextare = $("#raw-response-headers-textarea");
  347. let rawHeadersHidden = $("#raw-headers").getAttribute("hidden");
  348. if (rawHeadersHidden) {
  349. let selected = this.selectedItem.attachment;
  350. let selectedRequestHeaders = selected.requestHeaders.headers;
  351. // display Status-Line above other response headers
  352. let selectedStatusLine = selected.httpVersion
  353. + " " + selected.status
  354. + " " + selected.statusText
  355. + "\n";
  356. requestTextarea.value = writeHeaderText(selectedRequestHeaders);
  357. // sometimes it's empty
  358. if (selected.responseHeaders) {
  359. let selectedResponseHeaders = selected.responseHeaders.headers;
  360. responseTextare.value = selectedStatusLine
  361. + writeHeaderText(selectedResponseHeaders);
  362. } else {
  363. responseTextare.value = selectedStatusLine;
  364. }
  365. $("#raw-headers").hidden = false;
  366. } else {
  367. requestTextarea.value = null;
  368. responseTextare.value = null;
  369. $("#raw-headers").hidden = true;
  370. }
  371. },
  372. /**
  373. * Refreshes the view contents with the newly selected filters
  374. */
  375. reFilterRequests: function () {
  376. this.filterContents(this._filterPredicate);
  377. this.updateRequests();
  378. this.refreshZebra();
  379. },
  380. /**
  381. * Returns a predicate that can be used to test if a request matches any of
  382. * the active filters.
  383. */
  384. get _filterPredicate() {
  385. let currentFreetextFilter = this._currentFreetextFilter;
  386. return requestItem => {
  387. const { attachment } = requestItem;
  388. return this._activeFilters.some(filterName => Filters[filterName](attachment)) &&
  389. isFreetextMatch(attachment, currentFreetextFilter);
  390. };
  391. },
  392. /**
  393. * Sorts all network requests in this container by a specified detail.
  394. *
  395. * @param string type
  396. * Either "status", "method", "file", "domain", "type", "transferred",
  397. * "size" or "waterfall".
  398. */
  399. sortBy: function (type = "waterfall") {
  400. let target = $("#requests-menu-" + type + "-button");
  401. let headers = document.querySelectorAll(".requests-menu-header-button");
  402. for (let header of headers) {
  403. if (header != target) {
  404. header.removeAttribute("sorted");
  405. header.removeAttribute("tooltiptext");
  406. header.parentNode.removeAttribute("active");
  407. }
  408. }
  409. let direction = "";
  410. if (target) {
  411. if (target.getAttribute("sorted") == "ascending") {
  412. target.setAttribute("sorted", direction = "descending");
  413. target.setAttribute("tooltiptext",
  414. L10N.getStr("networkMenu.sortedDesc"));
  415. } else {
  416. target.setAttribute("sorted", direction = "ascending");
  417. target.setAttribute("tooltiptext",
  418. L10N.getStr("networkMenu.sortedAsc"));
  419. }
  420. // Used to style the next column.
  421. target.parentNode.setAttribute("active", "true");
  422. }
  423. // Sort by whatever was requested.
  424. switch (type) {
  425. case "status":
  426. if (direction == "ascending") {
  427. this.sortContents((a, b) => Sorters.status(a.attachment, b.attachment));
  428. } else {
  429. this.sortContents((a, b) => -Sorters.status(a.attachment, b.attachment));
  430. }
  431. break;
  432. case "method":
  433. if (direction == "ascending") {
  434. this.sortContents((a, b) => Sorters.method(a.attachment, b.attachment));
  435. } else {
  436. this.sortContents((a, b) => -Sorters.method(a.attachment, b.attachment));
  437. }
  438. break;
  439. case "file":
  440. if (direction == "ascending") {
  441. this.sortContents((a, b) => Sorters.file(a.attachment, b.attachment));
  442. } else {
  443. this.sortContents((a, b) => -Sorters.file(a.attachment, b.attachment));
  444. }
  445. break;
  446. case "domain":
  447. if (direction == "ascending") {
  448. this.sortContents((a, b) => Sorters.domain(a.attachment, b.attachment));
  449. } else {
  450. this.sortContents((a, b) => -Sorters.domain(a.attachment, b.attachment));
  451. }
  452. break;
  453. case "cause":
  454. if (direction == "ascending") {
  455. this.sortContents((a, b) => Sorters.cause(a.attachment, b.attachment));
  456. } else {
  457. this.sortContents((a, b) => -Sorters.cause(a.attachment, b.attachment));
  458. }
  459. break;
  460. case "type":
  461. if (direction == "ascending") {
  462. this.sortContents((a, b) => Sorters.type(a.attachment, b.attachment));
  463. } else {
  464. this.sortContents((a, b) => -Sorters.type(a.attachment, b.attachment));
  465. }
  466. break;
  467. case "transferred":
  468. if (direction == "ascending") {
  469. this.sortContents((a, b) => Sorters.transferred(a.attachment, b.attachment));
  470. } else {
  471. this.sortContents((a, b) => -Sorters.transferred(a.attachment, b.attachment));
  472. }
  473. break;
  474. case "size":
  475. if (direction == "ascending") {
  476. this.sortContents((a, b) => Sorters.size(a.attachment, b.attachment));
  477. } else {
  478. this.sortContents((a, b) => -Sorters.size(a.attachment, b.attachment));
  479. }
  480. break;
  481. case "waterfall":
  482. if (direction == "ascending") {
  483. this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
  484. } else {
  485. this.sortContents((a, b) => -Sorters.waterfall(a.attachment, b.attachment));
  486. }
  487. break;
  488. }
  489. this.updateRequests();
  490. this.refreshZebra();
  491. },
  492. /**
  493. * Removes all network requests and closes the sidebar if open.
  494. */
  495. clear: function () {
  496. NetMonitorController.NetworkEventsHandler.clearMarkers();
  497. NetMonitorView.Sidebar.toggle(false);
  498. $("#requests-menu-empty-notice").hidden = false;
  499. this.empty();
  500. this.updateRequests();
  501. },
  502. /**
  503. * Update store request itmes and trigger related UI update
  504. */
  505. updateRequests: function () {
  506. this.store.dispatch(Actions.updateRequests(this.visibleItems));
  507. },
  508. /**
  509. * Adds odd/even attributes to all the visible items in this container.
  510. */
  511. refreshZebra: function () {
  512. let visibleItems = this.visibleItems;
  513. for (let i = 0, len = visibleItems.length; i < len; i++) {
  514. let requestItem = visibleItems[i];
  515. let requestTarget = requestItem.target;
  516. if (i % 2 == 0) {
  517. requestTarget.setAttribute("even", "");
  518. requestTarget.removeAttribute("odd");
  519. } else {
  520. requestTarget.setAttribute("odd", "");
  521. requestTarget.removeAttribute("even");
  522. }
  523. }
  524. },
  525. /**
  526. * Attaches security icon click listener for the given request menu item.
  527. *
  528. * @param object item
  529. * The network request item to attach the listener to.
  530. */
  531. attachSecurityIconClickListener: function ({ target }) {
  532. let icon = $(".requests-security-state-icon", target);
  533. icon.addEventListener("click", this._onSecurityIconClick);
  534. },
  535. /**
  536. * Schedules adding additional information to a network request.
  537. *
  538. * @param string id
  539. * An identifier coming from the network monitor controller.
  540. * @param object data
  541. * An object containing several { key: value } tuples of network info.
  542. * Supported keys are "httpVersion", "status", "statusText" etc.
  543. * @param function callback
  544. * A function to call once the request has been updated in the view.
  545. */
  546. updateRequest: function (id, data, callback) {
  547. this._updateQueue.push([id, data, callback]);
  548. // Lazy updating is disabled in some tests.
  549. if (!this.lazyUpdate) {
  550. return void this._flushRequests();
  551. }
  552. this._flushRequestsTask.arm();
  553. return undefined;
  554. },
  555. /**
  556. * Starts adding all queued additional information about network requests.
  557. */
  558. _flushRequests: function () {
  559. // Prevent displaying any updates received after the target closed.
  560. if (NetMonitorView._isDestroyed) {
  561. return;
  562. }
  563. let widget = NetMonitorView.RequestsMenu.widget;
  564. let isScrolledToBottom = widget.isScrolledToBottom();
  565. for (let [id, startedDateTime, method, url, isXHR, cause, fromCache,
  566. fromServiceWorker] of this._addQueue) {
  567. // Convert the received date/time string to a unix timestamp.
  568. let unixTime = Date.parse(startedDateTime);
  569. // Create the element node for the network request item.
  570. let menuView = this._createMenuView(method, url, cause);
  571. // Remember the first and last event boundaries.
  572. this._registerFirstRequestStart(unixTime);
  573. this._registerLastRequestEnd(unixTime);
  574. // Append a network request item to this container.
  575. let requestItem = this.push([menuView, id], {
  576. attachment: {
  577. firstRequestStartedMillisNotPersistent: this._firstRequestStartedMillisNotPersistent,
  578. startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
  579. startedMillis: unixTime,
  580. method: method,
  581. url: url,
  582. isXHR: isXHR,
  583. cause: cause,
  584. fromCache: fromCache,
  585. fromServiceWorker: fromServiceWorker
  586. }
  587. });
  588. if (id == this._preferredItemId) {
  589. this.selectedItem = requestItem;
  590. }
  591. window.emit(EVENTS.REQUEST_ADDED, id);
  592. }
  593. if (isScrolledToBottom && this._addQueue.length) {
  594. widget.scrollToBottom();
  595. }
  596. // For each queued additional information packet, get the corresponding
  597. // request item in the view and update it based on the specified data.
  598. for (let [id, data, callback] of this._updateQueue) {
  599. let requestItem = this.getItemByValue(id);
  600. if (!requestItem) {
  601. // Packet corresponds to a dead request item, target navigated.
  602. continue;
  603. }
  604. // Each information packet may contain several { key: value } tuples of
  605. // network info, so update the view based on each one.
  606. for (let key in data) {
  607. let val = data[key];
  608. if (val === undefined) {
  609. // The information in the packet is empty, it can be safely ignored.
  610. continue;
  611. }
  612. switch (key) {
  613. case "requestHeaders":
  614. requestItem.attachment.requestHeaders = val;
  615. break;
  616. case "requestCookies":
  617. requestItem.attachment.requestCookies = val;
  618. break;
  619. case "requestPostData":
  620. // Search the POST data upload stream for request headers and add
  621. // them to a separate store, different from the classic headers.
  622. // XXX: Be really careful here! We're creating a function inside
  623. // a loop, so remember the actual request item we want to modify.
  624. let currentItem = requestItem;
  625. let currentStore = { headers: [], headersSize: 0 };
  626. Task.spawn(function* () {
  627. let postData = yield gNetwork.getString(val.postData.text);
  628. let payloadHeaders = CurlUtils.getHeadersFromMultipartText(
  629. postData);
  630. currentStore.headers = payloadHeaders;
  631. currentStore.headersSize = payloadHeaders.reduce(
  632. (acc, { name, value }) =>
  633. acc + name.length + value.length + 2, 0);
  634. // The `getString` promise is async, so we need to refresh the
  635. // information displayed in the network details pane again here.
  636. refreshNetworkDetailsPaneIfNecessary(currentItem);
  637. });
  638. requestItem.attachment.requestPostData = val;
  639. requestItem.attachment.requestHeadersFromUploadStream =
  640. currentStore;
  641. break;
  642. case "securityState":
  643. requestItem.attachment.securityState = val;
  644. this.updateMenuView(requestItem, key, val);
  645. break;
  646. case "securityInfo":
  647. requestItem.attachment.securityInfo = val;
  648. break;
  649. case "responseHeaders":
  650. requestItem.attachment.responseHeaders = val;
  651. break;
  652. case "responseCookies":
  653. requestItem.attachment.responseCookies = val;
  654. break;
  655. case "httpVersion":
  656. requestItem.attachment.httpVersion = val;
  657. break;
  658. case "remoteAddress":
  659. requestItem.attachment.remoteAddress = val;
  660. this.updateMenuView(requestItem, key, val);
  661. break;
  662. case "remotePort":
  663. requestItem.attachment.remotePort = val;
  664. break;
  665. case "status":
  666. requestItem.attachment.status = val;
  667. this.updateMenuView(requestItem, key, {
  668. status: val,
  669. cached: requestItem.attachment.fromCache,
  670. serviceWorker: requestItem.attachment.fromServiceWorker
  671. });
  672. break;
  673. case "statusText":
  674. requestItem.attachment.statusText = val;
  675. let text = (requestItem.attachment.status + " " +
  676. requestItem.attachment.statusText);
  677. if (requestItem.attachment.fromCache) {
  678. text += " (cached)";
  679. } else if (requestItem.attachment.fromServiceWorker) {
  680. text += " (service worker)";
  681. }
  682. this.updateMenuView(requestItem, key, text);
  683. break;
  684. case "headersSize":
  685. requestItem.attachment.headersSize = val;
  686. break;
  687. case "contentSize":
  688. requestItem.attachment.contentSize = val;
  689. this.updateMenuView(requestItem, key, val);
  690. break;
  691. case "transferredSize":
  692. if (requestItem.attachment.fromCache) {
  693. requestItem.attachment.transferredSize = 0;
  694. this.updateMenuView(requestItem, key, "cached");
  695. } else if (requestItem.attachment.fromServiceWorker) {
  696. requestItem.attachment.transferredSize = 0;
  697. this.updateMenuView(requestItem, key, "service worker");
  698. } else {
  699. requestItem.attachment.transferredSize = val;
  700. this.updateMenuView(requestItem, key, val);
  701. }
  702. break;
  703. case "mimeType":
  704. requestItem.attachment.mimeType = val;
  705. this.updateMenuView(requestItem, key, val);
  706. break;
  707. case "responseContent":
  708. // If there's no mime type available when the response content
  709. // is received, assume text/plain as a fallback.
  710. if (!requestItem.attachment.mimeType) {
  711. requestItem.attachment.mimeType = "text/plain";
  712. this.updateMenuView(requestItem, "mimeType", "text/plain");
  713. }
  714. requestItem.attachment.responseContent = val;
  715. this.updateMenuView(requestItem, key, val);
  716. break;
  717. case "totalTime":
  718. requestItem.attachment.totalTime = val;
  719. requestItem.attachment.endedMillis =
  720. requestItem.attachment.startedMillis + val;
  721. this.updateMenuView(requestItem, key, val);
  722. this._registerLastRequestEnd(requestItem.attachment.endedMillis);
  723. break;
  724. case "eventTimings":
  725. requestItem.attachment.eventTimings = val;
  726. this._createWaterfallView(
  727. requestItem, val.timings,
  728. requestItem.attachment.fromCache ||
  729. requestItem.attachment.fromServiceWorker
  730. );
  731. break;
  732. }
  733. }
  734. refreshNetworkDetailsPaneIfNecessary(requestItem);
  735. if (callback) {
  736. callback();
  737. }
  738. }
  739. /**
  740. * Refreshes the information displayed in the sidebar, in case this update
  741. * may have additional information about a request which isn't shown yet
  742. * in the network details pane.
  743. *
  744. * @param object requestItem
  745. * The item to repopulate the sidebar with in case it's selected in
  746. * this requests menu.
  747. */
  748. function refreshNetworkDetailsPaneIfNecessary(requestItem) {
  749. let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
  750. if (selectedItem == requestItem) {
  751. NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
  752. }
  753. }
  754. // We're done flushing all the requests, clear the update queue.
  755. this._updateQueue = [];
  756. this._addQueue = [];
  757. $("#requests-menu-empty-notice").hidden = !!this.itemCount;
  758. // Make sure all the requests are sorted and filtered.
  759. // Freshly added requests may not yet contain all the information required
  760. // for sorting and filtering predicates, so this is done each time the
  761. // network requests table is flushed (don't worry, events are drained first
  762. // so this doesn't happen once per network event update).
  763. this.sortContents();
  764. this.filterContents();
  765. this.updateRequests();
  766. this.refreshZebra();
  767. // Rescale all the waterfalls so that everything is visible at once.
  768. this._flushWaterfallViews();
  769. },
  770. /**
  771. * Customization function for creating an item's UI.
  772. *
  773. * @param string method
  774. * Specifies the request method (e.g. "GET", "POST", etc.)
  775. * @param string url
  776. * Specifies the request's url.
  777. * @param object cause
  778. * Specifies the request's cause. Has two properties:
  779. * - type: nsContentPolicyType constant
  780. * - uri: URI of the request origin
  781. * @return nsIDOMNode
  782. * The network request view.
  783. */
  784. _createMenuView: function (method, url, cause) {
  785. let template = $("#requests-menu-item-template");
  786. let fragment = document.createDocumentFragment();
  787. // Flatten the DOM by removing one redundant box (the template container).
  788. for (let node of template.childNodes) {
  789. fragment.appendChild(node.cloneNode(true));
  790. }
  791. this.updateMenuView(fragment, "method", method);
  792. this.updateMenuView(fragment, "url", url);
  793. this.updateMenuView(fragment, "cause", cause);
  794. return fragment;
  795. },
  796. /**
  797. * Get a human-readable string from a number of bytes, with the B, KB, MB, or
  798. * GB value. Note that the transition between abbreviations is by 1000 rather
  799. * than 1024 in order to keep the displayed digits smaller as "1016 KB" is
  800. * more awkward than 0.99 MB"
  801. */
  802. getFormattedSize(bytes) {
  803. if (bytes < MAX_BYTES_SIZE) {
  804. return L10N.getFormatStr("networkMenu.sizeB", bytes);
  805. } else if (bytes < MAX_KB_SIZE) {
  806. let kb = bytes / BYTES_IN_KB;
  807. let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
  808. return L10N.getFormatStr("networkMenu.sizeKB", size);
  809. } else if (bytes < MAX_MB_SIZE) {
  810. let mb = bytes / BYTES_IN_MB;
  811. let size = L10N.numberWithDecimals(mb, CONTENT_SIZE_DECIMALS);
  812. return L10N.getFormatStr("networkMenu.sizeMB", size);
  813. }
  814. let gb = bytes / BYTES_IN_GB;
  815. let size = L10N.numberWithDecimals(gb, CONTENT_SIZE_DECIMALS);
  816. return L10N.getFormatStr("networkMenu.sizeGB", size);
  817. },
  818. /**
  819. * Updates the information displayed in a network request item view.
  820. *
  821. * @param object item
  822. * The network request item in this container.
  823. * @param string key
  824. * The type of information that is to be updated.
  825. * @param any value
  826. * The new value to be shown.
  827. * @return object
  828. * A promise that is resolved once the information is displayed.
  829. */
  830. updateMenuView: Task.async(function* (item, key, value) {
  831. let target = item.target || item;
  832. switch (key) {
  833. case "method": {
  834. let node = $(".requests-menu-method", target);
  835. node.setAttribute("value", value);
  836. break;
  837. }
  838. case "url": {
  839. let uri;
  840. try {
  841. uri = NetworkHelper.nsIURL(value);
  842. } catch (e) {
  843. // User input may not make a well-formed url yet.
  844. break;
  845. }
  846. let nameWithQuery = getUriNameWithQuery(uri);
  847. let hostPort = getUriHostPort(uri);
  848. let host = getUriHost(uri);
  849. let unicodeUrl = NetworkHelper.convertToUnicode(unescape(uri.spec));
  850. let file = $(".requests-menu-file", target);
  851. file.setAttribute("value", nameWithQuery);
  852. file.setAttribute("tooltiptext", unicodeUrl);
  853. let domain = $(".requests-menu-domain", target);
  854. domain.setAttribute("value", hostPort);
  855. domain.setAttribute("tooltiptext", hostPort);
  856. // Mark local hosts specially, where "local" is as defined in the W3C
  857. // spec for secure contexts.
  858. // http://www.w3.org/TR/powerful-features/
  859. //
  860. // * If the name falls under 'localhost'
  861. // * If the name is an IPv4 address within 127.0.0.0/8
  862. // * If the name is an IPv6 address within ::1/128
  863. //
  864. // IPv6 parsing is a little sloppy; it assumes that the address has
  865. // been validated before it gets here.
  866. let icon = $(".requests-security-state-icon", target);
  867. icon.classList.remove("security-state-local");
  868. if (host.match(/(.+\.)?localhost$/) ||
  869. host.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}/) ||
  870. host.match(/\[[0:]+1\]/)) {
  871. let tooltip = L10N.getStr("netmonitor.security.state.secure");
  872. icon.classList.add("security-state-local");
  873. icon.setAttribute("tooltiptext", tooltip);
  874. }
  875. break;
  876. }
  877. case "remoteAddress":
  878. let domain = $(".requests-menu-domain", target);
  879. let tooltip = (domain.getAttribute("value") +
  880. (value ? " (" + value + ")" : ""));
  881. domain.setAttribute("tooltiptext", tooltip);
  882. break;
  883. case "securityState": {
  884. let icon = $(".requests-security-state-icon", target);
  885. this.attachSecurityIconClickListener(item);
  886. // Security icon for local hosts is set in the "url" branch
  887. if (icon.classList.contains("security-state-local")) {
  888. break;
  889. }
  890. let tooltip2 = L10N.getStr("netmonitor.security.state." + value);
  891. icon.classList.add("security-state-" + value);
  892. icon.setAttribute("tooltiptext", tooltip2);
  893. break;
  894. }
  895. case "status": {
  896. let node = $(".requests-menu-status-icon", target);
  897. // "code" attribute is only used by css to determine the icon color
  898. let code;
  899. if (value.cached) {
  900. code = "cached";
  901. } else if (value.serviceWorker) {
  902. code = "service worker";
  903. } else {
  904. code = value.status;
  905. }
  906. node.setAttribute("code", code);
  907. let codeNode = $(".requests-menu-status-code", target);
  908. codeNode.setAttribute("value", value.status);
  909. break;
  910. }
  911. case "statusText": {
  912. let node = $(".requests-menu-status", target);
  913. node.setAttribute("tooltiptext", value);
  914. break;
  915. }
  916. case "cause": {
  917. let labelNode = $(".requests-menu-cause-label", target);
  918. labelNode.setAttribute("value", loadCauseString(value.type));
  919. if (value.loadingDocumentUri) {
  920. labelNode.setAttribute("tooltiptext", value.loadingDocumentUri);
  921. }
  922. let stackNode = $(".requests-menu-cause-stack", target);
  923. if (value.stacktrace && value.stacktrace.length > 0) {
  924. stackNode.removeAttribute("hidden");
  925. }
  926. break;
  927. }
  928. case "contentSize": {
  929. let node = $(".requests-menu-size", target);
  930. let text = this.getFormattedSize(value);
  931. node.setAttribute("value", text);
  932. node.setAttribute("tooltiptext", text);
  933. break;
  934. }
  935. case "transferredSize": {
  936. let node = $(".requests-menu-transferred", target);
  937. let text;
  938. if (value === null) {
  939. text = L10N.getStr("networkMenu.sizeUnavailable");
  940. } else if (value === "cached") {
  941. text = L10N.getStr("networkMenu.sizeCached");
  942. node.classList.add("theme-comment");
  943. } else if (value === "service worker") {
  944. text = L10N.getStr("networkMenu.sizeServiceWorker");
  945. node.classList.add("theme-comment");
  946. } else {
  947. text = this.getFormattedSize(value);
  948. }
  949. node.setAttribute("value", text);
  950. node.setAttribute("tooltiptext", text);
  951. break;
  952. }
  953. case "mimeType": {
  954. let type = getAbbreviatedMimeType(value);
  955. let node = $(".requests-menu-type", target);
  956. let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
  957. node.setAttribute("value", text);
  958. node.setAttribute("tooltiptext", value);
  959. break;
  960. }
  961. case "responseContent": {
  962. let { mimeType } = item.attachment;
  963. if (mimeType.includes("image/")) {
  964. let { text, encoding } = value.content;
  965. let responseBody = yield gNetwork.getString(text);
  966. let node = $(".requests-menu-icon", item.target);
  967. node.src = formDataURI(mimeType, encoding, responseBody);
  968. node.setAttribute("type", "thumbnail");
  969. node.removeAttribute("hidden");
  970. window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
  971. }
  972. break;
  973. }
  974. case "totalTime": {
  975. let node = $(".requests-menu-timings-total", target);
  976. // integer
  977. let text = L10N.getFormatStr("networkMenu.totalMS", value);
  978. node.setAttribute("value", text);
  979. node.setAttribute("tooltiptext", text);
  980. break;
  981. }
  982. }
  983. }),
  984. /**
  985. * Creates a waterfall representing timing information in a network
  986. * request item view.
  987. *
  988. * @param object item
  989. * The network request item in this container.
  990. * @param object timings
  991. * An object containing timing information.
  992. * @param boolean fromCache
  993. * Indicates if the result came from the browser cache or
  994. * a service worker
  995. */
  996. _createWaterfallView: function (item, timings, fromCache) {
  997. let { target } = item;
  998. let sections = ["blocked", "dns", "connect", "ssl", "send", "wait", "receive"];
  999. // Skipping "blocked" because it doesn't work yet.
  1000. let timingsNode = $(".requests-menu-timings", target);
  1001. let timingsTotal = $(".requests-menu-timings-total", timingsNode);
  1002. if (fromCache) {
  1003. timingsTotal.style.display = "none";
  1004. return;
  1005. }
  1006. // Add a set of boxes representing timing information.
  1007. for (let key of sections) {
  1008. let width = timings[key];
  1009. // Don't render anything if it surely won't be visible.
  1010. // One millisecond == one unscaled pixel.
  1011. if (width > 0) {
  1012. let timingBox = document.createElement("hbox");
  1013. timingBox.className = "requests-menu-timings-box " + key;
  1014. timingBox.setAttribute("width", width);
  1015. timingsNode.insertBefore(timingBox, timingsTotal);
  1016. }
  1017. }
  1018. },
  1019. /**
  1020. * Rescales and redraws all the waterfall views in this container.
  1021. *
  1022. * @param boolean reset
  1023. * True if this container's width was changed.
  1024. */
  1025. _flushWaterfallViews: function (reset) {
  1026. // Don't paint things while the waterfall view isn't even visible,
  1027. // or there are no items added to this container.
  1028. if (NetMonitorView.currentFrontendMode !=
  1029. "network-inspector-view" || !this.itemCount) {
  1030. return;
  1031. }
  1032. // To avoid expensive operations like getBoundingClientRect() and
  1033. // rebuilding the waterfall background each time a new request comes in,
  1034. // stuff is cached. However, in certain scenarios like when the window
  1035. // is resized, this needs to be invalidated.
  1036. if (reset) {
  1037. this._cachedWaterfallWidth = 0;
  1038. }
  1039. // Determine the scaling to be applied to all the waterfalls so that
  1040. // everything is visible at once. One millisecond == one unscaled pixel.
  1041. let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
  1042. let longestWidth = this._lastRequestEndedMillis -
  1043. this._firstRequestStartedMillis;
  1044. let scale = Math.min(Math.max(availableWidth / longestWidth, EPSILON), 1);
  1045. // Redraw and set the canvas background for each waterfall view.
  1046. this._showWaterfallDivisionLabels(scale);
  1047. this._drawWaterfallBackground(scale);
  1048. // Apply CSS transforms to each waterfall in this container totalTime
  1049. // accurately translate and resize as needed.
  1050. for (let { target, attachment } of this) {
  1051. let timingsNode = $(".requests-menu-timings", target);
  1052. let totalNode = $(".requests-menu-timings-total", target);
  1053. let direction = window.isRTL ? -1 : 1;
  1054. // Render the timing information at a specific horizontal translation
  1055. // based on the delta to the first monitored event network.
  1056. let translateX = "translateX(" + (direction *
  1057. attachment.startedDeltaMillis) + "px)";
  1058. // Based on the total time passed until the last request, rescale
  1059. // all the waterfalls to a reasonable size.
  1060. let scaleX = "scaleX(" + scale + ")";
  1061. // Certain nodes should not be scaled, even if they're children of
  1062. // another scaled node. In this case, apply a reversed transformation.
  1063. let revScaleX = "scaleX(" + (1 / scale) + ")";
  1064. timingsNode.style.transform = scaleX + " " + translateX;
  1065. totalNode.style.transform = revScaleX;
  1066. }
  1067. },
  1068. /**
  1069. * Creates the labels displayed on the waterfall header in this container.
  1070. *
  1071. * @param number scale
  1072. * The current waterfall scale.
  1073. */
  1074. _showWaterfallDivisionLabels: function (scale) {
  1075. let container = $("#requests-menu-waterfall-label-wrapper");
  1076. let availableWidth = this._waterfallWidth - REQUESTS_WATERFALL_SAFE_BOUNDS;
  1077. // Nuke all existing labels.
  1078. while (container.hasChildNodes()) {
  1079. container.firstChild.remove();
  1080. }
  1081. // Build new millisecond tick labels...
  1082. let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
  1083. let optimalTickIntervalFound = false;
  1084. while (!optimalTickIntervalFound) {
  1085. // Ignore any divisions that would end up being too close to each other.
  1086. let scaledStep = scale * timingStep;
  1087. if (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
  1088. timingStep <<= 1;
  1089. continue;
  1090. }
  1091. optimalTickIntervalFound = true;
  1092. // Insert one label for each division on the current scale.
  1093. let fragment = document.createDocumentFragment();
  1094. let direction = window.isRTL ? -1 : 1;
  1095. for (let x = 0; x < availableWidth; x += scaledStep) {
  1096. let translateX = "translateX(" + ((direction * x) | 0) + "px)";
  1097. let millisecondTime = x / scale;
  1098. let normalizedTime = millisecondTime;
  1099. let divisionScale = "millisecond";
  1100. // If the division is greater than 1 minute.
  1101. if (normalizedTime > 60000) {
  1102. normalizedTime /= 60000;
  1103. divisionScale = "minute";
  1104. } else if (normalizedTime > 1000) {
  1105. // If the division is greater than 1 second.
  1106. normalizedTime /= 1000;
  1107. divisionScale = "second";
  1108. }
  1109. // Showing too many decimals is bad UX.
  1110. if (divisionScale == "millisecond") {
  1111. normalizedTime |= 0;
  1112. } else {
  1113. normalizedTime = L10N.numberWithDecimals(normalizedTime,
  1114. REQUEST_TIME_DECIMALS);
  1115. }
  1116. let node = document.createElement("label");
  1117. let text = L10N.getFormatStr("networkMenu." +
  1118. divisionScale, normalizedTime);
  1119. node.className = "plain requests-menu-timings-division";
  1120. node.setAttribute("division-scale", divisionScale);
  1121. node.style.transform = translateX;
  1122. node.setAttribute("value", text);
  1123. fragment.appendChild(node);
  1124. }
  1125. container.appendChild(fragment);
  1126. container.className = "requests-menu-waterfall-visible";
  1127. }
  1128. },
  1129. /**
  1130. * Creates the background displayed on each waterfall view in this container.
  1131. *
  1132. * @param number scale
  1133. * The current waterfall scale.
  1134. */
  1135. _drawWaterfallBackground: function (scale) {
  1136. if (!this._canvas || !this._ctx) {
  1137. this._canvas = document.createElementNS(HTML_NS, "canvas");
  1138. this._ctx = this._canvas.getContext("2d");
  1139. }
  1140. let canvas = this._canvas;
  1141. let ctx = this._ctx;
  1142. // Nuke the context.
  1143. let canvasWidth = canvas.width = this._waterfallWidth;
  1144. // Awww yeah, 1px, repeats on Y axis.
  1145. let canvasHeight = canvas.height = 1;
  1146. // Start over.
  1147. let imageData = ctx.createImageData(canvasWidth, canvasHeight);
  1148. let pixelArray = imageData.data;
  1149. let buf = new ArrayBuffer(pixelArray.length);
  1150. let view8bit = new Uint8ClampedArray(buf);
  1151. let view32bit = new Uint32Array(buf);
  1152. // Build new millisecond tick lines...
  1153. let timingStep = REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE;
  1154. let [r, g, b] = REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
  1155. let alphaComponent = REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
  1156. let optimalTickIntervalFound = false;
  1157. while (!optimalTickIntervalFound) {
  1158. // Ignore any divisions that would end up being too close to each other.
  1159. let scaledStep = scale * timingStep;
  1160. if (scaledStep < REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN) {
  1161. timingStep <<= 1;
  1162. continue;
  1163. }
  1164. optimalTickIntervalFound = true;
  1165. // Insert one pixel for each division on each scale.
  1166. for (let i = 1; i <= REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
  1167. let increment = scaledStep * Math.pow(2, i);
  1168. for (let x = 0; x < canvasWidth; x += increment) {
  1169. let position = (window.isRTL ? canvasWidth - x : x) | 0;
  1170. view32bit[position] =
  1171. (alphaComponent << 24) | (b << 16) | (g << 8) | r;
  1172. }
  1173. alphaComponent += REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
  1174. }
  1175. }
  1176. {
  1177. let t = NetMonitorController.NetworkEventsHandler
  1178. .firstDocumentDOMContentLoadedTimestamp;
  1179. let delta = Math.floor((t - this._firstRequestStartedMillis) * scale);
  1180. let [r1, g1, b1, a1] =
  1181. REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA;
  1182. view32bit[delta] = (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
  1183. }
  1184. {
  1185. let t = NetMonitorController.NetworkEventsHandler
  1186. .firstDocumentLoadTimestamp;
  1187. let delta = Math.floor((t - this._firstRequestStartedMillis) * scale);
  1188. let [r2, g2, b2, a2] = REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA;
  1189. view32bit[delta] = (a2 << 24) | (r2 << 16) | (g2 << 8) | b2;
  1190. }
  1191. // Flush the image data and cache the waterfall background.
  1192. pixelArray.set(view8bit);
  1193. ctx.putImageData(imageData, 0, 0);
  1194. document.mozSetImageElement("waterfall-background", canvas);
  1195. },
  1196. /**
  1197. * The selection listener for this container.
  1198. */
  1199. _onSelect: function ({ detail: item }) {
  1200. if (item) {
  1201. NetMonitorView.Sidebar.populate(item.attachment);
  1202. NetMonitorView.Sidebar.toggle(true);
  1203. } else {
  1204. NetMonitorView.Sidebar.toggle(false);
  1205. }
  1206. },
  1207. /**
  1208. * The swap listener for this container.
  1209. * Called when two items switch places, when the contents are sorted.
  1210. */
  1211. _onSwap: function ({ detail: [firstItem, secondItem] }) {
  1212. // Reattach click listener to the security icons
  1213. this.attachSecurityIconClickListener(firstItem);
  1214. this.attachSecurityIconClickListener(secondItem);
  1215. },
  1216. /**
  1217. * The predicate used when deciding whether a popup should be shown
  1218. * over a request item or not.
  1219. *
  1220. * @param nsIDOMNode target
  1221. * The element node currently being hovered.
  1222. * @param object tooltip
  1223. * The current tooltip instance.
  1224. * @return {Promise}
  1225. */
  1226. _onHover: Task.async(function* (target, tooltip) {
  1227. let requestItem = this.getItemForElement(target);
  1228. if (!requestItem) {
  1229. return false;
  1230. }
  1231. let hovered = requestItem.attachment;
  1232. if (hovered.responseContent && target.closest(".requests-menu-icon-and-file")) {
  1233. return this._setTooltipImageContent(tooltip, requestItem);
  1234. } else if (hovered.cause && target.closest(".requests-menu-cause-stack")) {
  1235. return this._setTooltipStackTraceContent(tooltip, requestItem);
  1236. }
  1237. return false;
  1238. }),
  1239. _setTooltipImageContent: Task.async(function* (tooltip, requestItem) {
  1240. let { mimeType, text, encoding } = requestItem.attachment.responseContent.content;
  1241. if (!mimeType || !mimeType.includes("image/")) {
  1242. return false;
  1243. }
  1244. let string = yield gNetwork.getString(text);
  1245. let src = formDataURI(mimeType, encoding, string);
  1246. let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
  1247. let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src);
  1248. let options = { maxDim, naturalWidth, naturalHeight };
  1249. setImageTooltip(tooltip, tooltip.doc, src, options);
  1250. return $(".requests-menu-icon", requestItem.target);
  1251. }),
  1252. _setTooltipStackTraceContent: Task.async(function* (tooltip, requestItem) {
  1253. let {stacktrace} = requestItem.attachment.cause;
  1254. if (!stacktrace || stacktrace.length == 0) {
  1255. return false;
  1256. }
  1257. let doc = tooltip.doc;
  1258. let el = doc.createElementNS(HTML_NS, "div");
  1259. el.className = "stack-trace-tooltip devtools-monospace";
  1260. for (let f of stacktrace) {
  1261. let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
  1262. if (asyncCause) {
  1263. // if there is asyncCause, append a "divider" row into the trace
  1264. let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
  1265. asyncFrameEl.className = "stack-frame stack-frame-async";
  1266. asyncFrameEl.textContent =
  1267. WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
  1268. el.appendChild(asyncFrameEl);
  1269. }
  1270. // Parse a source name in format "url -> url"
  1271. let sourceUrl = filename.split(" -> ").pop();
  1272. let frameEl = doc.createElementNS(HTML_NS, "div");
  1273. frameEl.className = "stack-frame stack-frame-call";
  1274. let funcEl = doc.createElementNS(HTML_NS, "span");
  1275. funcEl.className = "stack-frame-function-name";
  1276. funcEl.textContent =
  1277. functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
  1278. frameEl.appendChild(funcEl);
  1279. let sourceEl = doc.createElementNS(HTML_NS, "span");
  1280. sourceEl.className = "stack-frame-source-name";
  1281. frameEl.appendChild(sourceEl);
  1282. let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
  1283. sourceInnerEl.className = "stack-frame-source-name-inner";
  1284. sourceEl.appendChild(sourceInnerEl);
  1285. sourceInnerEl.textContent = sourceUrl;
  1286. sourceInnerEl.title = sourceUrl;
  1287. let lineEl = doc.createElementNS(HTML_NS, "span");
  1288. lineEl.className = "stack-frame-line";
  1289. lineEl.textContent = `:${lineNumber}:${columnNumber}`;
  1290. sourceInnerEl.appendChild(lineEl);
  1291. frameEl.addEventListener("click", () => {
  1292. // hide the tooltip immediately, not after delay
  1293. tooltip.hide();
  1294. NetMonitorController.viewSourceInDebugger(filename, lineNumber);
  1295. }, false);
  1296. el.appendChild(frameEl);
  1297. }
  1298. tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
  1299. return true;
  1300. }),
  1301. /**
  1302. * A handler that opens the security tab in the details view if secure or
  1303. * broken security indicator is clicked.
  1304. */
  1305. _onSecurityIconClick: function (e) {
  1306. let state = this.selectedItem.attachment.securityState;
  1307. if (state !== "insecure") {
  1308. // Choose the security tab.
  1309. NetMonitorView.NetworkDetails.widget.selectedIndex = 5;
  1310. }
  1311. },
  1312. /**
  1313. * The resize listener for this container's window.
  1314. */
  1315. _onResize: function (e) {
  1316. // Allow requests to settle down first.
  1317. setNamedTimeout("resize-events",
  1318. RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
  1319. },
  1320. /**
  1321. * Scroll listener for the requests menu view.
  1322. */
  1323. _onScroll: function () {
  1324. this.tooltip.hide();
  1325. },
  1326. /**
  1327. * Open context menu
  1328. */
  1329. _onContextMenu: function (e) {
  1330. e.preventDefault();
  1331. this.contextMenu.open(e);
  1332. },
  1333. /**
  1334. * Checks if the specified unix time is the first one to be known of,
  1335. * and saves it if so.
  1336. *
  1337. * @param number unixTime
  1338. * The milliseconds to check and save.
  1339. */
  1340. _registerFirstRequestStart: function (unixTime) {
  1341. if (this._firstRequestStartedMillis == -1) {
  1342. this._firstRequestStartedMillis = unixTime;
  1343. }
  1344. if (this._firstRequestStartedMillisNotPersistent == -1) {
  1345. this._firstRequestStartedMillisNotPersistent = unixTime;
  1346. }
  1347. },
  1348. /**
  1349. * Checks if the specified unix time is the last one to be known of,
  1350. * and saves it if so.
  1351. *
  1352. * @param number unixTime
  1353. * The milliseconds to check and save.
  1354. */
  1355. _registerLastRequestEnd: function (unixTime) {
  1356. if (this._lastRequestEndedMillis < unixTime) {
  1357. this._lastRequestEndedMillis = unixTime;
  1358. }
  1359. },
  1360. /**
  1361. * Gets the available waterfall width in this container.
  1362. * @return number
  1363. */
  1364. get _waterfallWidth() {
  1365. if (this._cachedWaterfallWidth == 0) {
  1366. let container = $("#requests-menu-toolbar");
  1367. let waterfall = $("#requests-menu-waterfall-header-box");
  1368. let containerBounds = container.getBoundingClientRect();
  1369. let waterfallBounds = waterfall.getBoundingClientRect();
  1370. if (!window.isRTL) {
  1371. this._cachedWaterfallWidth = containerBounds.width -
  1372. waterfallBounds.left;
  1373. } else {
  1374. this._cachedWaterfallWidth = waterfallBounds.right;
  1375. }
  1376. }
  1377. return this._cachedWaterfallWidth;
  1378. },
  1379. _splitter: null,
  1380. _summary: null,
  1381. _canvas: null,
  1382. _ctx: null,
  1383. _cachedWaterfallWidth: 0,
  1384. _firstRequestStartedMillis: -1,
  1385. _firstRequestStartedMillisNotPersistent: -1,
  1386. _lastRequestEndedMillis: -1,
  1387. _updateQueue: [],
  1388. _addQueue: [],
  1389. _updateTimeout: null,
  1390. _resizeTimeout: null,
  1391. _activeFilters: ["all"],
  1392. _currentFreetextFilter: ""
  1393. });
  1394. exports.RequestsMenuView = RequestsMenuView;