aboutTelemetry.js 69 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169
  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. 'use strict';
  5. var Ci = Components.interfaces;
  6. var Cc = Components.classes;
  7. var Cu = Components.utils;
  8. Cu.import("resource://gre/modules/Services.jsm");
  9. Cu.import("resource://gre/modules/TelemetryTimestamps.jsm");
  10. Cu.import("resource://gre/modules/TelemetryController.jsm");
  11. Cu.import("resource://gre/modules/TelemetrySession.jsm");
  12. Cu.import("resource://gre/modules/TelemetryArchive.jsm");
  13. Cu.import("resource://gre/modules/TelemetryUtils.jsm");
  14. Cu.import("resource://gre/modules/TelemetryLog.jsm");
  15. Cu.import("resource://gre/modules/Preferences.jsm");
  16. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  17. Cu.import("resource://gre/modules/Task.jsm");
  18. const Telemetry = Services.telemetry;
  19. const bundle = Services.strings.createBundle(
  20. "chrome://global/locale/aboutTelemetry.properties");
  21. const brandBundle = Services.strings.createBundle(
  22. "chrome://branding/locale/brand.properties");
  23. // Maximum height of a histogram bar (in em for html, in chars for text)
  24. const MAX_BAR_HEIGHT = 18;
  25. const MAX_BAR_CHARS = 25;
  26. const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner";
  27. const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
  28. const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql";
  29. const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl";
  30. const DEFAULT_SYMBOL_SERVER_URI = "http://symbolapi.mozilla.org";
  31. const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
  32. // ms idle before applying the filter (allow uninterrupted typing)
  33. const FILTER_IDLE_TIMEOUT = 500;
  34. const isWindows = (Services.appinfo.OS == "WINNT");
  35. const EOL = isWindows ? "\r\n" : "\n";
  36. // This is the ping object currently displayed in the page.
  37. var gPingData = null;
  38. // Cached value of document's RTL mode
  39. var documentRTLMode = "";
  40. /**
  41. * Helper function for determining whether the document direction is RTL.
  42. * Caches result of check on first invocation.
  43. */
  44. function isRTL() {
  45. if (!documentRTLMode)
  46. documentRTLMode = window.getComputedStyle(document.body).direction;
  47. return (documentRTLMode == "rtl");
  48. }
  49. function isArray(arg) {
  50. return Object.prototype.toString.call(arg) === '[object Array]';
  51. }
  52. function isFlatArray(obj) {
  53. if (!isArray(obj)) {
  54. return false;
  55. }
  56. return !obj.some(e => typeof(e) == "object");
  57. }
  58. /**
  59. * This is a helper function for explodeObject.
  60. */
  61. function flattenObject(obj, map, path, array) {
  62. if (!obj) {
  63. return;
  64. }
  65. for (let k of Object.keys(obj)) {
  66. let newPath = [...path, array ? "[" + k + "]" : k];
  67. let v = obj[k];
  68. if (!v || (typeof(v) != "object")) {
  69. map.set(newPath.join("."), v);
  70. } else if (isFlatArray(v)) {
  71. map.set(newPath.join("."), "[" + v.join(", ") + "]");
  72. } else {
  73. flattenObject(v, map, newPath, isArray(v));
  74. }
  75. }
  76. }
  77. /**
  78. * This turns a JSON object into a "flat" stringified form.
  79. *
  80. * For an object like {a: "1", b: {c: "2", d: "3"}} it returns a Map of the
  81. * form Map(["a","1"], ["b.c", "2"], ["b.d", "3"]).
  82. */
  83. function explodeObject(obj) {
  84. let map = new Map();
  85. flattenObject(obj, map, []);
  86. return map;
  87. }
  88. function filterObject(obj, filterOut) {
  89. let ret = {};
  90. for (let k of Object.keys(obj)) {
  91. if (filterOut.indexOf(k) == -1) {
  92. ret[k] = obj[k];
  93. }
  94. }
  95. return ret;
  96. }
  97. /**
  98. * This turns a JSON object into a "flat" stringified form, separated into top-level sections.
  99. *
  100. * For an object like:
  101. * {
  102. * a: {b: "1"},
  103. * c: {d: "2", e: {f: "3"}}
  104. * }
  105. * it returns a Map of the form:
  106. * Map([
  107. * ["a", Map(["b","1"])],
  108. * ["c", Map([["d", "2"], ["e.f", "3"]])]
  109. * ])
  110. */
  111. function sectionalizeObject(obj) {
  112. let map = new Map();
  113. for (let k of Object.keys(obj)) {
  114. map.set(k, explodeObject(obj[k]));
  115. }
  116. return map;
  117. }
  118. /**
  119. * Obtain the main DOMWindow for the current context.
  120. */
  121. function getMainWindow() {
  122. return window.QueryInterface(Ci.nsIInterfaceRequestor)
  123. .getInterface(Ci.nsIWebNavigation)
  124. .QueryInterface(Ci.nsIDocShellTreeItem)
  125. .rootTreeItem
  126. .QueryInterface(Ci.nsIInterfaceRequestor)
  127. .getInterface(Ci.nsIDOMWindow);
  128. }
  129. /**
  130. * Obtain the DOMWindow that can open a preferences pane.
  131. *
  132. * This is essentially "get the browser chrome window" with the added check
  133. * that the supposed browser chrome window is capable of opening a preferences
  134. * pane.
  135. *
  136. * This may return null if we can't find the browser chrome window.
  137. */
  138. function getMainWindowWithPreferencesPane() {
  139. let mainWindow = getMainWindow();
  140. if (mainWindow && "openAdvancedPreferences" in mainWindow) {
  141. return mainWindow;
  142. }
  143. return null;
  144. }
  145. /**
  146. * Remove all child nodes of a document node.
  147. */
  148. function removeAllChildNodes(node) {
  149. while (node.hasChildNodes()) {
  150. node.removeChild(node.lastChild);
  151. }
  152. }
  153. /**
  154. * Pad a number to two digits with leading "0".
  155. */
  156. function padToTwoDigits(n) {
  157. return (n > 9) ? n: "0" + n;
  158. }
  159. /**
  160. * Return yesterdays date with the same time.
  161. */
  162. function yesterday(date) {
  163. let d = new Date(date);
  164. d.setDate(d.getDate() - 1);
  165. return d;
  166. }
  167. /**
  168. * This returns a short date string of the form YYYY/MM/DD.
  169. */
  170. function shortDateString(date) {
  171. return date.getFullYear()
  172. + "/" + padToTwoDigits(date.getMonth() + 1)
  173. + "/" + padToTwoDigits(date.getDate());
  174. }
  175. /**
  176. * This returns a short time string of the form hh:mm:ss.
  177. */
  178. function shortTimeString(date) {
  179. return padToTwoDigits(date.getHours())
  180. + ":" + padToTwoDigits(date.getMinutes())
  181. + ":" + padToTwoDigits(date.getSeconds());
  182. }
  183. var Settings = {
  184. SETTINGS: [
  185. // data upload
  186. {
  187. pref: PREF_FHR_UPLOAD_ENABLED,
  188. defaultPrefValue: false,
  189. descriptionEnabledId: "description-upload-enabled",
  190. descriptionDisabledId: "description-upload-disabled",
  191. },
  192. // extended "Telemetry" recording
  193. {
  194. pref: PREF_TELEMETRY_ENABLED,
  195. defaultPrefValue: false,
  196. descriptionEnabledId: "description-extended-recording-enabled",
  197. descriptionDisabledId: "description-extended-recording-disabled",
  198. },
  199. ],
  200. attachObservers: function() {
  201. for (let s of this.SETTINGS) {
  202. let setting = s;
  203. Preferences.observe(setting.pref, this.render, this);
  204. }
  205. let elements = document.getElementsByClassName("change-data-choices-link");
  206. for (let el of elements) {
  207. el.addEventListener("click", function() {
  208. // Show the data choices preferences on desktop.
  209. let mainWindow = getMainWindowWithPreferencesPane();
  210. mainWindow.openAdvancedPreferences("dataChoicesTab");
  211. }, false);
  212. }
  213. },
  214. detachObservers: function() {
  215. for (let setting of this.SETTINGS) {
  216. Preferences.ignore(setting.pref, this.render, this);
  217. }
  218. },
  219. /**
  220. * Updates the button & text at the top of the page to reflect Telemetry state.
  221. */
  222. render: function() {
  223. for (let setting of this.SETTINGS) {
  224. let enabledElement = document.getElementById(setting.descriptionEnabledId);
  225. let disabledElement = document.getElementById(setting.descriptionDisabledId);
  226. if (Preferences.get(setting.pref, setting.defaultPrefValue)) {
  227. enabledElement.classList.remove("hidden");
  228. disabledElement.classList.add("hidden");
  229. } else {
  230. enabledElement.classList.add("hidden");
  231. disabledElement.classList.remove("hidden");
  232. }
  233. }
  234. }
  235. };
  236. var PingPicker = {
  237. viewCurrentPingData: null,
  238. viewStructuredPingData: null,
  239. _archivedPings: null,
  240. attachObservers: function() {
  241. let elements = document.getElementsByName("choose-ping-source");
  242. for (let el of elements) {
  243. el.addEventListener("change", () => this.onPingSourceChanged(), false);
  244. }
  245. let displays = document.getElementsByName("choose-ping-display");
  246. for (let el of displays) {
  247. el.addEventListener("change", () => this.onPingDisplayChanged(), false);
  248. }
  249. document.getElementById("show-subsession-data").addEventListener("change", () => {
  250. this._updateCurrentPingData();
  251. });
  252. document.getElementById("choose-ping-week").addEventListener("change", () => {
  253. this._renderPingList();
  254. this._updateArchivedPingData();
  255. }, false);
  256. document.getElementById("choose-ping-id").addEventListener("change", () => {
  257. this._updateArchivedPingData()
  258. }, false);
  259. document.getElementById("newer-ping")
  260. .addEventListener("click", () => this._movePingIndex(-1), false);
  261. document.getElementById("older-ping")
  262. .addEventListener("click", () => this._movePingIndex(1), false);
  263. document.getElementById("choose-payload")
  264. .addEventListener("change", () => displayPingData(gPingData), false);
  265. document.getElementById("histograms-processes")
  266. .addEventListener("change", () => displayPingData(gPingData), false);
  267. document.getElementById("keyed-histograms-processes")
  268. .addEventListener("change", () => displayPingData(gPingData), false);
  269. },
  270. onPingSourceChanged: function() {
  271. this.update();
  272. },
  273. onPingDisplayChanged: function() {
  274. this.update();
  275. },
  276. update: Task.async(function*() {
  277. let viewCurrent = document.getElementById("ping-source-current").checked;
  278. let viewStructured = document.getElementById("ping-source-structured").checked;
  279. let currentChanged = viewCurrent !== this.viewCurrentPingData;
  280. let structuredChanged = viewStructured !== this.viewStructuredPingData;
  281. this.viewCurrentPingData = viewCurrent;
  282. this.viewStructuredPingData = viewStructured;
  283. // If we have no archived pings, disable the ping archive selection.
  284. // This can happen on new profiles or if the ping archive is disabled.
  285. let archivedPingList = yield TelemetryArchive.promiseArchivedPingList();
  286. let sourceArchived = document.getElementById("ping-source-archive");
  287. sourceArchived.disabled = (archivedPingList.length == 0);
  288. if (currentChanged) {
  289. if (this.viewCurrentPingData) {
  290. document.getElementById("current-ping-picker").classList.remove("hidden");
  291. document.getElementById("archived-ping-picker").classList.add("hidden");
  292. this._updateCurrentPingData();
  293. } else {
  294. document.getElementById("current-ping-picker").classList.add("hidden");
  295. yield this._updateArchivedPingList(archivedPingList);
  296. document.getElementById("archived-ping-picker").classList.remove("hidden");
  297. }
  298. }
  299. if (structuredChanged) {
  300. if (this.viewStructuredPingData) {
  301. this._showStructuredPingData();
  302. } else {
  303. this._showRawPingData();
  304. }
  305. }
  306. }),
  307. _updateCurrentPingData: function() {
  308. const subsession = document.getElementById("show-subsession-data").checked;
  309. const ping = TelemetryController.getCurrentPingData(subsession);
  310. if (!ping) {
  311. return;
  312. }
  313. displayPingData(ping, true);
  314. },
  315. _updateArchivedPingData: function() {
  316. let id = this._getSelectedPingId();
  317. return TelemetryArchive.promiseArchivedPingById(id)
  318. .then((ping) => displayPingData(ping, true));
  319. },
  320. _updateArchivedPingList: Task.async(function*(pingList) {
  321. // The archived ping list is sorted in ascending timestamp order,
  322. // but descending is more practical for the operations we do here.
  323. pingList.reverse();
  324. this._archivedPings = pingList;
  325. // Collect the start dates for all the weeks we have pings for.
  326. let weekStart = (date) => {
  327. let weekDay = (date.getDay() + 6) % 7;
  328. let monday = new Date(date);
  329. monday.setDate(date.getDate() - weekDay);
  330. return TelemetryUtils.truncateToDays(monday);
  331. };
  332. let weekStartDates = new Set();
  333. for (let p of pingList) {
  334. weekStartDates.add(weekStart(new Date(p.timestampCreated)).getTime());
  335. }
  336. // Build a list of the week date ranges we have ping data for.
  337. let plusOneWeek = (date) => {
  338. let d = date;
  339. d.setDate(d.getDate() + 7);
  340. return d;
  341. };
  342. this._weeks = Array.from(weekStartDates.values(), startTime => ({
  343. startDate: new Date(startTime),
  344. endDate: plusOneWeek(new Date(startTime)),
  345. }));
  346. // Render the archive data.
  347. this._renderWeeks();
  348. this._renderPingList();
  349. // Update the displayed ping.
  350. yield this._updateArchivedPingData();
  351. }),
  352. _renderWeeks: function() {
  353. let weekSelector = document.getElementById("choose-ping-week");
  354. removeAllChildNodes(weekSelector);
  355. let index = 0;
  356. for (let week of this._weeks) {
  357. let text = shortDateString(week.startDate)
  358. + " - " + shortDateString(yesterday(week.endDate));
  359. let option = document.createElement("option");
  360. let content = document.createTextNode(text);
  361. option.appendChild(content);
  362. weekSelector.appendChild(option);
  363. }
  364. },
  365. _getSelectedWeek: function() {
  366. let weekSelector = document.getElementById("choose-ping-week");
  367. return this._weeks[weekSelector.selectedIndex];
  368. },
  369. _renderPingList: function(id = null) {
  370. let pingSelector = document.getElementById("choose-ping-id");
  371. removeAllChildNodes(pingSelector);
  372. let weekRange = this._getSelectedWeek();
  373. let pings = this._archivedPings.filter(
  374. (p) => p.timestampCreated >= weekRange.startDate.getTime() &&
  375. p.timestampCreated < weekRange.endDate.getTime());
  376. for (let p of pings) {
  377. let date = new Date(p.timestampCreated);
  378. let text = shortDateString(date)
  379. + " " + shortTimeString(date)
  380. + " - " + p.type;
  381. let option = document.createElement("option");
  382. let content = document.createTextNode(text);
  383. option.appendChild(content);
  384. option.setAttribute("value", p.id);
  385. if (id && p.id == id) {
  386. option.selected = true;
  387. }
  388. pingSelector.appendChild(option);
  389. }
  390. },
  391. _getSelectedPingId: function() {
  392. let pingSelector = document.getElementById("choose-ping-id");
  393. let selected = pingSelector.selectedOptions.item(0);
  394. return selected.getAttribute("value");
  395. },
  396. _movePingIndex: function(offset) {
  397. const id = this._getSelectedPingId();
  398. const index = this._archivedPings.findIndex((p) => p.id == id);
  399. const newIndex = Math.min(Math.max(index + offset, 0), this._archivedPings.length - 1);
  400. const ping = this._archivedPings[newIndex];
  401. const weekIndex = this._weeks.findIndex(
  402. (week) => ping.timestampCreated >= week.startDate.getTime() &&
  403. ping.timestampCreated < week.endDate.getTime());
  404. const options = document.getElementById("choose-ping-week").options;
  405. options.item(weekIndex).selected = true;
  406. this._renderPingList(ping.id);
  407. this._updateArchivedPingData();
  408. },
  409. _showRawPingData: function() {
  410. document.getElementById("raw-ping-data-section").classList.remove("hidden");
  411. document.getElementById("structured-ping-data-section").classList.add("hidden");
  412. },
  413. _showStructuredPingData: function() {
  414. document.getElementById("raw-ping-data-section").classList.add("hidden");
  415. document.getElementById("structured-ping-data-section").classList.remove("hidden");
  416. },
  417. };
  418. var GeneralData = {
  419. /**
  420. * Renders the general data
  421. */
  422. render: function(aPing) {
  423. setHasData("general-data-section", true);
  424. let table = document.createElement("table");
  425. let caption = document.createElement("caption");
  426. let captionString = bundle.GetStringFromName("generalDataTitle");
  427. caption.appendChild(document.createTextNode(captionString + "\n"));
  428. table.appendChild(caption);
  429. let headings = document.createElement("tr");
  430. this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingName") + "\t");
  431. this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingValue") + "\t");
  432. table.appendChild(headings);
  433. // The payload & environment parts are handled by other renderers.
  434. let ignoreSections = ["payload", "environment"];
  435. let data = explodeObject(filterObject(aPing, ignoreSections));
  436. for (let [path, value] of data) {
  437. let row = document.createElement("tr");
  438. this.appendColumn(row, "td", path + "\t");
  439. this.appendColumn(row, "td", value + "\t");
  440. table.appendChild(row);
  441. }
  442. let dataDiv = document.getElementById("general-data");
  443. removeAllChildNodes(dataDiv);
  444. dataDiv.appendChild(table);
  445. },
  446. /**
  447. * Helper function for appending a column to the data table.
  448. *
  449. * @param aRowElement Parent row element
  450. * @param aColType Column's tag name
  451. * @param aColText Column contents
  452. */
  453. appendColumn: function(aRowElement, aColType, aColText) {
  454. let colElement = document.createElement(aColType);
  455. let colTextElement = document.createTextNode(aColText);
  456. colElement.appendChild(colTextElement);
  457. aRowElement.appendChild(colElement);
  458. },
  459. };
  460. var EnvironmentData = {
  461. /**
  462. * Renders the environment data
  463. */
  464. render: function(ping) {
  465. let dataDiv = document.getElementById("environment-data");
  466. removeAllChildNodes(dataDiv);
  467. const hasData = !!ping.environment;
  468. setHasData("environment-data-section", hasData);
  469. if (!hasData) {
  470. return;
  471. }
  472. let data = sectionalizeObject(ping.environment);
  473. for (let [section, sectionData] of data) {
  474. if (section == "addons") {
  475. break;
  476. }
  477. let table = document.createElement("table");
  478. this.appendHeading(table);
  479. for (let [path, value] of sectionData) {
  480. let row = document.createElement("tr");
  481. this.appendColumn(row, "td", path);
  482. this.appendColumn(row, "td", value);
  483. table.appendChild(row);
  484. }
  485. let hasData = sectionData.size > 0;
  486. this.createSubsection(section, hasData, table, dataDiv);
  487. }
  488. // We use specialized rendering here to make the addon and plugin listings
  489. // more readable.
  490. this.createAddonSection(dataDiv, ping);
  491. },
  492. createSubsection: function(title, hasSubdata, subSectionData, dataDiv) {
  493. let dataSection = document.createElement("section");
  494. dataSection.classList.add("data-subsection");
  495. if (hasSubdata) {
  496. dataSection.classList.add("has-subdata");
  497. }
  498. // Create section heading
  499. let sectionName = document.createElement("h2");
  500. sectionName.setAttribute("class", "section-name");
  501. sectionName.appendChild(document.createTextNode(title));
  502. sectionName.addEventListener("click", toggleSection, false);
  503. // Create caption for toggling the subsection visibility.
  504. let toggleCaption = document.createElement("span");
  505. toggleCaption.setAttribute("class", "toggle-caption");
  506. let toggleText = bundle.GetStringFromName("environmentDataSubsectionToggle");
  507. toggleCaption.appendChild(document.createTextNode(" " + toggleText));
  508. toggleCaption.addEventListener("click", toggleSection, false);
  509. // Create caption for empty subsections.
  510. let emptyCaption = document.createElement("span");
  511. emptyCaption.setAttribute("class", "empty-caption");
  512. let emptyText = bundle.GetStringFromName("environmentDataSubsectionEmpty");
  513. emptyCaption.appendChild(document.createTextNode(" " + emptyText));
  514. // Create data container
  515. let data = document.createElement("div");
  516. data.setAttribute("class", "subsection-data subdata");
  517. data.appendChild(subSectionData);
  518. // Append elements
  519. dataSection.appendChild(sectionName);
  520. dataSection.appendChild(toggleCaption);
  521. dataSection.appendChild(emptyCaption);
  522. dataSection.appendChild(data);
  523. dataDiv.appendChild(dataSection);
  524. },
  525. renderPersona: function(addonObj, addonSection, sectionTitle) {
  526. let table = document.createElement("table");
  527. table.setAttribute("id", sectionTitle);
  528. this.appendAddonSubsectionTitle(sectionTitle, table);
  529. this.appendRow(table, "persona", addonObj.persona);
  530. addonSection.appendChild(table);
  531. },
  532. renderActivePlugins: function(addonObj, addonSection, sectionTitle) {
  533. let data = explodeObject(addonObj);
  534. let table = document.createElement("table");
  535. table.setAttribute("id", sectionTitle);
  536. this.appendAddonSubsectionTitle(sectionTitle, table);
  537. for (let plugin of addonObj) {
  538. let data = explodeObject(plugin);
  539. this.appendHeadingName(table, data.get("name"));
  540. for (let [key, value] of data) {
  541. this.appendRow(table, key, value);
  542. }
  543. }
  544. addonSection.appendChild(table);
  545. },
  546. renderAddonsObject: function(addonObj, addonSection, sectionTitle) {
  547. let table = document.createElement("table");
  548. table.setAttribute("id", sectionTitle);
  549. this.appendAddonSubsectionTitle(sectionTitle, table);
  550. for (let id of Object.keys(addonObj)) {
  551. let addon = addonObj[id];
  552. this.appendHeadingName(table, addon.name || id);
  553. this.appendAddonID(table, id);
  554. let data = explodeObject(addon);
  555. for (let [key, value] of data) {
  556. this.appendRow(table, key, value);
  557. }
  558. }
  559. addonSection.appendChild(table);
  560. },
  561. renderKeyValueObject: function(addonObj, addonSection, sectionTitle) {
  562. let data = explodeObject(addonObj);
  563. let table = document.createElement("table");
  564. table.setAttribute("class", sectionTitle);
  565. this.appendAddonSubsectionTitle(sectionTitle, table);
  566. this.appendHeading(table);
  567. for (let [key, value] of data) {
  568. this.appendRow(table, key, value);
  569. }
  570. addonSection.appendChild(table);
  571. },
  572. appendAddonID: function(table, addonID) {
  573. this.appendRow(table, "id", addonID);
  574. },
  575. appendHeading: function(table) {
  576. let headings = document.createElement("tr");
  577. this.appendColumn(headings, "th", bundle.GetStringFromName("environmentDataHeadingName"));
  578. this.appendColumn(headings, "th", bundle.GetStringFromName("environmentDataHeadingValue"));
  579. table.appendChild(headings);
  580. },
  581. appendHeadingName: function(table, name) {
  582. let headings = document.createElement("tr");
  583. this.appendColumn(headings, "th", name);
  584. headings.cells[0].colSpan = 2;
  585. table.appendChild(headings);
  586. },
  587. appendAddonSubsectionTitle: function(section, table) {
  588. let caption = document.createElement("caption");
  589. caption.setAttribute("class", "addon-caption");
  590. caption.appendChild(document.createTextNode(section));
  591. table.appendChild(caption);
  592. },
  593. createAddonSection: function(dataDiv, ping) {
  594. let addonSection = document.createElement("div");
  595. let addons = ping.environment.addons;
  596. this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
  597. this.renderActivePlugins(addons.activePlugins, addonSection, "activePlugins");
  598. this.renderKeyValueObject(addons.theme, addonSection, "theme");
  599. this.renderKeyValueObject(addons.activeExperiment, addonSection, "activeExperiment");
  600. this.renderAddonsObject(addons.activeGMPlugins, addonSection, "activeGMPlugins");
  601. this.renderPersona(addons, addonSection, "persona");
  602. let hasAddonData = Object.keys(ping.environment.addons).length > 0;
  603. this.createSubsection("addons", hasAddonData, addonSection, dataDiv);
  604. },
  605. appendRow: function(table, id, value) {
  606. let row = document.createElement("tr");
  607. this.appendColumn(row, "td", id);
  608. this.appendColumn(row, "td", value);
  609. table.appendChild(row);
  610. },
  611. /**
  612. * Helper function for appending a column to the data table.
  613. *
  614. * @param aRowElement Parent row element
  615. * @param aColType Column's tag name
  616. * @param aColText Column contents
  617. */
  618. appendColumn: function(aRowElement, aColType, aColText) {
  619. let colElement = document.createElement(aColType);
  620. let colTextElement = document.createTextNode(aColText);
  621. colElement.appendChild(colTextElement);
  622. aRowElement.appendChild(colElement);
  623. },
  624. };
  625. var TelLog = {
  626. /**
  627. * Renders the telemetry log
  628. */
  629. render: function(aPing) {
  630. let entries = aPing.payload.log;
  631. const hasData = entries && entries.length > 0;
  632. setHasData("telemetry-log-section", hasData);
  633. if (!hasData) {
  634. return;
  635. }
  636. let table = document.createElement("table");
  637. let caption = document.createElement("caption");
  638. let captionString = bundle.GetStringFromName("telemetryLogTitle");
  639. caption.appendChild(document.createTextNode(captionString + "\n"));
  640. table.appendChild(caption);
  641. let headings = document.createElement("tr");
  642. this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingId") + "\t");
  643. this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingTimestamp") + "\t");
  644. this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingData") + "\t");
  645. table.appendChild(headings);
  646. for (let entry of entries) {
  647. let row = document.createElement("tr");
  648. for (let elem of entry) {
  649. this.appendColumn(row, "td", elem + "\t");
  650. }
  651. table.appendChild(row);
  652. }
  653. let dataDiv = document.getElementById("telemetry-log");
  654. removeAllChildNodes(dataDiv);
  655. dataDiv.appendChild(table);
  656. },
  657. /**
  658. * Helper function for appending a column to the data table.
  659. *
  660. * @param aRowElement Parent row element
  661. * @param aColType Column's tag name
  662. * @param aColText Column contents
  663. */
  664. appendColumn: function(aRowElement, aColType, aColText) {
  665. let colElement = document.createElement(aColType);
  666. let colTextElement = document.createTextNode(aColText);
  667. colElement.appendChild(colTextElement);
  668. aRowElement.appendChild(colElement);
  669. },
  670. };
  671. var SlowSQL = {
  672. slowSqlHits: bundle.GetStringFromName("slowSqlHits"),
  673. slowSqlAverage: bundle.GetStringFromName("slowSqlAverage"),
  674. slowSqlStatement: bundle.GetStringFromName("slowSqlStatement"),
  675. mainThreadTitle: bundle.GetStringFromName("slowSqlMain"),
  676. otherThreadTitle: bundle.GetStringFromName("slowSqlOther"),
  677. /**
  678. * Render slow SQL statistics
  679. */
  680. render: function SlowSQL_render(aPing) {
  681. // We can add the debug SQL data to the current ping later.
  682. // However, we need to be careful to never send that debug data
  683. // out due to privacy concerns.
  684. // We want to show the actual ping data for archived pings,
  685. // so skip this there.
  686. let debugSlowSql = PingPicker.viewCurrentPingData && Preferences.get(PREF_DEBUG_SLOW_SQL, false);
  687. let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
  688. if (!slowSql) {
  689. setHasData("slow-sql-section", false);
  690. return;
  691. }
  692. let {mainThread, otherThreads} =
  693. debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
  694. let mainThreadCount = Object.keys(mainThread).length;
  695. let otherThreadCount = Object.keys(otherThreads).length;
  696. if (mainThreadCount == 0 && otherThreadCount == 0) {
  697. setHasData("slow-sql-section", false);
  698. return;
  699. }
  700. setHasData("slow-sql-section", true);
  701. if (debugSlowSql) {
  702. document.getElementById("sql-warning").classList.remove("hidden");
  703. }
  704. let slowSqlDiv = document.getElementById("slow-sql-tables");
  705. removeAllChildNodes(slowSqlDiv);
  706. // Main thread
  707. if (mainThreadCount > 0) {
  708. let table = document.createElement("table");
  709. this.renderTableHeader(table, this.mainThreadTitle);
  710. this.renderTable(table, mainThread);
  711. slowSqlDiv.appendChild(table);
  712. slowSqlDiv.appendChild(document.createElement("hr"));
  713. }
  714. // Other threads
  715. if (otherThreadCount > 0) {
  716. let table = document.createElement("table");
  717. this.renderTableHeader(table, this.otherThreadTitle);
  718. this.renderTable(table, otherThreads);
  719. slowSqlDiv.appendChild(table);
  720. slowSqlDiv.appendChild(document.createElement("hr"));
  721. }
  722. },
  723. /**
  724. * Creates a header row for a Slow SQL table
  725. * Tabs & newlines added to cells to make it easier to copy-paste.
  726. *
  727. * @param aTable Parent table element
  728. * @param aTitle Table's title
  729. */
  730. renderTableHeader: function SlowSQL_renderTableHeader(aTable, aTitle) {
  731. let caption = document.createElement("caption");
  732. caption.appendChild(document.createTextNode(aTitle + "\n"));
  733. aTable.appendChild(caption);
  734. let headings = document.createElement("tr");
  735. this.appendColumn(headings, "th", this.slowSqlHits + "\t");
  736. this.appendColumn(headings, "th", this.slowSqlAverage + "\t");
  737. this.appendColumn(headings, "th", this.slowSqlStatement + "\n");
  738. aTable.appendChild(headings);
  739. },
  740. /**
  741. * Fills out the table body
  742. * Tabs & newlines added to cells to make it easier to copy-paste.
  743. *
  744. * @param aTable Parent table element
  745. * @param aSql SQL stats object
  746. */
  747. renderTable: function SlowSQL_renderTable(aTable, aSql) {
  748. for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) {
  749. let averageTime = totalTime / hitCount;
  750. let sqlRow = document.createElement("tr");
  751. this.appendColumn(sqlRow, "td", hitCount + "\t");
  752. this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
  753. this.appendColumn(sqlRow, "td", sql + "\n");
  754. aTable.appendChild(sqlRow);
  755. }
  756. },
  757. /**
  758. * Helper function for appending a column to a Slow SQL table.
  759. *
  760. * @param aRowElement Parent row element
  761. * @param aColType Column's tag name
  762. * @param aColText Column contents
  763. */
  764. appendColumn: function SlowSQL_appendColumn(aRowElement, aColType, aColText) {
  765. let colElement = document.createElement(aColType);
  766. let colTextElement = document.createTextNode(aColText);
  767. colElement.appendChild(colTextElement);
  768. aRowElement.appendChild(colElement);
  769. }
  770. };
  771. var StackRenderer = {
  772. stackTitle: bundle.GetStringFromName("stackTitle"),
  773. memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"),
  774. /**
  775. * Outputs the memory map associated with this hang report
  776. *
  777. * @param aDiv Output div
  778. */
  779. renderMemoryMap: function StackRenderer_renderMemoryMap(aDiv, memoryMap) {
  780. aDiv.appendChild(document.createTextNode(this.memoryMapTitle));
  781. aDiv.appendChild(document.createElement("br"));
  782. for (let currentModule of memoryMap) {
  783. aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
  784. aDiv.appendChild(document.createElement("br"));
  785. }
  786. aDiv.appendChild(document.createElement("br"));
  787. },
  788. /**
  789. * Outputs the raw PCs from the hang's stack
  790. *
  791. * @param aDiv Output div
  792. * @param aStack Array of PCs from the hang stack
  793. */
  794. renderStack: function StackRenderer_renderStack(aDiv, aStack) {
  795. aDiv.appendChild(document.createTextNode(this.stackTitle));
  796. let stackText = " " + aStack.join(" ");
  797. aDiv.appendChild(document.createTextNode(stackText));
  798. aDiv.appendChild(document.createElement("br"));
  799. aDiv.appendChild(document.createElement("br"));
  800. },
  801. renderStacks: function StackRenderer_renderStacks(aPrefix, aStacks,
  802. aMemoryMap, aRenderHeader) {
  803. let div = document.getElementById(aPrefix + '-data');
  804. removeAllChildNodes(div);
  805. let fetchE = document.getElementById(aPrefix + '-fetch-symbols');
  806. if (fetchE) {
  807. fetchE.classList.remove("hidden");
  808. }
  809. let hideE = document.getElementById(aPrefix + '-hide-symbols');
  810. if (hideE) {
  811. hideE.classList.add("hidden");
  812. }
  813. if (aStacks.length == 0) {
  814. return;
  815. }
  816. setHasData(aPrefix + '-section', true);
  817. this.renderMemoryMap(div, aMemoryMap);
  818. for (let i = 0; i < aStacks.length; ++i) {
  819. let stack = aStacks[i];
  820. aRenderHeader(i);
  821. this.renderStack(div, stack)
  822. }
  823. },
  824. /**
  825. * Renders the title of the stack: e.g. "Late Write #1" or
  826. * "Hang Report #1 (6 seconds)".
  827. *
  828. * @param aFormatArgs formating args to be passed to formatStringFromName.
  829. */
  830. renderHeader: function StackRenderer_renderHeader(aPrefix, aFormatArgs) {
  831. let div = document.getElementById(aPrefix + "-data");
  832. let titleElement = document.createElement("span");
  833. titleElement.className = "stack-title";
  834. let titleText = bundle.formatStringFromName(
  835. aPrefix + "-title", aFormatArgs, aFormatArgs.length);
  836. titleElement.appendChild(document.createTextNode(titleText));
  837. div.appendChild(titleElement);
  838. div.appendChild(document.createElement("br"));
  839. }
  840. };
  841. var RawPayload = {
  842. /**
  843. * Renders the raw payload
  844. */
  845. render: function(aPing) {
  846. setHasData("raw-payload-section", true);
  847. let pre = document.getElementById("raw-payload-data-pre");
  848. pre.textContent = JSON.stringify(aPing.payload, null, 2);
  849. }
  850. };
  851. function SymbolicationRequest(aPrefix, aRenderHeader,
  852. aMemoryMap, aStacks, aDurations = null) {
  853. this.prefix = aPrefix;
  854. this.renderHeader = aRenderHeader;
  855. this.memoryMap = aMemoryMap;
  856. this.stacks = aStacks;
  857. this.durations = aDurations;
  858. }
  859. /**
  860. * A callback for onreadystatechange. It replaces the numeric stack with
  861. * the symbolicated one returned by the symbolication server.
  862. */
  863. SymbolicationRequest.prototype.handleSymbolResponse =
  864. function SymbolicationRequest_handleSymbolResponse() {
  865. if (this.symbolRequest.readyState != 4)
  866. return;
  867. let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
  868. fetchElement.classList.add("hidden");
  869. let hideElement = document.getElementById(this.prefix + "-hide-symbols");
  870. hideElement.classList.remove("hidden");
  871. let div = document.getElementById(this.prefix + "-data");
  872. removeAllChildNodes(div);
  873. let errorMessage = bundle.GetStringFromName("errorFetchingSymbols");
  874. if (this.symbolRequest.status != 200) {
  875. div.appendChild(document.createTextNode(errorMessage));
  876. return;
  877. }
  878. let jsonResponse = {};
  879. try {
  880. jsonResponse = JSON.parse(this.symbolRequest.responseText);
  881. } catch (e) {
  882. div.appendChild(document.createTextNode(errorMessage));
  883. return;
  884. }
  885. for (let i = 0; i < jsonResponse.length; ++i) {
  886. let stack = jsonResponse[i];
  887. this.renderHeader(i, this.durations);
  888. for (let symbol of stack) {
  889. div.appendChild(document.createTextNode(symbol));
  890. div.appendChild(document.createElement("br"));
  891. }
  892. div.appendChild(document.createElement("br"));
  893. }
  894. };
  895. /**
  896. * Send a request to the symbolication server to symbolicate this stack.
  897. */
  898. SymbolicationRequest.prototype.fetchSymbols =
  899. function SymbolicationRequest_fetchSymbols() {
  900. let symbolServerURI =
  901. Preferences.get(PREF_SYMBOL_SERVER_URI, DEFAULT_SYMBOL_SERVER_URI);
  902. let request = {"memoryMap" : this.memoryMap, "stacks" : this.stacks,
  903. "version" : 3};
  904. let requestJSON = JSON.stringify(request);
  905. this.symbolRequest = new XMLHttpRequest();
  906. this.symbolRequest.open("POST", symbolServerURI, true);
  907. this.symbolRequest.setRequestHeader("Content-type", "application/json");
  908. this.symbolRequest.setRequestHeader("Content-length",
  909. requestJSON.length);
  910. this.symbolRequest.setRequestHeader("Connection", "close");
  911. this.symbolRequest.onreadystatechange = this.handleSymbolResponse.bind(this);
  912. this.symbolRequest.send(requestJSON);
  913. }
  914. var ChromeHangs = {
  915. symbolRequest: null,
  916. /**
  917. * Renders raw chrome hang data
  918. */
  919. render: function ChromeHangs_render(aPing) {
  920. let hangs = aPing.payload.chromeHangs;
  921. setHasData("chrome-hangs-section", !!hangs);
  922. if (!hangs) {
  923. return;
  924. }
  925. let stacks = hangs.stacks;
  926. let memoryMap = hangs.memoryMap;
  927. let durations = hangs.durations;
  928. StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap,
  929. (index) => this.renderHangHeader(index, durations));
  930. },
  931. renderHangHeader: function ChromeHangs_renderHangHeader(aIndex, aDurations) {
  932. StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, aDurations[aIndex]]);
  933. }
  934. };
  935. var ThreadHangStats = {
  936. /**
  937. * Renders raw thread hang stats data
  938. */
  939. render: function(aPayload) {
  940. let div = document.getElementById("thread-hang-stats");
  941. removeAllChildNodes(div);
  942. let stats = aPayload.threadHangStats;
  943. setHasData("thread-hang-stats-section", stats && (stats.length > 0));
  944. if (!stats) {
  945. return;
  946. }
  947. stats.forEach((thread) => {
  948. div.appendChild(this.renderThread(thread));
  949. });
  950. },
  951. /**
  952. * Creates and fills data corresponding to a thread
  953. */
  954. renderThread: function(aThread) {
  955. let div = document.createElement("div");
  956. let title = document.createElement("h2");
  957. title.textContent = aThread.name;
  958. div.appendChild(title);
  959. // Don't localize the histogram name, because the
  960. // name is also used as the div element's ID
  961. Histogram.render(div, aThread.name + "-Activity",
  962. aThread.activity, {exponential: true}, true);
  963. aThread.hangs.forEach((hang, index) => {
  964. let hangName = aThread.name + "-Hang-" + (index + 1);
  965. let hangDiv = Histogram.render(
  966. div, hangName, hang.histogram, {exponential: true}, true);
  967. let stackDiv = document.createElement("div");
  968. let stack = hang.nativeStack || hang.stack;
  969. stack.forEach((frame) => {
  970. stackDiv.appendChild(document.createTextNode(frame));
  971. // Leave an extra <br> at the end of the stack listing
  972. stackDiv.appendChild(document.createElement("br"));
  973. });
  974. // Insert stack after the histogram title
  975. hangDiv.insertBefore(stackDiv, hangDiv.childNodes[1]);
  976. });
  977. return div;
  978. },
  979. };
  980. var Histogram = {
  981. hgramSamplesCaption: bundle.GetStringFromName("histogramSamples"),
  982. hgramAverageCaption: bundle.GetStringFromName("histogramAverage"),
  983. hgramSumCaption: bundle.GetStringFromName("histogramSum"),
  984. hgramCopyCaption: bundle.GetStringFromName("histogramCopy"),
  985. /**
  986. * Renders a single Telemetry histogram
  987. *
  988. * @param aParent Parent element
  989. * @param aName Histogram name
  990. * @param aHgram Histogram information
  991. * @param aOptions Object with render options
  992. * * exponential: bars follow logarithmic scale
  993. * @param aIsBHR whether or not requires fixing the labels for TimeHistogram
  994. */
  995. render: function Histogram_render(aParent, aName, aHgram, aOptions, aIsBHR) {
  996. let options = aOptions || {};
  997. let hgram = this.processHistogram(aHgram, aName, aIsBHR);
  998. let outerDiv = document.createElement("div");
  999. outerDiv.className = "histogram";
  1000. outerDiv.id = aName;
  1001. let divTitle = document.createElement("div");
  1002. divTitle.className = "histogram-title";
  1003. divTitle.appendChild(document.createTextNode(aName));
  1004. outerDiv.appendChild(divTitle);
  1005. let stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " +
  1006. this.hgramAverageCaption + " = " + hgram.pretty_average + ", " +
  1007. this.hgramSumCaption + " = " + hgram.sum;
  1008. let divStats = document.createElement("div");
  1009. divStats.appendChild(document.createTextNode(stats));
  1010. outerDiv.appendChild(divStats);
  1011. if (isRTL()) {
  1012. hgram.buckets.reverse();
  1013. hgram.values.reverse();
  1014. }
  1015. let textData = this.renderValues(outerDiv, hgram, options);
  1016. // The 'Copy' button contains the textual data, copied to clipboard on click
  1017. let copyButton = document.createElement("button");
  1018. copyButton.className = "copy-node";
  1019. copyButton.appendChild(document.createTextNode(this.hgramCopyCaption));
  1020. copyButton.histogramText = aName + EOL + stats + EOL + EOL + textData;
  1021. copyButton.addEventListener("click", function() {
  1022. Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper)
  1023. .copyString(this.histogramText);
  1024. });
  1025. outerDiv.appendChild(copyButton);
  1026. aParent.appendChild(outerDiv);
  1027. return outerDiv;
  1028. },
  1029. processHistogram: function(aHgram, aName, aIsBHR) {
  1030. const values = Object.keys(aHgram.values).map(k => aHgram.values[k]);
  1031. if (!values.length) {
  1032. // If we have no values collected for this histogram, just return
  1033. // zero values so we still render it.
  1034. return {
  1035. values: [],
  1036. pretty_average: 0,
  1037. max: 0,
  1038. sample_count: 0,
  1039. sum: 0
  1040. };
  1041. }
  1042. const sample_count = values.reduceRight((a, b) => a + b);
  1043. const average = Math.round(aHgram.sum * 10 / sample_count) / 10;
  1044. const max_value = Math.max(...values);
  1045. function labelFunc(k) {
  1046. // - BHR histograms are TimeHistograms: Exactly power-of-two buckets (from 0)
  1047. // (buckets: [0..1], [2..3], [4..7], [8..15], ... note the 0..1 anomaly - same bucket)
  1048. // - TimeHistogram's JS representation adds a dummy (empty) "0" bucket, and
  1049. // the rest of the buckets have the label as the upper value of the
  1050. // bucket (non TimeHistograms have the lower value of the bucket as label).
  1051. // So JS TimeHistograms bucket labels are: 0 (dummy), 1, 3, 7, 15, ...
  1052. // - see toolkit/components/telemetry/Telemetry.cpp
  1053. // (CreateJSTimeHistogram, CreateJSThreadHangStats, CreateJSHangHistogram)
  1054. // - see toolkit/components/telemetry/ThreadHangStats.h
  1055. // Fix BHR labels to the "standard" format for about:telemetry as follows:
  1056. // - The dummy 0 label+bucket will be filtered before arriving here
  1057. // - If it's 1 -> manually correct it to 0 (the 0..1 anomaly)
  1058. // - For the rest, set the label as the bottom value instead of the upper.
  1059. // --> so we'll end with the following (non dummy) labels: 0, 2, 4, 8, 16, ...
  1060. if (!aIsBHR) {
  1061. return k;
  1062. }
  1063. return k == 1 ? 0 : (k + 1) / 2;
  1064. }
  1065. const labelledValues = Object.keys(aHgram.values)
  1066. .filter(label => !aIsBHR || Number(label) != 0) // remove dummy 0 label for BHR
  1067. .map(k => [labelFunc(Number(k)), aHgram.values[k]]);
  1068. let result = {
  1069. values: labelledValues,
  1070. pretty_average: average,
  1071. max: max_value,
  1072. sample_count: sample_count,
  1073. sum: aHgram.sum
  1074. };
  1075. return result;
  1076. },
  1077. /**
  1078. * Return a non-negative, logarithmic representation of a non-negative number.
  1079. * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
  1080. *
  1081. * @param aNumber Non-negative number
  1082. */
  1083. getLogValue: function(aNumber) {
  1084. return Math.max(0, Math.log10(aNumber) + 1);
  1085. },
  1086. /**
  1087. * Create histogram HTML bars, also returns a textual representation
  1088. * Both aMaxValue and aSumValues must be positive.
  1089. * Values are assumed to use 0 as baseline.
  1090. *
  1091. * @param aDiv Outer parent div
  1092. * @param aHgram The histogram data
  1093. * @param aOptions Object with render options (@see #render)
  1094. */
  1095. renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
  1096. let text = "";
  1097. // If the last label is not the longest string, alignment will break a little
  1098. let labelPadTo = 0;
  1099. if (aHgram.values.length) {
  1100. labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
  1101. }
  1102. let maxBarValue = aOptions.exponential ? this.getLogValue(aHgram.max) : aHgram.max;
  1103. for (let [label, value] of aHgram.values) {
  1104. let barValue = aOptions.exponential ? this.getLogValue(value) : value;
  1105. // Create a text representation: <right-aligned-label> |<bar-of-#><value> <percentage>
  1106. text += EOL
  1107. + " ".repeat(Math.max(0, labelPadTo - String(label).length)) + label // Right-aligned label
  1108. + " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar
  1109. + " " + value // Value
  1110. + " " + Math.round(100 * value / aHgram.sample_count) + "%"; // Percentage
  1111. // Construct the HTML labels + bars
  1112. let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
  1113. let aboveEm = MAX_BAR_HEIGHT - belowEm;
  1114. let barDiv = document.createElement("div");
  1115. barDiv.className = "bar";
  1116. barDiv.style.paddingTop = aboveEm + "em";
  1117. // Add value label or an nbsp if no value
  1118. barDiv.appendChild(document.createTextNode(value ? value : '\u00A0'));
  1119. // Create the blue bar
  1120. let bar = document.createElement("div");
  1121. bar.className = "bar-inner";
  1122. bar.style.height = belowEm + "em";
  1123. barDiv.appendChild(bar);
  1124. // Add bucket label
  1125. barDiv.appendChild(document.createTextNode(label));
  1126. aDiv.appendChild(barDiv);
  1127. }
  1128. return text.substr(EOL.length); // Trim the EOL before the first line
  1129. },
  1130. /**
  1131. * Helper function for filtering histogram elements by their id
  1132. * Adds the "filter-blocked" class to histogram nodes whose IDs don't match the filter.
  1133. *
  1134. * @param aContainerNode Container node containing the histogram class nodes to filter
  1135. * @param aFilterText either text or /RegEx/. If text, case-insensitive and AND words
  1136. */
  1137. filterHistograms: function _filterHistograms(aContainerNode, aFilterText) {
  1138. let filter = aFilterText.toString();
  1139. // Pass if: all non-empty array items match (case-sensitive)
  1140. function isPassText(subject, filter) {
  1141. for (let item of filter) {
  1142. if (item.length && subject.indexOf(item) < 0) {
  1143. return false; // mismatch and not a spurious space
  1144. }
  1145. }
  1146. return true;
  1147. }
  1148. function isPassRegex(subject, filter) {
  1149. return filter.test(subject);
  1150. }
  1151. // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
  1152. let isPassFunc; // filter function, set once, then applied to all elements
  1153. filter = filter.trim();
  1154. if (filter[0] != "/") { // Plain text: case insensitive, AND if multi-string
  1155. isPassFunc = isPassText;
  1156. filter = filter.toLowerCase().split(" ");
  1157. } else {
  1158. isPassFunc = isPassRegex;
  1159. var r = filter.match(/^\/(.*)\/(i?)$/);
  1160. try {
  1161. filter = RegExp(r[1], r[2]);
  1162. }
  1163. catch (e) { // Incomplete or bad RegExp - always no match
  1164. isPassFunc = function() {
  1165. return false;
  1166. };
  1167. }
  1168. }
  1169. let needLower = (isPassFunc === isPassText);
  1170. let histograms = aContainerNode.getElementsByClassName("histogram");
  1171. for (let hist of histograms) {
  1172. hist.classList[isPassFunc((needLower ? hist.id.toLowerCase() : hist.id), filter) ? "remove" : "add"]("filter-blocked");
  1173. }
  1174. },
  1175. /**
  1176. * Event handler for change at histograms filter input
  1177. *
  1178. * When invoked, 'this' is expected to be the filter HTML node.
  1179. */
  1180. histogramFilterChanged: function _histogramFilterChanged() {
  1181. if (this.idleTimeout) {
  1182. clearTimeout(this.idleTimeout);
  1183. }
  1184. this.idleTimeout = setTimeout( () => {
  1185. Histogram.filterHistograms(document.getElementById(this.getAttribute("target_id")), this.value);
  1186. }, FILTER_IDLE_TIMEOUT);
  1187. }
  1188. };
  1189. /*
  1190. * Helper function to render JS objects with white space between top level elements
  1191. * so that they look better in the browser
  1192. * @param aObject JavaScript object or array to render
  1193. * @return String
  1194. */
  1195. function RenderObject(aObject) {
  1196. let output = "";
  1197. if (Array.isArray(aObject)) {
  1198. if (aObject.length == 0) {
  1199. return "[]";
  1200. }
  1201. output = "[" + JSON.stringify(aObject[0]);
  1202. for (let i = 1; i < aObject.length; i++) {
  1203. output += ", " + JSON.stringify(aObject[i]);
  1204. }
  1205. return output + "]";
  1206. }
  1207. let keys = Object.keys(aObject);
  1208. if (keys.length == 0) {
  1209. return "{}";
  1210. }
  1211. output = "{\"" + keys[0] + "\":\u00A0" + JSON.stringify(aObject[keys[0]]);
  1212. for (let i = 1; i < keys.length; i++) {
  1213. output += ", \"" + keys[i] + "\":\u00A0" + JSON.stringify(aObject[keys[i]]);
  1214. }
  1215. return output + "}";
  1216. }
  1217. var KeyValueTable = {
  1218. /**
  1219. * Returns a 2-column table with keys and values
  1220. * @param aMeasurements Each key in this JS object is rendered as a row in
  1221. * the table with its corresponding value
  1222. * @param aKeysLabel Column header for the keys column
  1223. * @param aValuesLabel Column header for the values column
  1224. */
  1225. render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) {
  1226. let table = document.createElement("table");
  1227. this.renderHeader(table, aKeysLabel, aValuesLabel);
  1228. this.renderBody(table, aMeasurements);
  1229. return table;
  1230. },
  1231. /**
  1232. * Create the table header
  1233. * Tabs & newlines added to cells to make it easier to copy-paste.
  1234. *
  1235. * @param aTable Table element
  1236. * @param aKeysLabel Column header for the keys column
  1237. * @param aValuesLabel Column header for the values column
  1238. */
  1239. renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) {
  1240. let headerRow = document.createElement("tr");
  1241. aTable.appendChild(headerRow);
  1242. let keysColumn = document.createElement("th");
  1243. keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t"));
  1244. let valuesColumn = document.createElement("th");
  1245. valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n"));
  1246. headerRow.appendChild(keysColumn);
  1247. headerRow.appendChild(valuesColumn);
  1248. },
  1249. /**
  1250. * Create the table body
  1251. * Tabs & newlines added to cells to make it easier to copy-paste.
  1252. *
  1253. * @param aTable Table element
  1254. * @param aMeasurements Key/value map
  1255. */
  1256. renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) {
  1257. for (let [key, value] of Object.entries(aMeasurements)) {
  1258. // use .valueOf() to unbox Number, String, etc. objects
  1259. if (value &&
  1260. (typeof value == "object") &&
  1261. (typeof value.valueOf() == "object")) {
  1262. value = RenderObject(value);
  1263. }
  1264. let newRow = document.createElement("tr");
  1265. aTable.appendChild(newRow);
  1266. let keyField = document.createElement("td");
  1267. keyField.appendChild(document.createTextNode(key + "\t"));
  1268. newRow.appendChild(keyField);
  1269. let valueField = document.createElement("td");
  1270. valueField.appendChild(document.createTextNode(value + "\n"));
  1271. newRow.appendChild(valueField);
  1272. }
  1273. }
  1274. };
  1275. var GenericTable = {
  1276. /**
  1277. * Returns a n-column table.
  1278. * @param rows An array of arrays, each containing data to render
  1279. * for one row.
  1280. * @param headings The column header strings.
  1281. */
  1282. render: function(rows, headings) {
  1283. let table = document.createElement("table");
  1284. this.renderHeader(table, headings);
  1285. this.renderBody(table, rows);
  1286. return table;
  1287. },
  1288. /**
  1289. * Create the table header.
  1290. * Tabs & newlines added to cells to make it easier to copy-paste.
  1291. *
  1292. * @param table Table element
  1293. * @param headings Array of column header strings.
  1294. */
  1295. renderHeader: function(table, headings) {
  1296. let headerRow = document.createElement("tr");
  1297. table.appendChild(headerRow);
  1298. for (let i = 0; i < headings.length; ++i) {
  1299. let suffix = (i == (headings.length - 1)) ? "\n" : "\t";
  1300. let column = document.createElement("th");
  1301. column.appendChild(document.createTextNode(headings[i] + suffix));
  1302. headerRow.appendChild(column);
  1303. }
  1304. },
  1305. /**
  1306. * Create the table body
  1307. * Tabs & newlines added to cells to make it easier to copy-paste.
  1308. *
  1309. * @param table Table element
  1310. * @param rows An array of arrays, each containing data to render
  1311. * for one row.
  1312. */
  1313. renderBody: function(table, rows) {
  1314. for (let row of rows) {
  1315. row = row.map(value => {
  1316. // use .valueOf() to unbox Number, String, etc. objects
  1317. if (value &&
  1318. (typeof value == "object") &&
  1319. (typeof value.valueOf() == "object")) {
  1320. return RenderObject(value);
  1321. }
  1322. return value;
  1323. });
  1324. let newRow = document.createElement("tr");
  1325. table.appendChild(newRow);
  1326. for (let i = 0; i < row.length; ++i) {
  1327. let suffix = (i == (row.length - 1)) ? "\n" : "\t";
  1328. let field = document.createElement("td");
  1329. field.appendChild(document.createTextNode(row[i] + suffix));
  1330. newRow.appendChild(field);
  1331. }
  1332. }
  1333. }
  1334. };
  1335. var KeyedHistogram = {
  1336. render: function(parent, id, keyedHistogram) {
  1337. let outerDiv = document.createElement("div");
  1338. outerDiv.className = "keyed-histogram";
  1339. outerDiv.id = id;
  1340. let divTitle = document.createElement("div");
  1341. divTitle.className = "keyed-histogram-title";
  1342. divTitle.appendChild(document.createTextNode(id));
  1343. outerDiv.appendChild(divTitle);
  1344. for (let [name, hgram] of Object.entries(keyedHistogram)) {
  1345. Histogram.render(outerDiv, name, hgram);
  1346. }
  1347. parent.appendChild(outerDiv);
  1348. return outerDiv;
  1349. },
  1350. };
  1351. var AddonDetails = {
  1352. tableIDTitle: bundle.GetStringFromName("addonTableID"),
  1353. tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"),
  1354. /**
  1355. * Render the addon details section as a series of headers followed by key/value tables
  1356. * @param aPing A ping object to render the data from.
  1357. */
  1358. render: function AddonDetails_render(aPing) {
  1359. let addonSection = document.getElementById("addon-details");
  1360. removeAllChildNodes(addonSection);
  1361. let addonDetails = aPing.payload.addonDetails;
  1362. const hasData = addonDetails && Object.keys(addonDetails).length > 0;
  1363. setHasData("addon-details-section", hasData);
  1364. if (!hasData) {
  1365. return;
  1366. }
  1367. for (let provider in addonDetails) {
  1368. let providerSection = document.createElement("h2");
  1369. let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
  1370. providerSection.appendChild(document.createTextNode(titleText));
  1371. addonSection.appendChild(providerSection);
  1372. addonSection.appendChild(
  1373. KeyValueTable.render(addonDetails[provider],
  1374. this.tableIDTitle, this.tableDetailsTitle));
  1375. }
  1376. }
  1377. };
  1378. var Scalars = {
  1379. /**
  1380. * Render the scalar data - if present - from the payload in a simple key-value table.
  1381. * @param aPayload A payload object to render the data from.
  1382. */
  1383. render: function(aPayload) {
  1384. let scalarsSection = document.getElementById("scalars");
  1385. removeAllChildNodes(scalarsSection);
  1386. if (!aPayload.processes || !aPayload.processes.parent) {
  1387. return;
  1388. }
  1389. let scalars = aPayload.processes.parent.scalars;
  1390. const hasData = scalars && Object.keys(scalars).length > 0;
  1391. setHasData("scalars-section", hasData);
  1392. if (!hasData) {
  1393. return;
  1394. }
  1395. const headingName = bundle.GetStringFromName("namesHeader");
  1396. const headingValue = bundle.GetStringFromName("valuesHeader");
  1397. const table = KeyValueTable.render(scalars, headingName, headingValue);
  1398. scalarsSection.appendChild(table);
  1399. }
  1400. };
  1401. var KeyedScalars = {
  1402. /**
  1403. * Render the keyed scalar data - if present - from the payload in a simple key-value table.
  1404. * @param aPayload A payload object to render the data from.
  1405. */
  1406. render: function(aPayload) {
  1407. let scalarsSection = document.getElementById("keyed-scalars");
  1408. removeAllChildNodes(scalarsSection);
  1409. if (!aPayload.processes || !aPayload.processes.parent) {
  1410. return;
  1411. }
  1412. let keyedScalars = aPayload.processes.parent.keyedScalars;
  1413. const hasData = keyedScalars && Object.keys(keyedScalars).length > 0;
  1414. setHasData("keyed-scalars-section", hasData);
  1415. if (!hasData) {
  1416. return;
  1417. }
  1418. const headingName = bundle.GetStringFromName("namesHeader");
  1419. const headingValue = bundle.GetStringFromName("valuesHeader");
  1420. for (let scalar in keyedScalars) {
  1421. // Add the name of the scalar.
  1422. let scalarNameSection = document.createElement("h2");
  1423. scalarNameSection.appendChild(document.createTextNode(scalar));
  1424. scalarsSection.appendChild(scalarNameSection);
  1425. // Populate the section with the key-value pairs from the scalar.
  1426. const table = KeyValueTable.render(keyedScalars[scalar], headingName, headingValue);
  1427. scalarsSection.appendChild(table);
  1428. }
  1429. }
  1430. };
  1431. var Events = {
  1432. /**
  1433. * Render the event data - if present - from the payload in a simple table.
  1434. * @param aPayload A payload object to render the data from.
  1435. */
  1436. render: function(aPayload) {
  1437. let eventsSection = document.getElementById("events");
  1438. removeAllChildNodes(eventsSection);
  1439. if (!aPayload.processes || !aPayload.processes.parent) {
  1440. return;
  1441. }
  1442. const events = aPayload.processes.parent.events;
  1443. const hasData = events && Object.keys(events).length > 0;
  1444. setHasData("events-section", hasData);
  1445. if (!hasData) {
  1446. return;
  1447. }
  1448. const headings = [
  1449. "timestamp",
  1450. "category",
  1451. "method",
  1452. "object",
  1453. "value",
  1454. "extra",
  1455. ];
  1456. const table = GenericTable.render(events, headings);
  1457. eventsSection.appendChild(table);
  1458. }
  1459. };
  1460. /**
  1461. * Helper function for showing either the toggle element or "No data collected" message for a section
  1462. *
  1463. * @param aSectionID ID of the section element that needs to be changed
  1464. * @param aHasData true (default) indicates that toggle should be displayed
  1465. */
  1466. function setHasData(aSectionID, aHasData) {
  1467. let sectionElement = document.getElementById(aSectionID);
  1468. sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
  1469. }
  1470. /**
  1471. * Helper function that expands and collapses sections +
  1472. * changes caption on the toggle text
  1473. */
  1474. function toggleSection(aEvent) {
  1475. let parentElement = aEvent.target.parentElement;
  1476. if (!parentElement.classList.contains("has-data") &&
  1477. !parentElement.classList.contains("has-subdata")) {
  1478. return; // nothing to toggle
  1479. }
  1480. parentElement.classList.toggle("expanded");
  1481. // Store section opened/closed state in a hidden checkbox (which is then used on reload)
  1482. let statebox = parentElement.getElementsByClassName("statebox")[0];
  1483. if (statebox) {
  1484. statebox.checked = parentElement.classList.contains("expanded");
  1485. }
  1486. }
  1487. /**
  1488. * Sets the text of the page header based on a config pref + bundle strings
  1489. */
  1490. function setupPageHeader()
  1491. {
  1492. let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
  1493. let brandName = brandBundle.GetStringFromName("brandFullName");
  1494. let subtitleText = bundle.formatStringFromName(
  1495. "pageSubtitle", [serverOwner, brandName], 2);
  1496. let subtitleElement = document.getElementById("page-subtitle");
  1497. subtitleElement.appendChild(document.createTextNode(subtitleText));
  1498. }
  1499. /**
  1500. * Initializes load/unload, pref change and mouse-click listeners
  1501. */
  1502. function setupListeners() {
  1503. Settings.attachObservers();
  1504. PingPicker.attachObservers();
  1505. // Clean up observers when page is closed
  1506. window.addEventListener("unload",
  1507. function unloadHandler(aEvent) {
  1508. window.removeEventListener("unload", unloadHandler);
  1509. Settings.detachObservers();
  1510. }, false);
  1511. document.getElementById("chrome-hangs-fetch-symbols").addEventListener("click",
  1512. function () {
  1513. if (!gPingData) {
  1514. return;
  1515. }
  1516. let hangs = gPingData.payload.chromeHangs;
  1517. let req = new SymbolicationRequest("chrome-hangs",
  1518. ChromeHangs.renderHangHeader,
  1519. hangs.memoryMap,
  1520. hangs.stacks,
  1521. hangs.durations);
  1522. req.fetchSymbols();
  1523. }, false);
  1524. document.getElementById("chrome-hangs-hide-symbols").addEventListener("click",
  1525. function () {
  1526. if (!gPingData) {
  1527. return;
  1528. }
  1529. ChromeHangs.render(gPingData);
  1530. }, false);
  1531. document.getElementById("late-writes-fetch-symbols").addEventListener("click",
  1532. function () {
  1533. if (!gPingData) {
  1534. return;
  1535. }
  1536. let lateWrites = gPingData.payload.lateWrites;
  1537. let req = new SymbolicationRequest("late-writes",
  1538. LateWritesSingleton.renderHeader,
  1539. lateWrites.memoryMap,
  1540. lateWrites.stacks);
  1541. req.fetchSymbols();
  1542. }, false);
  1543. document.getElementById("late-writes-hide-symbols").addEventListener("click",
  1544. function () {
  1545. if (!gPingData) {
  1546. return;
  1547. }
  1548. LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
  1549. }, false);
  1550. // Clicking on the section name will toggle its state
  1551. let sectionHeaders = document.getElementsByClassName("section-name");
  1552. for (let sectionHeader of sectionHeaders) {
  1553. sectionHeader.addEventListener("click", toggleSection, false);
  1554. }
  1555. // Clicking on the "toggle" text will also toggle section's state
  1556. let toggleLinks = document.getElementsByClassName("toggle-caption");
  1557. for (let toggleLink of toggleLinks) {
  1558. toggleLink.addEventListener("click", toggleSection, false);
  1559. }
  1560. }
  1561. function onLoad() {
  1562. window.removeEventListener("load", onLoad);
  1563. // Set the text in the page header
  1564. setupPageHeader();
  1565. // Set up event listeners
  1566. setupListeners();
  1567. // Render settings.
  1568. Settings.render();
  1569. // Restore sections states
  1570. let stateboxes = document.getElementsByClassName("statebox");
  1571. for (let box of stateboxes) {
  1572. if (box.checked) { // Was open. Will still display as empty if not has-data
  1573. box.parentElement.classList.add("expanded");
  1574. }
  1575. }
  1576. // Update ping data when async Telemetry init is finished.
  1577. Telemetry.asyncFetchTelemetryData(() => PingPicker.update());
  1578. }
  1579. var LateWritesSingleton = {
  1580. renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
  1581. StackRenderer.renderHeader("late-writes", [aIndex + 1]);
  1582. },
  1583. renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
  1584. setHasData("late-writes-section", !!lateWrites);
  1585. if (!lateWrites) {
  1586. return;
  1587. }
  1588. let stacks = lateWrites.stacks;
  1589. let memoryMap = lateWrites.memoryMap;
  1590. StackRenderer.renderStacks('late-writes', stacks, memoryMap,
  1591. LateWritesSingleton.renderHeader);
  1592. }
  1593. };
  1594. /**
  1595. * Helper function for sorting the startup milestones in the Simple Measurements
  1596. * section into temporal order.
  1597. *
  1598. * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
  1599. * @return Sorted measurements
  1600. */
  1601. function sortStartupMilestones(aSimpleMeasurements) {
  1602. const telemetryTimestamps = TelemetryTimestamps.get();
  1603. let startupEvents = Services.startup.getStartupInfo();
  1604. delete startupEvents['process'];
  1605. function keyIsMilestone(k) {
  1606. return (k in startupEvents) || (k in telemetryTimestamps);
  1607. }
  1608. let sortedKeys = Object.keys(aSimpleMeasurements);
  1609. // Sort the measurements, with startup milestones at the front + ordered by time
  1610. sortedKeys.sort(function keyCompare(keyA, keyB) {
  1611. let isKeyAMilestone = keyIsMilestone(keyA);
  1612. let isKeyBMilestone = keyIsMilestone(keyB);
  1613. // First order by startup vs non-startup measurement
  1614. if (isKeyAMilestone && !isKeyBMilestone)
  1615. return -1;
  1616. if (!isKeyAMilestone && isKeyBMilestone)
  1617. return 1;
  1618. // Don't change order of non-startup measurements
  1619. if (!isKeyAMilestone && !isKeyBMilestone)
  1620. return 0;
  1621. // If both keys are startup measurements, order them by value
  1622. return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
  1623. });
  1624. // Insert measurements into a result object in sort-order
  1625. let result = {};
  1626. for (let key of sortedKeys) {
  1627. result[key] = aSimpleMeasurements[key];
  1628. }
  1629. return result;
  1630. }
  1631. function renderProcessList(ping, selectEl) {
  1632. removeAllChildNodes(selectEl);
  1633. let option = document.createElement("option");
  1634. option.appendChild(document.createTextNode("parent"));
  1635. option.setAttribute("value", "");
  1636. option.selected = true;
  1637. selectEl.appendChild(option);
  1638. if (!("processes" in ping.payload)) {
  1639. selectEl.disabled = true;
  1640. return;
  1641. }
  1642. selectEl.disabled = false;
  1643. for (let process of Object.keys(ping.payload.processes)) {
  1644. // TODO: parent hgrams are on root payload, not in payload.processes.parent
  1645. // When/If that gets moved, you'll need to remove this:
  1646. if (process === "parent") {
  1647. continue;
  1648. }
  1649. option = document.createElement("option");
  1650. option.appendChild(document.createTextNode(process));
  1651. option.setAttribute("value", process);
  1652. selectEl.appendChild(option);
  1653. }
  1654. }
  1655. function renderPayloadList(ping) {
  1656. // Rebuild the payload select with options:
  1657. // Parent Payload (selected)
  1658. // Child Payload 1..ping.payload.childPayloads.length
  1659. let listEl = document.getElementById("choose-payload");
  1660. removeAllChildNodes(listEl);
  1661. let option = document.createElement("option");
  1662. let text = bundle.GetStringFromName("parentPayload");
  1663. let content = document.createTextNode(text);
  1664. let payloadIndex = 0;
  1665. option.appendChild(content);
  1666. option.setAttribute("value", payloadIndex++);
  1667. option.selected = true;
  1668. listEl.appendChild(option);
  1669. if (!ping.payload.childPayloads) {
  1670. listEl.disabled = true;
  1671. return
  1672. }
  1673. listEl.disabled = false;
  1674. for (; payloadIndex <= ping.payload.childPayloads.length; ++payloadIndex) {
  1675. option = document.createElement("option");
  1676. text = bundle.formatStringFromName("childPayloadN", [payloadIndex], 1);
  1677. content = document.createTextNode(text);
  1678. option.appendChild(content);
  1679. option.setAttribute("value", payloadIndex);
  1680. listEl.appendChild(option);
  1681. }
  1682. }
  1683. function toggleElementHidden(element, isHidden) {
  1684. if (isHidden) {
  1685. element.classList.add("hidden");
  1686. } else {
  1687. element.classList.remove("hidden");
  1688. }
  1689. }
  1690. function togglePingSections(isMainPing) {
  1691. // We always show the sections that are "common" to all pings.
  1692. // The raw payload section is only used for pings other than "main" and "saved-session".
  1693. let commonSections = new Set(["general-data-section", "environment-data-section"]);
  1694. let otherPingSections = new Set(["raw-payload-section"]);
  1695. let elements = document.getElementById("structured-ping-data-section").children;
  1696. for (let section of elements) {
  1697. if (commonSections.has(section.id)) {
  1698. continue;
  1699. }
  1700. let showElement = isMainPing != otherPingSections.has(section.id);
  1701. toggleElementHidden(section, !showElement);
  1702. }
  1703. }
  1704. function displayPingData(ping, updatePayloadList = false) {
  1705. gPingData = ping;
  1706. // Render raw ping data.
  1707. let pre = document.getElementById("raw-ping-data");
  1708. pre.textContent = JSON.stringify(gPingData, null, 2);
  1709. // Update the structured data rendering.
  1710. const keysHeader = bundle.GetStringFromName("keysHeader");
  1711. const valuesHeader = bundle.GetStringFromName("valuesHeader");
  1712. // Update the payload list and process lists
  1713. if (updatePayloadList) {
  1714. renderPayloadList(ping);
  1715. renderProcessList(ping, document.getElementById("histograms-processes"));
  1716. renderProcessList(ping, document.getElementById("keyed-histograms-processes"));
  1717. }
  1718. // Show general data.
  1719. GeneralData.render(ping);
  1720. // Show environment data.
  1721. EnvironmentData.render(ping);
  1722. // We only have special rendering code for the payloads from "main" pings.
  1723. // For any other pings we just render the raw JSON payload.
  1724. let isMainPing = (ping.type == "main" || ping.type == "saved-session");
  1725. togglePingSections(isMainPing);
  1726. if (!isMainPing) {
  1727. RawPayload.render(ping);
  1728. return;
  1729. }
  1730. // Show telemetry log.
  1731. TelLog.render(ping);
  1732. // Show slow SQL stats
  1733. SlowSQL.render(ping);
  1734. // Show chrome hang stacks
  1735. ChromeHangs.render(ping);
  1736. // Render Addon details.
  1737. AddonDetails.render(ping);
  1738. // Select payload to render
  1739. let payloadSelect = document.getElementById("choose-payload");
  1740. let payloadOption = payloadSelect.selectedOptions.item(0);
  1741. let payloadIndex = payloadOption.getAttribute("value");
  1742. let payload = ping.payload;
  1743. if (payloadIndex > 0) {
  1744. payload = ping.payload.childPayloads[payloadIndex - 1];
  1745. }
  1746. // Show thread hang stats
  1747. ThreadHangStats.render(payload);
  1748. // Show simple measurements
  1749. let simpleMeasurements = sortStartupMilestones(payload.simpleMeasurements);
  1750. let hasData = Object.keys(simpleMeasurements).length > 0;
  1751. setHasData("simple-measurements-section", hasData);
  1752. let simpleSection = document.getElementById("simple-measurements");
  1753. removeAllChildNodes(simpleSection);
  1754. if (hasData) {
  1755. simpleSection.appendChild(KeyValueTable.render(simpleMeasurements,
  1756. keysHeader, valuesHeader));
  1757. }
  1758. LateWritesSingleton.renderLateWrites(payload.lateWrites);
  1759. // Show basic session info gathered
  1760. hasData = Object.keys(ping.payload.info).length > 0;
  1761. setHasData("session-info-section", hasData);
  1762. let infoSection = document.getElementById("session-info");
  1763. removeAllChildNodes(infoSection);
  1764. if (hasData) {
  1765. infoSection.appendChild(KeyValueTable.render(ping.payload.info,
  1766. keysHeader, valuesHeader));
  1767. }
  1768. // Show scalar data.
  1769. Scalars.render(payload);
  1770. KeyedScalars.render(payload);
  1771. // Show histogram data
  1772. let hgramDiv = document.getElementById("histograms");
  1773. removeAllChildNodes(hgramDiv);
  1774. let histograms = payload.histograms;
  1775. let hgramsSelect = document.getElementById("histograms-processes");
  1776. let hgramsOption = hgramsSelect.selectedOptions.item(0);
  1777. let hgramsProcess = hgramsOption.getAttribute("value");
  1778. if (hgramsProcess &&
  1779. "processes" in ping.payload &&
  1780. hgramsProcess in ping.payload.processes) {
  1781. histograms = ping.payload.processes[hgramsProcess].histograms;
  1782. }
  1783. hasData = Object.keys(histograms).length > 0;
  1784. setHasData("histograms-section", hasData || hgramsSelect.options.length);
  1785. if (hasData) {
  1786. for (let [name, hgram] of Object.entries(histograms)) {
  1787. Histogram.render(hgramDiv, name, hgram, {unpacked: true});
  1788. }
  1789. let filterBox = document.getElementById("histograms-filter");
  1790. filterBox.addEventListener("input", Histogram.histogramFilterChanged, false);
  1791. if (filterBox.value.trim() != "") { // on load, no need to filter if empty
  1792. Histogram.filterHistograms(hgramDiv, filterBox.value);
  1793. }
  1794. setHasData("histograms-section", true);
  1795. }
  1796. // Show keyed histogram data
  1797. let keyedDiv = document.getElementById("keyed-histograms");
  1798. removeAllChildNodes(keyedDiv);
  1799. let keyedHistograms = payload.keyedHistograms;
  1800. let keyedHgramsSelect = document.getElementById("keyed-histograms-processes");
  1801. let keyedHgramsOption = keyedHgramsSelect.selectedOptions.item(0);
  1802. let keyedHgramsProcess = keyedHgramsOption.getAttribute("value");
  1803. if (keyedHgramsProcess &&
  1804. "processes" in ping.payload &&
  1805. keyedHgramsProcess in ping.payload.processes) {
  1806. keyedHistograms = ping.payload.processes[keyedHgramsProcess].keyedHistograms;
  1807. }
  1808. setHasData("keyed-histograms-section", keyedHgramsSelect.options.length);
  1809. if (keyedHistograms) {
  1810. let hasData = false;
  1811. for (let [id, keyed] of Object.entries(keyedHistograms)) {
  1812. if (Object.keys(keyed).length > 0) {
  1813. hasData = true;
  1814. KeyedHistogram.render(keyedDiv, id, keyed, {unpacked: true});
  1815. }
  1816. }
  1817. setHasData("keyed-histograms-section", hasData || keyedHgramsSelect.options.length);
  1818. }
  1819. // Show event data.
  1820. Events.render(payload);
  1821. // Show addon histogram data
  1822. let addonDiv = document.getElementById("addon-histograms");
  1823. removeAllChildNodes(addonDiv);
  1824. let addonHistogramsRendered = false;
  1825. let addonData = payload.addonHistograms;
  1826. if (addonData) {
  1827. for (let [addon, histograms] of Object.entries(addonData)) {
  1828. for (let [name, hgram] of Object.entries(histograms)) {
  1829. addonHistogramsRendered = true;
  1830. Histogram.render(addonDiv, addon + ": " + name, hgram, {unpacked: true});
  1831. }
  1832. }
  1833. }
  1834. setHasData("addon-histograms-section", addonHistogramsRendered);
  1835. }
  1836. window.addEventListener("load", onLoad, false);