no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / toolkit / content / aboutTelemetry.js
blobb25c2687d93dfc46c9960c4cbffbc6dc1080534e
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/. */
5 "use strict";
7 const { BrowserUtils } = ChromeUtils.importESModule(
8   "resource://gre/modules/BrowserUtils.sys.mjs"
9 );
10 const { TelemetryTimestamps } = ChromeUtils.importESModule(
11   "resource://gre/modules/TelemetryTimestamps.sys.mjs"
13 const { TelemetryController } = ChromeUtils.importESModule(
14   "resource://gre/modules/TelemetryController.sys.mjs"
16 const { TelemetryArchive } = ChromeUtils.importESModule(
17   "resource://gre/modules/TelemetryArchive.sys.mjs"
19 const { TelemetrySend } = ChromeUtils.importESModule(
20   "resource://gre/modules/TelemetrySend.sys.mjs"
23 const { AppConstants } = ChromeUtils.importESModule(
24   "resource://gre/modules/AppConstants.sys.mjs"
26 ChromeUtils.defineESModuleGetters(this, {
27   ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
28   Preferences: "resource://gre/modules/Preferences.sys.mjs",
29 });
31 const Telemetry = Services.telemetry;
33 // Maximum height of a histogram bar (in em for html, in chars for text)
34 const MAX_BAR_HEIGHT = 8;
35 const MAX_BAR_CHARS = 25;
36 const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner";
37 const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
38 const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql";
39 const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl";
40 const DEFAULT_SYMBOL_SERVER_URI =
41   "https://symbolication.services.mozilla.com/symbolicate/v4";
42 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
44 // ms idle before applying the filter (allow uninterrupted typing)
45 const FILTER_IDLE_TIMEOUT = 500;
47 const isWindows = Services.appinfo.OS == "WINNT";
48 const EOL = isWindows ? "\r\n" : "\n";
50 // This is the ping object currently displayed in the page.
51 var gPingData = null;
53 // Cached value of document's RTL mode
54 var documentRTLMode = "";
56 /**
57  * Helper function for determining whether the document direction is RTL.
58  * Caches result of check on first invocation.
59  */
60 function isRTL() {
61   if (!documentRTLMode) {
62     documentRTLMode = window.getComputedStyle(document.body).direction;
63   }
64   return documentRTLMode == "rtl";
67 function isFlatArray(obj) {
68   if (!Array.isArray(obj)) {
69     return false;
70   }
71   return !obj.some(e => typeof e == "object");
74 /**
75  * This is a helper function for explodeObject.
76  */
77 function flattenObject(obj, map, path, array) {
78   for (let k of Object.keys(obj)) {
79     let newPath = [...path, array ? "[" + k + "]" : k];
80     let v = obj[k];
81     if (!v || typeof v != "object") {
82       map.set(newPath.join("."), v);
83     } else if (isFlatArray(v)) {
84       map.set(newPath.join("."), "[" + v.join(", ") + "]");
85     } else {
86       flattenObject(v, map, newPath, Array.isArray(v));
87     }
88   }
91 /**
92  * This turns a JSON object into a "flat" stringified form.
93  *
94  * For an object like {a: "1", b: {c: "2", d: "3"}} it returns a Map of the
95  * form Map(["a","1"], ["b.c", "2"], ["b.d", "3"]).
96  */
97 function explodeObject(obj) {
98   let map = new Map();
99   flattenObject(obj, map, []);
100   return map;
103 function filterObject(obj, filterOut) {
104   let ret = {};
105   for (let k of Object.keys(obj)) {
106     if (!filterOut.includes(k)) {
107       ret[k] = obj[k];
108     }
109   }
110   return ret;
114  * This turns a JSON object into a "flat" stringified form, separated into top-level sections.
116  * For an object like:
117  *   {
118  *     a: {b: "1"},
119  *     c: {d: "2", e: {f: "3"}}
120  *   }
121  * it returns a Map of the form:
122  *   Map([
123  *     ["a", Map(["b","1"])],
124  *     ["c", Map([["d", "2"], ["e.f", "3"]])]
125  *   ])
126  */
127 function sectionalizeObject(obj) {
128   let map = new Map();
129   for (let k of Object.keys(obj)) {
130     map.set(k, explodeObject(obj[k]));
131   }
132   return map;
136  * Obtain the main DOMWindow for the current context.
137  */
138 function getMainWindow() {
139   return window.browsingContext.topChromeWindow;
143  * Obtain the DOMWindow that can open a preferences pane.
145  * This is essentially "get the browser chrome window" with the added check
146  * that the supposed browser chrome window is capable of opening a preferences
147  * pane.
149  * This may return null if we can't find the browser chrome window.
150  */
151 function getMainWindowWithPreferencesPane() {
152   let mainWindow = getMainWindow();
153   if (mainWindow && "openPreferences" in mainWindow) {
154     return mainWindow;
155   }
156   return null;
160  * Remove all child nodes of a document node.
161  */
162 function removeAllChildNodes(node) {
163   while (node.hasChildNodes()) {
164     node.removeChild(node.lastChild);
165   }
168 var Settings = {
169   attachObservers() {
170     let elements = document.getElementsByClassName("change-data-choices-link");
171     for (let el of elements) {
172       el.parentElement.addEventListener("click", function (event) {
173         if (event.target.localName === "a") {
174           if (AppConstants.platform == "android") {
175             var { EventDispatcher } = ChromeUtils.importESModule(
176               "resource://gre/modules/Messaging.sys.mjs"
177             );
178             EventDispatcher.instance.sendRequest({
179               type: "Settings:Show",
180               resource: "preferences_privacy",
181             });
182           } else {
183             // Show the data choices preferences on desktop.
184             let mainWindow = getMainWindowWithPreferencesPane();
185             mainWindow.openPreferences("privacy-reports");
186           }
187         }
188       });
189     }
190   },
192   /**
193    * Updates the button & text at the top of the page to reflect Telemetry state.
194    */
195   render() {
196     let settingsExplanation = document.getElementById("settings-explanation");
197     let extendedEnabled = Services.telemetry.canRecordExtended;
199     let channel = extendedEnabled ? "prerelease" : "release";
200     let uploadcase = TelemetrySend.sendingEnabled() ? "enabled" : "disabled";
202     document.l10n.setAttributes(
203       settingsExplanation,
204       "about-telemetry-settings-explanation",
205       { channel, uploadcase }
206     );
208     this.attachObservers();
209   },
212 var PingPicker = {
213   viewCurrentPingData: null,
214   _archivedPings: null,
215   TYPE_ALL: "all",
217   attachObservers() {
218     let pingSourceElements = document.getElementsByName("choose-ping-source");
219     for (let el of pingSourceElements) {
220       el.addEventListener("change", () => this.onPingSourceChanged());
221     }
223     let displays = document.getElementsByName("choose-ping-display");
224     for (let el of displays) {
225       el.addEventListener("change", () => this.onPingDisplayChanged());
226     }
228     document
229       .getElementById("show-subsession-data")
230       .addEventListener("change", () => {
231         this._updateCurrentPingData();
232       });
234     document.getElementById("choose-ping-id").addEventListener("change", () => {
235       this._updateArchivedPingData();
236     });
237     document
238       .getElementById("choose-ping-type")
239       .addEventListener("change", () => {
240         this.filterDisplayedPings();
241       });
243     document
244       .getElementById("newer-ping")
245       .addEventListener("click", () => this._movePingIndex(-1));
246     document
247       .getElementById("older-ping")
248       .addEventListener("click", () => this._movePingIndex(1));
250     let pingPickerNeedHide = false;
251     let pingPicker = document.getElementById("ping-picker");
252     pingPicker.addEventListener(
253       "mouseenter",
254       () => (pingPickerNeedHide = false)
255     );
256     pingPicker.addEventListener(
257       "mouseleave",
258       () => (pingPickerNeedHide = true)
259     );
260     document.addEventListener("click", ev => {
261       if (pingPickerNeedHide) {
262         pingPicker.classList.add("hidden");
263       }
264     });
265     document
266       .getElementById("stores")
267       .addEventListener("change", () => displayPingData(gPingData));
268     Array.from(document.querySelectorAll(".change-ping")).forEach(el => {
269       el.addEventListener("click", event => {
270         if (!pingPicker.classList.contains("hidden")) {
271           pingPicker.classList.add("hidden");
272         } else {
273           pingPicker.classList.remove("hidden");
274           event.stopPropagation();
275         }
276       });
277     });
278   },
280   onPingSourceChanged() {
281     this.update();
282   },
284   onPingDisplayChanged() {
285     this.update();
286   },
288   render() {
289     // Display the type and controls if the ping is not current
290     let pingDate = document.getElementById("ping-date");
291     let pingType = document.getElementById("ping-type");
292     let controls = document.getElementById("controls");
293     let pingExplanation = document.getElementById("ping-explanation");
295     if (!this.viewCurrentPingData) {
296       let pingName = this._getSelectedPingName();
297       // Change sidebar heading text.
298       pingDate.textContent = pingName;
299       pingDate.setAttribute("title", pingName);
300       let pingTypeText = this._getSelectedPingType();
301       controls.classList.remove("hidden");
302       pingType.textContent = pingTypeText;
303       document.l10n.setAttributes(
304         pingExplanation,
305         "about-telemetry-ping-details",
306         { timestamp: pingTypeText, name: pingName }
307       );
308     } else {
309       // Change sidebar heading text.
310       controls.classList.add("hidden");
311       document.l10n.setAttributes(
312         pingType,
313         "about-telemetry-current-data-sidebar"
314       );
315       // Change home page text.
316       document.l10n.setAttributes(
317         pingExplanation,
318         "about-telemetry-data-details-current"
319       );
320     }
322     GenericSubsection.deleteAllSubSections();
323   },
325   async update() {
326     let viewCurrent = document.getElementById("ping-source-current").checked;
327     let currentChanged = viewCurrent !== this.viewCurrentPingData;
328     this.viewCurrentPingData = viewCurrent;
330     // If we have no archived pings, disable the ping archive selection.
331     // This can happen on new profiles or if the ping archive is disabled.
332     let archivedPingList = await TelemetryArchive.promiseArchivedPingList();
333     let sourceArchived = document.getElementById("ping-source-archive");
334     let sourceArchivedContainer = document.getElementById(
335       "ping-source-archive-container"
336     );
337     let archivedDisabled = !archivedPingList.length;
338     sourceArchived.disabled = archivedDisabled;
339     sourceArchivedContainer.classList.toggle("disabled", archivedDisabled);
341     if (currentChanged) {
342       if (this.viewCurrentPingData) {
343         document.getElementById("current-ping-picker").hidden = false;
344         document.getElementById("archived-ping-picker").hidden = true;
345         this._updateCurrentPingData();
346       } else {
347         document.getElementById("current-ping-picker").hidden = true;
348         await this._updateArchivedPingList(archivedPingList);
349         document.getElementById("archived-ping-picker").hidden = false;
350       }
351     }
352   },
354   _updateCurrentPingData() {
355     const subsession = document.getElementById("show-subsession-data").checked;
356     let ping = TelemetryController.getCurrentPingData(subsession);
357     if (!ping) {
358       return;
359     }
361     let stores = Telemetry.getAllStores();
362     let getData = {
363       histograms: Telemetry.getSnapshotForHistograms,
364       keyedHistograms: Telemetry.getSnapshotForKeyedHistograms,
365       scalars: Telemetry.getSnapshotForScalars,
366       keyedScalars: Telemetry.getSnapshotForKeyedScalars,
367     };
369     let data = {};
370     for (const [name, fn] of Object.entries(getData)) {
371       for (const store of stores) {
372         if (!data[store]) {
373           data[store] = {};
374         }
375         let measurement = fn(store, /* clear */ false, /* filterTest */ true);
376         let processes = Object.keys(measurement);
378         for (const process of processes) {
379           if (!data[store][process]) {
380             data[store][process] = {};
381           }
383           data[store][process][name] = measurement[process];
384         }
385       }
386     }
387     ping.payload.stores = data;
389     // Delete the unused data from the payload of the current ping.
390     // It's included in the above `stores` attribute.
391     for (const data of Object.values(ping.payload.processes)) {
392       delete data.scalars;
393       delete data.keyedScalars;
394       delete data.histograms;
395       delete data.keyedHistograms;
396     }
397     delete ping.payload.histograms;
398     delete ping.payload.keyedHistograms;
400     // augment ping payload with event telemetry
401     let eventSnapshot = Telemetry.snapshotEvents(
402       Telemetry.DATASET_PRERELEASE_CHANNELS,
403       false
404     );
405     for (let process of Object.keys(eventSnapshot)) {
406       if (process in ping.payload.processes) {
407         ping.payload.processes[process].events = eventSnapshot[process].filter(
408           e => !e[1].startsWith("telemetry.test")
409         );
410       }
411     }
413     displayPingData(ping, true);
414   },
416   _updateArchivedPingData() {
417     let id = this._getSelectedPingId();
418     let res = Promise.resolve();
419     if (id) {
420       res = TelemetryArchive.promiseArchivedPingById(id).then(ping =>
421         displayPingData(ping, true)
422       );
423     }
424     return res;
425   },
427   async _updateArchivedPingList(pingList) {
428     // The archived ping list is sorted in ascending timestamp order,
429     // but descending is more practical for the operations we do here.
430     pingList.reverse();
431     this._archivedPings = pingList;
432     // Render the archive data.
433     this._renderPingList();
434     // Update the displayed ping.
435     await this._updateArchivedPingData();
436   },
438   _renderPingList() {
439     let pingSelector = document.getElementById("choose-ping-id");
440     Array.from(pingSelector.children).forEach(child =>
441       removeAllChildNodes(child)
442     );
444     let pingTypes = new Set();
445     pingTypes.add(this.TYPE_ALL);
447     const today = new Date();
448     today.setHours(0, 0, 0, 0);
449     const yesterday = new Date(today);
450     yesterday.setDate(today.getDate() - 1);
452     for (let p of this._archivedPings) {
453       pingTypes.add(p.type);
454       const pingDate = new Date(p.timestampCreated);
455       const datetimeText = new Services.intl.DateTimeFormat(undefined, {
456         dateStyle: "short",
457         timeStyle: "medium",
458       }).format(pingDate);
459       const pingName = `${datetimeText}, ${p.type}`;
461       let option = document.createElement("option");
462       let content = document.createTextNode(pingName);
463       option.appendChild(content);
464       option.setAttribute("value", p.id);
465       option.dataset.type = p.type;
466       option.dataset.date = datetimeText;
468       pingDate.setHours(0, 0, 0, 0);
469       if (pingDate.getTime() === today.getTime()) {
470         pingSelector.children[0].appendChild(option);
471       } else if (pingDate.getTime() === yesterday.getTime()) {
472         pingSelector.children[1].appendChild(option);
473       } else {
474         pingSelector.children[2].appendChild(option);
475       }
476     }
477     this._renderPingTypes(pingTypes);
478   },
480   _renderPingTypes(pingTypes) {
481     let pingTypeSelector = document.getElementById("choose-ping-type");
482     removeAllChildNodes(pingTypeSelector);
483     pingTypes.forEach(type => {
484       let option = document.createElement("option");
485       option.appendChild(document.createTextNode(type));
486       option.setAttribute("value", type);
487       pingTypeSelector.appendChild(option);
488     });
489   },
491   _movePingIndex(offset) {
492     if (this.viewCurrentPingData) {
493       return;
494     }
495     let typeSelector = document.getElementById("choose-ping-type");
496     let type = typeSelector.selectedOptions.item(0).value;
498     let id = this._getSelectedPingId();
499     let index = this._archivedPings.findIndex(p => p.id == id);
500     let newIndex = Math.min(
501       Math.max(0, index + offset),
502       this._archivedPings.length - 1
503     );
505     let pingList;
506     if (offset > 0) {
507       pingList = this._archivedPings.slice(newIndex);
508     } else {
509       pingList = this._archivedPings.slice(0, newIndex);
510       pingList.reverse();
511     }
513     let ping = pingList.find(p => {
514       return type == this.TYPE_ALL || p.type == type;
515     });
517     if (ping) {
518       this.selectPing(ping);
519       this._updateArchivedPingData();
520     }
521   },
523   selectPing(ping) {
524     let pingSelector = document.getElementById("choose-ping-id");
525     // Use some() to break if we find the ping.
526     Array.from(pingSelector.children).some(group => {
527       return Array.from(group.children).some(option => {
528         if (option.value == ping.id) {
529           option.selected = true;
530           return true;
531         }
532         return false;
533       });
534     });
535   },
537   filterDisplayedPings() {
538     let pingSelector = document.getElementById("choose-ping-id");
539     let typeSelector = document.getElementById("choose-ping-type");
540     let type = typeSelector.selectedOptions.item(0).value;
541     let first = true;
542     Array.from(pingSelector.children).forEach(group => {
543       Array.from(group.children).forEach(option => {
544         if (first && option.dataset.type == type) {
545           option.selected = true;
546           first = false;
547         }
548         option.hidden = type != this.TYPE_ALL && option.dataset.type != type;
549         // Arrow keys should only iterate over visible options
550         option.disabled = option.hidden;
551       });
552     });
553     this._updateArchivedPingData();
554   },
556   _getSelectedPingName() {
557     let pingSelector = document.getElementById("choose-ping-id");
558     let selected = pingSelector.selectedOptions.item(0);
559     return selected.dataset.date;
560   },
562   _getSelectedPingType() {
563     let pingSelector = document.getElementById("choose-ping-id");
564     let selected = pingSelector.selectedOptions.item(0);
565     return selected.dataset.type;
566   },
568   _getSelectedPingId() {
569     let pingSelector = document.getElementById("choose-ping-id");
570     let selected = pingSelector.selectedOptions.item(0);
571     return selected.getAttribute("value");
572   },
574   _showRawPingData() {
575     show(document.getElementById("category-raw"));
576   },
578   _showStructuredPingData() {
579     show(document.getElementById("category-home"));
580   },
583 var GeneralData = {
584   /**
585    * Renders the general data
586    */
587   render(aPing) {
588     setHasData("general-data-section", true);
589     let generalDataSection = document.getElementById("general-data");
590     removeAllChildNodes(generalDataSection);
592     const headings = [
593       "about-telemetry-names-header",
594       "about-telemetry-values-header",
595     ];
597     // The payload & environment parts are handled by other renderers.
598     let ignoreSections = ["payload", "environment"];
599     let data = explodeObject(filterObject(aPing, ignoreSections));
601     const table = GenericTable.render(data, headings);
602     generalDataSection.appendChild(table);
603   },
606 var EnvironmentData = {
607   /**
608    * Renders the environment data
609    */
610   render(ping) {
611     let dataDiv = document.getElementById("environment-data");
612     removeAllChildNodes(dataDiv);
613     const hasData = !!ping.environment;
614     setHasData("environment-data-section", hasData);
615     if (!hasData) {
616       return;
617     }
619     let ignore = ["addons"];
620     let env = filterObject(ping.environment, ignore);
621     let sections = sectionalizeObject(env);
622     GenericSubsection.render(sections, dataDiv, "environment-data-section");
624     // We use specialized rendering here to make the addon and plugin listings
625     // more readable.
626     this.createAddonSection(dataDiv, ping);
627   },
629   renderAddonsObject(addonObj, addonSection, sectionTitle) {
630     let table = document.createElement("table");
631     table.setAttribute("id", sectionTitle);
632     this.appendAddonSubsectionTitle(sectionTitle, table);
634     for (let id of Object.keys(addonObj)) {
635       let addon = addonObj[id];
636       this.appendHeadingName(table, addon.name || id);
637       this.appendAddonID(table, id);
638       let data = explodeObject(addon);
640       for (let [key, value] of data) {
641         this.appendRow(table, key, value);
642       }
643     }
645     addonSection.appendChild(table);
646   },
648   renderKeyValueObject(addonObj, addonSection, sectionTitle) {
649     let data = explodeObject(addonObj);
650     let table = GenericTable.render(data);
651     table.setAttribute("class", sectionTitle);
652     this.appendAddonSubsectionTitle(sectionTitle, table);
653     addonSection.appendChild(table);
654   },
656   appendAddonID(table, addonID) {
657     this.appendRow(table, "id", addonID);
658   },
660   appendHeadingName(table, name) {
661     let headings = document.createElement("tr");
662     this.appendColumn(headings, "th", name);
663     headings.cells[0].colSpan = 2;
664     table.appendChild(headings);
665   },
667   appendAddonSubsectionTitle(section, table) {
668     let caption = document.createElement("caption");
669     caption.appendChild(document.createTextNode(section));
670     table.appendChild(caption);
671   },
673   createAddonSection(dataDiv, ping) {
674     if (!ping || !("environment" in ping) || !("addons" in ping.environment)) {
675       return;
676     }
677     let addonSection = document.createElement("div");
678     addonSection.setAttribute("class", "subsection-data subdata");
679     let addons = ping.environment.addons;
680     this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
681     this.renderKeyValueObject(addons.theme, addonSection, "theme");
682     this.renderAddonsObject(
683       addons.activeGMPlugins,
684       addonSection,
685       "activeGMPlugins"
686     );
688     let hasAddonData = !!Object.keys(ping.environment.addons).length;
689     let s = GenericSubsection.renderSubsectionHeader(
690       "addons",
691       hasAddonData,
692       "environment-data-section"
693     );
694     s.appendChild(addonSection);
695     dataDiv.appendChild(s);
696   },
698   appendRow(table, id, value) {
699     let row = document.createElement("tr");
700     row.id = id;
701     this.appendColumn(row, "td", id);
702     this.appendColumn(row, "td", value);
703     table.appendChild(row);
704   },
705   /**
706    * Helper function for appending a column to the data table.
707    *
708    * @param aRowElement Parent row element
709    * @param aColType Column's tag name
710    * @param aColText Column contents
711    */
712   appendColumn(aRowElement, aColType, aColText) {
713     let colElement = document.createElement(aColType);
714     let colTextElement = document.createTextNode(aColText);
715     colElement.appendChild(colTextElement);
716     aRowElement.appendChild(colElement);
717   },
720 var SlowSQL = {
721   /**
722    * Render slow SQL statistics
723    */
724   render: function SlowSQL_render(aPing) {
725     // We can add the debug SQL data to the current ping later.
726     // However, we need to be careful to never send that debug data
727     // out due to privacy concerns.
728     // We want to show the actual ping data for archived pings,
729     // so skip this there.
731     let debugSlowSql =
732       PingPicker.viewCurrentPingData &&
733       Preferences.get(PREF_DEBUG_SLOW_SQL, false);
734     let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
735     if (!slowSql) {
736       setHasData("slow-sql-section", false);
737       return;
738     }
740     let { mainThread, otherThreads } = debugSlowSql
741       ? Telemetry.debugSlowSQL
742       : aPing.payload.slowSQL;
744     let mainThreadCount = Object.keys(mainThread).length;
745     let otherThreadCount = Object.keys(otherThreads).length;
746     if (mainThreadCount == 0 && otherThreadCount == 0) {
747       setHasData("slow-sql-section", false);
748       return;
749     }
751     setHasData("slow-sql-section", true);
752     if (debugSlowSql) {
753       document.getElementById("sql-warning").hidden = false;
754     }
756     let slowSqlDiv = document.getElementById("slow-sql-tables");
757     removeAllChildNodes(slowSqlDiv);
759     // Main thread
760     if (mainThreadCount > 0) {
761       let table = document.createElement("table");
762       this.renderTableHeader(table, "main");
763       this.renderTable(table, mainThread);
764       slowSqlDiv.appendChild(table);
765     }
767     // Other threads
768     if (otherThreadCount > 0) {
769       let table = document.createElement("table");
770       this.renderTableHeader(table, "other");
771       this.renderTable(table, otherThreads);
772       slowSqlDiv.appendChild(table);
773     }
774   },
776   /**
777    * Creates a header row for a Slow SQL table
778    * Tabs & newlines added to cells to make it easier to copy-paste.
779    *
780    * @param aTable Parent table element
781    * @param aTitle Table's title
782    */
783   renderTableHeader: function SlowSQL_renderTableHeader(aTable, threadType) {
784     let caption = document.createElement("caption");
785     if (threadType == "main") {
786       document.l10n.setAttributes(caption, "about-telemetry-slow-sql-main");
787     }
789     if (threadType == "other") {
790       document.l10n.setAttributes(caption, "about-telemetry-slow-sql-other");
791     }
792     aTable.appendChild(caption);
794     let headings = document.createElement("tr");
795     document.l10n.setAttributes(
796       this.appendColumn(headings, "th"),
797       "about-telemetry-slow-sql-hits"
798     );
799     document.l10n.setAttributes(
800       this.appendColumn(headings, "th"),
801       "about-telemetry-slow-sql-average"
802     );
803     document.l10n.setAttributes(
804       this.appendColumn(headings, "th"),
805       "about-telemetry-slow-sql-statement"
806     );
807     aTable.appendChild(headings);
808   },
810   /**
811    * Fills out the table body
812    * Tabs & newlines added to cells to make it easier to copy-paste.
813    *
814    * @param aTable Parent table element
815    * @param aSql SQL stats object
816    */
817   renderTable: function SlowSQL_renderTable(aTable, aSql) {
818     for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) {
819       let averageTime = totalTime / hitCount;
821       let sqlRow = document.createElement("tr");
823       this.appendColumn(sqlRow, "td", hitCount + "\t");
824       this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
825       this.appendColumn(sqlRow, "td", sql + "\n");
827       aTable.appendChild(sqlRow);
828     }
829   },
831   /**
832    * Helper function for appending a column to a Slow SQL table.
833    *
834    * @param aRowElement Parent row element
835    * @param aColType Column's tag name
836    * @param aColText Column contents
837    */
838   appendColumn: function SlowSQL_appendColumn(
839     aRowElement,
840     aColType,
841     aColText = ""
842   ) {
843     let colElement = document.createElement(aColType);
844     if (aColText) {
845       let colTextElement = document.createTextNode(aColText);
846       colElement.appendChild(colTextElement);
847     }
848     aRowElement.appendChild(colElement);
849     return colElement;
850   },
853 var StackRenderer = {
854   /**
855    * Outputs the memory map associated with this hang report
856    *
857    * @param aDiv Output div
858    */
859   renderMemoryMap: async function StackRenderer_renderMemoryMap(
860     aDiv,
861     memoryMap
862   ) {
863     let memoryMapTitleElement = document.createElement("span");
864     document.l10n.setAttributes(
865       memoryMapTitleElement,
866       "about-telemetry-memory-map-title"
867     );
868     aDiv.appendChild(memoryMapTitleElement);
869     aDiv.appendChild(document.createElement("br"));
871     for (let currentModule of memoryMap) {
872       aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
873       aDiv.appendChild(document.createElement("br"));
874     }
876     aDiv.appendChild(document.createElement("br"));
877   },
879   /**
880    * Outputs the raw PCs from the hang's stack
881    *
882    * @param aDiv Output div
883    * @param aStack Array of PCs from the hang stack
884    */
885   renderStack: function StackRenderer_renderStack(aDiv, aStack) {
886     let stackTitleElement = document.createElement("span");
887     document.l10n.setAttributes(
888       stackTitleElement,
889       "about-telemetry-stack-title"
890     );
891     aDiv.appendChild(stackTitleElement);
892     let stackText = " " + aStack.join(" ");
893     aDiv.appendChild(document.createTextNode(stackText));
895     aDiv.appendChild(document.createElement("br"));
896     aDiv.appendChild(document.createElement("br"));
897   },
898   renderStacks: function StackRenderer_renderStacks(
899     aPrefix,
900     aStacks,
901     aMemoryMap,
902     aRenderHeader
903   ) {
904     let div = document.getElementById(aPrefix);
905     removeAllChildNodes(div);
907     let fetchE = document.getElementById(aPrefix + "-fetch-symbols");
908     if (fetchE) {
909       fetchE.hidden = false;
910     }
911     let hideE = document.getElementById(aPrefix + "-hide-symbols");
912     if (hideE) {
913       hideE.hidden = true;
914     }
916     if (!aStacks.length) {
917       return;
918     }
920     setHasData(aPrefix + "-section", true);
922     this.renderMemoryMap(div, aMemoryMap);
924     for (let i = 0; i < aStacks.length; ++i) {
925       let stack = aStacks[i];
926       aRenderHeader(i);
927       this.renderStack(div, stack);
928     }
929   },
931   /**
932    * Renders the title of the stack: e.g. "Late Write #1" or
933    * "Hang Report #1 (6 seconds)".
934    *
935    * @param aDivId The id of the div to append the header to.
936    * @param aL10nId The l10n id of the message to use for the title.
937    * @param aL10nArgs The l10n args for the provided message id.
938    */
939   renderHeader: function StackRenderer_renderHeader(
940     aDivId,
941     aL10nId,
942     aL10nArgs
943   ) {
944     let div = document.getElementById(aDivId);
946     let titleElement = document.createElement("span");
947     titleElement.className = "stack-title";
949     document.l10n.setAttributes(titleElement, aL10nId, aL10nArgs);
951     div.appendChild(titleElement);
952     div.appendChild(document.createElement("br"));
953   },
956 var RawPayloadData = {
957   /**
958    * Renders the raw pyaload.
959    */
960   render(aPing) {
961     setHasData("raw-payload-section", true);
962     let pre = document.getElementById("raw-payload-data");
963     pre.textContent = JSON.stringify(aPing.payload, null, 2);
964   },
966   attachObservers() {
967     document
968       .getElementById("payload-json-viewer")
969       .addEventListener("click", e => {
970         openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2));
971       });
972   },
975 function SymbolicationRequest(
976   aPrefix,
977   aRenderHeader,
978   aMemoryMap,
979   aStacks,
980   aDurations = null
981 ) {
982   this.prefix = aPrefix;
983   this.renderHeader = aRenderHeader;
984   this.memoryMap = aMemoryMap;
985   this.stacks = aStacks;
986   this.durations = aDurations;
989  * A callback for onreadystatechange. It replaces the numeric stack with
990  * the symbolicated one returned by the symbolication server.
991  */
992 SymbolicationRequest.prototype.handleSymbolResponse =
993   async function SymbolicationRequest_handleSymbolResponse() {
994     if (this.symbolRequest.readyState != 4) {
995       return;
996     }
998     let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
999     fetchElement.hidden = true;
1000     let hideElement = document.getElementById(this.prefix + "-hide-symbols");
1001     hideElement.hidden = false;
1002     let div = document.getElementById(this.prefix);
1003     removeAllChildNodes(div);
1004     let errorMessage = await document.l10n.formatValue(
1005       "about-telemetry-error-fetching-symbols"
1006     );
1008     if (this.symbolRequest.status != 200) {
1009       div.appendChild(document.createTextNode(errorMessage));
1010       return;
1011     }
1013     let jsonResponse = {};
1014     try {
1015       jsonResponse = JSON.parse(this.symbolRequest.responseText);
1016     } catch (e) {
1017       div.appendChild(document.createTextNode(errorMessage));
1018       return;
1019     }
1021     for (let i = 0; i < jsonResponse.length; ++i) {
1022       let stack = jsonResponse[i];
1023       this.renderHeader(i, this.durations);
1025       for (let symbol of stack) {
1026         div.appendChild(document.createTextNode(symbol));
1027         div.appendChild(document.createElement("br"));
1028       }
1029       div.appendChild(document.createElement("br"));
1030     }
1031   };
1033  * Send a request to the symbolication server to symbolicate this stack.
1034  */
1035 SymbolicationRequest.prototype.fetchSymbols =
1036   function SymbolicationRequest_fetchSymbols() {
1037     let symbolServerURI = Preferences.get(
1038       PREF_SYMBOL_SERVER_URI,
1039       DEFAULT_SYMBOL_SERVER_URI
1040     );
1041     let request = {
1042       memoryMap: this.memoryMap,
1043       stacks: this.stacks,
1044       version: 3,
1045     };
1046     let requestJSON = JSON.stringify(request);
1048     this.symbolRequest = new XMLHttpRequest();
1049     this.symbolRequest.open("POST", symbolServerURI, true);
1050     this.symbolRequest.setRequestHeader("Content-type", "application/json");
1051     this.symbolRequest.setRequestHeader("Content-length", requestJSON.length);
1052     this.symbolRequest.setRequestHeader("Connection", "close");
1053     this.symbolRequest.onreadystatechange =
1054       this.handleSymbolResponse.bind(this);
1055     this.symbolRequest.send(requestJSON);
1056   };
1058 var Histogram = {
1059   /**
1060    * Renders a single Telemetry histogram
1061    *
1062    * @param aParent Parent element
1063    * @param aName Histogram name
1064    * @param aHgram Histogram information
1065    * @param aOptions Object with render options
1066    *                 * exponential: bars follow logarithmic scale
1067    */
1068   render: function Histogram_render(aParent, aName, aHgram, aOptions) {
1069     let options = aOptions || {};
1070     let hgram = this.processHistogram(aHgram, aName);
1072     let outerDiv = document.createElement("div");
1073     outerDiv.className = "histogram";
1074     outerDiv.id = aName;
1076     let divTitle = document.createElement("div");
1077     divTitle.classList.add("histogram-title");
1078     divTitle.appendChild(document.createTextNode(aName));
1079     outerDiv.appendChild(divTitle);
1081     let divStats = document.createElement("div");
1082     divStats.classList.add("histogram-stats");
1084     let histogramStatsArgs = {
1085       sampleCount: hgram.sample_count,
1086       prettyAverage: hgram.pretty_average,
1087       sum: hgram.sum,
1088     };
1090     document.l10n.setAttributes(
1091       divStats,
1092       "about-telemetry-histogram-stats",
1093       histogramStatsArgs
1094     );
1096     if (isRTL()) {
1097       hgram.values.reverse();
1098     }
1100     let textData = this.renderValues(outerDiv, hgram, options);
1102     // The 'Copy' button contains the textual data, copied to clipboard on click
1103     let copyButton = document.createElement("button");
1104     copyButton.className = "copy-node";
1105     document.l10n.setAttributes(copyButton, "about-telemetry-histogram-copy");
1107     copyButton.addEventListener("click", async function () {
1108       let divStatsString = await document.l10n.formatValue(
1109         "about-telemetry-histogram-stats",
1110         histogramStatsArgs
1111       );
1112       copyButton.histogramText =
1113         aName + EOL + divStatsString + EOL + EOL + textData;
1114       Cc["@mozilla.org/widget/clipboardhelper;1"]
1115         .getService(Ci.nsIClipboardHelper)
1116         .copyString(this.histogramText);
1117     });
1118     outerDiv.appendChild(copyButton);
1120     aParent.appendChild(outerDiv);
1121     return outerDiv;
1122   },
1124   processHistogram(aHgram, aName) {
1125     const values = Object.keys(aHgram.values).map(k => aHgram.values[k]);
1126     if (!values.length) {
1127       // If we have no values collected for this histogram, just return
1128       // zero values so we still render it.
1129       return {
1130         values: [],
1131         pretty_average: 0,
1132         max: 0,
1133         sample_count: 0,
1134         sum: 0,
1135       };
1136     }
1138     const sample_count = values.reduceRight((a, b) => a + b);
1139     const average = Math.round((aHgram.sum * 10) / sample_count) / 10;
1140     const max_value = Math.max(...values);
1142     const labelledValues = Object.keys(aHgram.values).map(k => [
1143       Number(k),
1144       aHgram.values[k],
1145     ]);
1147     let result = {
1148       values: labelledValues,
1149       pretty_average: average,
1150       max: max_value,
1151       sample_count,
1152       sum: aHgram.sum,
1153     };
1155     return result;
1156   },
1158   /**
1159    * Return a non-negative, logarithmic representation of a non-negative number.
1160    * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
1161    *
1162    * @param aNumber Non-negative number
1163    */
1164   getLogValue(aNumber) {
1165     return Math.max(0, Math.log10(aNumber) + 1);
1166   },
1168   /**
1169    * Create histogram HTML bars, also returns a textual representation
1170    * Both aMaxValue and aSumValues must be positive.
1171    * Values are assumed to use 0 as baseline.
1172    *
1173    * @param aDiv Outer parent div
1174    * @param aHgram The histogram data
1175    * @param aOptions Object with render options (@see #render)
1176    */
1177   renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
1178     let text = "";
1179     // If the last label is not the longest string, alignment will break a little
1180     let labelPadTo = 0;
1181     if (aHgram.values.length) {
1182       labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
1183     }
1184     let maxBarValue = aOptions.exponential
1185       ? this.getLogValue(aHgram.max)
1186       : aHgram.max;
1188     for (let [label, value] of aHgram.values) {
1189       label = String(label);
1190       let barValue = aOptions.exponential ? this.getLogValue(value) : value;
1192       // Create a text representation: <right-aligned-label> |<bar-of-#><value>  <percentage>
1193       text +=
1194         EOL +
1195         " ".repeat(Math.max(0, labelPadTo - label.length)) +
1196         label + // Right-aligned label
1197         " |" +
1198         "#".repeat(Math.round((MAX_BAR_CHARS * barValue) / maxBarValue)) + // Bar
1199         "  " +
1200         value + // Value
1201         "  " +
1202         Math.round((100 * value) / aHgram.sample_count) +
1203         "%"; // Percentage
1205       // Construct the HTML labels + bars
1206       let belowEm =
1207         Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
1208       let aboveEm = MAX_BAR_HEIGHT - belowEm;
1210       let barDiv = document.createElement("div");
1211       barDiv.className = "bar";
1212       barDiv.style.paddingTop = aboveEm + "em";
1214       // Add value label or an nbsp if no value
1215       barDiv.appendChild(document.createTextNode(value ? value : "\u00A0"));
1217       // Create the blue bar
1218       let bar = document.createElement("div");
1219       bar.className = "bar-inner";
1220       bar.style.height = belowEm + "em";
1221       barDiv.appendChild(bar);
1223       // Add a special class to move the text down to prevent text overlap
1224       if (label.length > 3) {
1225         bar.classList.add("long-label");
1226       }
1227       // Add bucket label
1228       barDiv.appendChild(document.createTextNode(label));
1230       aDiv.appendChild(barDiv);
1231     }
1233     return text.substr(EOL.length); // Trim the EOL before the first line
1234   },
1237 var Search = {
1238   HASH_SEARCH: "search=",
1240   // A list of ids of sections that do not support search.
1241   blacklist: ["late-writes-section", "raw-payload-section"],
1243   // Pass if: all non-empty array items match (case-sensitive)
1244   isPassText(subject, filter) {
1245     for (let item of filter) {
1246       if (item.length && !subject.includes(item)) {
1247         return false; // mismatch and not a spurious space
1248       }
1249     }
1250     return true;
1251   },
1253   isPassRegex(subject, filter) {
1254     return filter.test(subject);
1255   },
1257   chooseFilter(filterText) {
1258     let filter = filterText.toString();
1259     // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
1260     let isPassFunc; // filter function, set once, then applied to all elements
1261     filter = filter.trim();
1262     if (filter[0] != "/") {
1263       // Plain text: case insensitive, AND if multi-string
1264       isPassFunc = this.isPassText;
1265       filter = filter.toLowerCase().split(" ");
1266     } else {
1267       isPassFunc = this.isPassRegex;
1268       var r = filter.match(/^\/(.*)\/(i?)$/);
1269       try {
1270         filter = RegExp(r[1], r[2]);
1271       } catch (e) {
1272         // Incomplete or bad RegExp - always no match
1273         isPassFunc = function () {
1274           return false;
1275         };
1276       }
1277     }
1278     return [isPassFunc, filter];
1279   },
1281   filterTextRows(table, filterText) {
1282     let [isPassFunc, filter] = this.chooseFilter(filterText);
1283     let allElementHidden = true;
1285     let needLowerCase = isPassFunc === this.isPassText;
1286     let elements = table.rows;
1287     for (let element of elements) {
1288       if (element.firstChild.nodeName == "th") {
1289         continue;
1290       }
1291       for (let cell of element.children) {
1292         let subject = needLowerCase
1293           ? cell.textContent.toLowerCase()
1294           : cell.textContent;
1295         element.hidden = !isPassFunc(subject, filter);
1296         if (!element.hidden) {
1297           if (allElementHidden) {
1298             allElementHidden = false;
1299           }
1300           // Don't need to check the rest of this row.
1301           break;
1302         }
1303       }
1304     }
1305     // Unhide the first row:
1306     if (!allElementHidden) {
1307       table.rows[0].hidden = false;
1308     }
1309     return allElementHidden;
1310   },
1312   filterElements(elements, filterText) {
1313     let [isPassFunc, filter] = this.chooseFilter(filterText);
1314     let allElementHidden = true;
1316     let needLowerCase = isPassFunc === this.isPassText;
1317     for (let element of elements) {
1318       let subject = needLowerCase ? element.id.toLowerCase() : element.id;
1319       element.hidden = !isPassFunc(subject, filter);
1320       if (allElementHidden && !element.hidden) {
1321         allElementHidden = false;
1322       }
1323     }
1324     return allElementHidden;
1325   },
1327   filterKeyedElements(keyedElements, filterText) {
1328     let [isPassFunc, filter] = this.chooseFilter(filterText);
1329     let allElementsHidden = true;
1331     let needLowerCase = isPassFunc === this.isPassText;
1332     keyedElements.forEach(keyedElement => {
1333       let subject = needLowerCase
1334         ? keyedElement.key.id.toLowerCase()
1335         : keyedElement.key.id;
1336       if (!isPassFunc(subject, filter)) {
1337         // If the keyedHistogram's name is not matched
1338         let allKeyedElementsHidden = true;
1339         for (let element of keyedElement.datas) {
1340           let subject = needLowerCase ? element.id.toLowerCase() : element.id;
1341           let match = isPassFunc(subject, filter);
1342           element.hidden = !match;
1343           if (match) {
1344             allKeyedElementsHidden = false;
1345           }
1346         }
1347         if (allElementsHidden && !allKeyedElementsHidden) {
1348           allElementsHidden = false;
1349         }
1350         keyedElement.key.hidden = allKeyedElementsHidden;
1351       } else {
1352         // If the keyedHistogram's name is matched
1353         allElementsHidden = false;
1354         keyedElement.key.hidden = false;
1355         for (let element of keyedElement.datas) {
1356           element.hidden = false;
1357         }
1358       }
1359     });
1360     return allElementsHidden;
1361   },
1363   searchHandler(e) {
1364     if (this.idleTimeout) {
1365       clearTimeout(this.idleTimeout);
1366     }
1367     this.idleTimeout = setTimeout(
1368       () => Search.search(e.target.value),
1369       FILTER_IDLE_TIMEOUT
1370     );
1371   },
1373   search(text, sectionParam = null) {
1374     let section = sectionParam;
1375     if (!section) {
1376       let sectionId = document
1377         .querySelector(".category.selected")
1378         .getAttribute("value");
1379       section = document.getElementById(sectionId);
1380     }
1381     if (Search.blacklist.includes(section.id)) {
1382       return false;
1383     }
1384     let noSearchResults = true;
1385     // In the home section, we search all other sections:
1386     if (section.id === "home-section") {
1387       return this.homeSearch(text);
1388     }
1390     if (section.id === "histograms-section") {
1391       let histograms = section.getElementsByClassName("histogram");
1392       noSearchResults = this.filterElements(histograms, text);
1393     } else if (section.id === "keyed-histograms-section") {
1394       let keyedElements = [];
1395       let keyedHistograms = section.getElementsByClassName("keyed-histogram");
1396       for (let key of keyedHistograms) {
1397         let datas = key.getElementsByClassName("histogram");
1398         keyedElements.push({ key, datas });
1399       }
1400       noSearchResults = this.filterKeyedElements(keyedElements, text);
1401     } else if (section.id === "keyed-scalars-section") {
1402       let keyedElements = [];
1403       let keyedScalars = section.getElementsByClassName("keyed-scalar");
1404       for (let key of keyedScalars) {
1405         let datas = key.querySelector("table").rows;
1406         keyedElements.push({ key, datas });
1407       }
1408       noSearchResults = this.filterKeyedElements(keyedElements, text);
1409     } else if (section.matches(".text-search")) {
1410       let tables = section.querySelectorAll("table");
1411       for (let table of tables) {
1412         // If we unhide anything, flip noSearchResults to
1413         // false so we don't show the "no results" bits.
1414         if (!this.filterTextRows(table, text)) {
1415           noSearchResults = false;
1416         }
1417       }
1418     } else if (section.querySelector(".sub-section")) {
1419       let keyedSubSections = [];
1420       let subsections = section.querySelectorAll(".sub-section");
1421       for (let section of subsections) {
1422         let datas = section.querySelector("table").rows;
1423         keyedSubSections.push({ key: section, datas });
1424       }
1425       noSearchResults = this.filterKeyedElements(keyedSubSections, text);
1426     } else {
1427       let tables = section.querySelectorAll("table");
1428       for (let table of tables) {
1429         noSearchResults = this.filterElements(table.rows, text);
1430         if (table.caption) {
1431           table.caption.hidden = noSearchResults;
1432         }
1433       }
1434     }
1436     changeUrlSearch(text);
1438     if (!sectionParam) {
1439       // If we are not searching in all section.
1440       this.updateNoResults(text, noSearchResults);
1441     }
1442     return noSearchResults;
1443   },
1445   updateNoResults(text, noSearchResults) {
1446     document
1447       .getElementById("no-search-results")
1448       .classList.toggle("hidden", !noSearchResults);
1449     if (noSearchResults) {
1450       let section = document.querySelector(".category.selected > span");
1451       let searchResultsText = document.getElementById("no-search-results-text");
1452       if (section.parentElement.id === "category-home") {
1453         document.l10n.setAttributes(
1454           searchResultsText,
1455           "about-telemetry-no-search-results-all",
1456           { searchTerms: text }
1457         );
1458       } else {
1459         let sectionName = section.textContent.trim();
1460         text === ""
1461           ? document.l10n.setAttributes(
1462               searchResultsText,
1463               "about-telemetry-no-data-to-display",
1464               { sectionName }
1465             )
1466           : document.l10n.setAttributes(
1467               searchResultsText,
1468               "about-telemetry-no-search-results",
1469               { sectionName, currentSearchText: text }
1470             );
1471       }
1472     }
1473   },
1475   resetHome() {
1476     document.getElementById("main").classList.remove("search");
1477     document.getElementById("no-search-results").classList.add("hidden");
1478     adjustHeaderState();
1479     Array.from(document.querySelectorAll("section")).forEach(section => {
1480       section.classList.toggle("active", section.id == "home-section");
1481     });
1482   },
1484   homeSearch(text) {
1485     changeUrlSearch(text);
1486     removeSearchSectionTitles();
1487     if (text === "") {
1488       this.resetHome();
1489       return;
1490     }
1491     document.getElementById("main").classList.add("search");
1492     adjustHeaderState(text);
1493     let noSearchResults = true;
1494     Array.from(document.querySelectorAll("section")).forEach(section => {
1495       if (section.id == "home-section" || section.id == "raw-payload-section") {
1496         section.classList.remove("active");
1497         return;
1498       }
1499       section.classList.add("active");
1500       let sectionHidden = this.search(text, section);
1501       if (!sectionHidden) {
1502         let sectionTitle = document.querySelector(
1503           `.category[value="${section.id}"] .category-name`
1504         ).textContent;
1505         let sectionDataDiv = document.querySelector(
1506           `#${section.id}.has-data.active .data`
1507         );
1508         let titleDiv = document.createElement("h1");
1509         titleDiv.classList.add("data", "search-section-title");
1510         titleDiv.textContent = sectionTitle;
1511         section.insertBefore(titleDiv, sectionDataDiv);
1512         noSearchResults = false;
1513       } else {
1514         // Hide all subsections if the section is hidden
1515         let subsections = section.querySelectorAll(".sub-section");
1516         for (let subsection of subsections) {
1517           subsection.hidden = true;
1518         }
1519       }
1520     });
1521     this.updateNoResults(text, noSearchResults);
1522   },
1526  * Helper function to render JS objects with white space between top level elements
1527  * so that they look better in the browser
1528  * @param   aObject JavaScript object or array to render
1529  * @return  String
1530  */
1531 function RenderObject(aObject) {
1532   let output = "";
1533   if (Array.isArray(aObject)) {
1534     if (!aObject.length) {
1535       return "[]";
1536     }
1537     output = "[" + JSON.stringify(aObject[0]);
1538     for (let i = 1; i < aObject.length; i++) {
1539       output += ", " + JSON.stringify(aObject[i]);
1540     }
1541     return output + "]";
1542   }
1543   let keys = Object.keys(aObject);
1544   if (!keys.length) {
1545     return "{}";
1546   }
1547   output = '{"' + keys[0] + '":\u00A0' + JSON.stringify(aObject[keys[0]]);
1548   for (let i = 1; i < keys.length; i++) {
1549     output += ', "' + keys[i] + '":\u00A0' + JSON.stringify(aObject[keys[i]]);
1550   }
1551   return output + "}";
1554 var GenericSubsection = {
1555   addSubSectionToSidebar(id, title) {
1556     let category = document.querySelector("#categories > [value=" + id + "]");
1557     category.classList.add("has-subsection");
1558     let subCategory = document.createElement("div");
1559     subCategory.classList.add("category-subsection");
1560     subCategory.setAttribute("value", id + "-" + title);
1561     subCategory.addEventListener("click", ev => {
1562       let section = ev.target;
1563       showSubSection(section);
1564     });
1565     subCategory.appendChild(document.createTextNode(title));
1566     category.appendChild(subCategory);
1567   },
1569   render(data, dataDiv, sectionID) {
1570     for (let [title, sectionData] of data) {
1571       let hasData = sectionData.size > 0;
1572       let s = this.renderSubsectionHeader(title, hasData, sectionID);
1573       s.appendChild(this.renderSubsectionData(title, sectionData));
1574       dataDiv.appendChild(s);
1575     }
1576   },
1578   renderSubsectionHeader(title, hasData, sectionID) {
1579     this.addSubSectionToSidebar(sectionID, title);
1580     let section = document.createElement("div");
1581     section.setAttribute("id", sectionID + "-" + title);
1582     section.classList.add("sub-section");
1583     if (hasData) {
1584       section.classList.add("has-subdata");
1585     }
1586     return section;
1587   },
1589   renderSubsectionData(title, data) {
1590     // Create data container
1591     let dataDiv = document.createElement("div");
1592     dataDiv.setAttribute("class", "subsection-data subdata");
1593     // Instanciate the data
1594     let table = GenericTable.render(data);
1595     let caption = document.createElement("caption");
1596     caption.textContent = title;
1597     table.appendChild(caption);
1598     dataDiv.appendChild(table);
1600     return dataDiv;
1601   },
1603   deleteAllSubSections() {
1604     let subsections = document.querySelectorAll(".category-subsection");
1605     subsections.forEach(el => {
1606       el.parentElement.removeChild(el);
1607     });
1608   },
1611 var GenericTable = {
1612   // Returns a table with key and value headers
1613   defaultHeadings() {
1614     return ["about-telemetry-keys-header", "about-telemetry-values-header"];
1615   },
1617   /**
1618    * Returns a n-column table.
1619    * @param rows An array of arrays, each containing data to render
1620    *             for one row.
1621    * @param headings The column header strings.
1622    */
1623   render(rows, headings = this.defaultHeadings()) {
1624     let table = document.createElement("table");
1625     this.renderHeader(table, headings);
1626     this.renderBody(table, rows);
1627     return table;
1628   },
1630   /**
1631    * Create the table header.
1632    * Tabs & newlines added to cells to make it easier to copy-paste.
1633    *
1634    * @param table Table element
1635    * @param headings Array of column header strings.
1636    */
1637   renderHeader(table, headings) {
1638     let headerRow = document.createElement("tr");
1639     table.appendChild(headerRow);
1641     for (let i = 0; i < headings.length; ++i) {
1642       let column = document.createElement("th");
1643       document.l10n.setAttributes(column, headings[i]);
1644       headerRow.appendChild(column);
1645     }
1646   },
1648   /**
1649    * Create the table body
1650    * Tabs & newlines added to cells to make it easier to copy-paste.
1651    *
1652    * @param table Table element
1653    * @param rows An array of arrays, each containing data to render
1654    *             for one row.
1655    */
1656   renderBody(table, rows) {
1657     for (let row of rows) {
1658       row = row.map(value => {
1659         // use .valueOf() to unbox Number, String, etc. objects
1660         if (
1661           value &&
1662           typeof value == "object" &&
1663           typeof value.valueOf() == "object"
1664         ) {
1665           return RenderObject(value);
1666         }
1667         return value;
1668       });
1670       let newRow = document.createElement("tr");
1671       newRow.id = row[0];
1672       table.appendChild(newRow);
1674       for (let i = 0; i < row.length; ++i) {
1675         let suffix = i == row.length - 1 ? "\n" : "\t";
1676         let field = document.createElement("td");
1677         field.appendChild(document.createTextNode(row[i] + suffix));
1678         newRow.appendChild(field);
1679       }
1680     }
1681   },
1684 var KeyedHistogram = {
1685   render(parent, id, keyedHistogram) {
1686     let outerDiv = document.createElement("div");
1687     outerDiv.className = "keyed-histogram";
1688     outerDiv.id = id;
1690     let divTitle = document.createElement("div");
1691     divTitle.classList.add("keyed-title");
1692     divTitle.appendChild(document.createTextNode(id));
1693     outerDiv.appendChild(divTitle);
1695     for (let [name, hgram] of Object.entries(keyedHistogram)) {
1696       Histogram.render(outerDiv, name, hgram);
1697     }
1699     parent.appendChild(outerDiv);
1700     return outerDiv;
1701   },
1704 var AddonDetails = {
1705   /**
1706    * Render the addon details section as a series of headers followed by key/value tables
1707    * @param aPing A ping object to render the data from.
1708    */
1709   render(aPing) {
1710     let addonSection = document.getElementById("addon-details");
1711     removeAllChildNodes(addonSection);
1712     let addonDetails = aPing.payload.addonDetails;
1713     const hasData = addonDetails && !!Object.keys(addonDetails).length;
1714     setHasData("addon-details-section", hasData);
1715     if (!hasData) {
1716       return;
1717     }
1719     for (let provider in addonDetails) {
1720       let providerSection = document.createElement("caption");
1721       document.l10n.setAttributes(
1722         providerSection,
1723         "about-telemetry-addon-provider",
1724         { addonProvider: provider }
1725       );
1726       let headingStrings = [
1727         "about-telemetry-addon-table-id",
1728         "about-telemetry-addon-table-details",
1729       ];
1730       let table = GenericTable.render(
1731         explodeObject(addonDetails[provider]),
1732         headingStrings
1733       );
1734       table.appendChild(providerSection);
1735       addonSection.appendChild(table);
1736     }
1737   },
1740 class Section {
1741   static renderContent(data, process, div, section) {
1742     if (data && Object.keys(data).length) {
1743       let s = GenericSubsection.renderSubsectionHeader(process, true, section);
1744       let heading = document.createElement("h2");
1745       document.l10n.setAttributes(heading, "about-telemetry-process", {
1746         process,
1747       });
1748       s.appendChild(heading);
1750       this.renderData(data, s);
1752       div.appendChild(s);
1753       let separator = document.createElement("div");
1754       separator.classList.add("clearfix");
1755       div.appendChild(separator);
1756     }
1757   }
1759   /**
1760    * Make parent process the first one, content process the second
1761    * then sort processes alphabetically
1762    */
1763   static processesComparator(a, b) {
1764     if (a === "parent" || (a === "content" && b !== "parent")) {
1765       return -1;
1766     } else if (b === "parent" || b === "content") {
1767       return 1;
1768     } else if (a < b) {
1769       return -1;
1770     } else if (a > b) {
1771       return 1;
1772     }
1773     return 0;
1774   }
1776   /**
1777    * Render sections
1778    */
1779   static renderSection(divName, section, aPayload) {
1780     let div = document.getElementById(divName);
1781     removeAllChildNodes(div);
1783     let data = {};
1784     let hasData = false;
1785     let selectedStore = getSelectedStore();
1787     let payload = aPayload.stores;
1789     let isCurrentPayload = !!payload;
1791     // Sort processes
1792     let sortedProcesses = isCurrentPayload
1793       ? Object.keys(payload[selectedStore]).sort(this.processesComparator)
1794       : Object.keys(aPayload.processes).sort(this.processesComparator);
1796     // Render content by process
1797     for (const process of sortedProcesses) {
1798       data = isCurrentPayload
1799         ? this.dataFiltering(payload, selectedStore, process)
1800         : this.archivePingDataFiltering(aPayload, process);
1801       hasData = hasData || !ObjectUtils.isEmpty(data);
1802       this.renderContent(data, process, div, section, this.renderData);
1803     }
1804     setHasData(section, hasData);
1805   }
1808 class Scalars extends Section {
1809   /**
1810    * Return data from the current ping
1811    */
1812   static dataFiltering(payload, selectedStore, process) {
1813     return payload[selectedStore][process].scalars;
1814   }
1816   /**
1817    * Return data from an archived ping
1818    */
1819   static archivePingDataFiltering(payload, process) {
1820     return payload.processes[process].scalars;
1821   }
1823   static renderData(data, div) {
1824     const scalarsHeadings = [
1825       "about-telemetry-names-header",
1826       "about-telemetry-values-header",
1827     ];
1828     let scalarsTable = GenericTable.render(
1829       explodeObject(data),
1830       scalarsHeadings
1831     );
1832     div.appendChild(scalarsTable);
1833   }
1835   /**
1836    * Render the scalar data - if present - from the payload in a simple key-value table.
1837    * @param aPayload A payload object to render the data from.
1838    */
1839   static render(aPayload) {
1840     const divName = "scalars";
1841     const section = "scalars-section";
1842     this.renderSection(divName, section, aPayload);
1843   }
1846 class KeyedScalars extends Section {
1847   /**
1848    * Return data from the current ping
1849    */
1850   static dataFiltering(payload, selectedStore, process) {
1851     return payload[selectedStore][process].keyedScalars;
1852   }
1854   /**
1855    * Return data from an archived ping
1856    */
1857   static archivePingDataFiltering(payload, process) {
1858     return payload.processes[process].keyedScalars;
1859   }
1861   static renderData(data, div) {
1862     const scalarsHeadings = [
1863       "about-telemetry-names-header",
1864       "about-telemetry-values-header",
1865     ];
1866     for (let scalarId in data) {
1867       // Add the name of the scalar.
1868       let container = document.createElement("div");
1869       container.classList.add("keyed-scalar");
1870       container.id = scalarId;
1871       let scalarNameSection = document.createElement("p");
1872       scalarNameSection.classList.add("keyed-title");
1873       scalarNameSection.appendChild(document.createTextNode(scalarId));
1874       container.appendChild(scalarNameSection);
1875       // Populate the section with the key-value pairs from the scalar.
1876       const table = GenericTable.render(
1877         explodeObject(data[scalarId]),
1878         scalarsHeadings
1879       );
1880       container.appendChild(table);
1881       div.appendChild(container);
1882     }
1883   }
1885   /**
1886    * Render the keyed scalar data - if present - from the payload in a simple key-value table.
1887    * @param aPayload A payload object to render the data from.
1888    */
1889   static render(aPayload) {
1890     const divName = "keyed-scalars";
1891     const section = "keyed-scalars-section";
1892     this.renderSection(divName, section, aPayload);
1893   }
1896 var Events = {
1897   /**
1898    * Render the event data - if present - from the payload in a simple table.
1899    * @param aPayload A payload object to render the data from.
1900    */
1901   render(aPayload) {
1902     let eventsDiv = document.getElementById("events");
1903     removeAllChildNodes(eventsDiv);
1904     const headings = [
1905       "about-telemetry-time-stamp-header",
1906       "about-telemetry-category-header",
1907       "about-telemetry-method-header",
1908       "about-telemetry-object-header",
1909       "about-telemetry-values-header",
1910       "about-telemetry-extra-header",
1911     ];
1912     let payload = aPayload.processes;
1913     let hasData = false;
1914     if (payload) {
1915       for (const process of Object.keys(aPayload.processes)) {
1916         let data = aPayload.processes[process].events;
1917         if (data && Object.keys(data).length) {
1918           hasData = true;
1919           let s = GenericSubsection.renderSubsectionHeader(
1920             process,
1921             true,
1922             "events-section"
1923           );
1924           let heading = document.createElement("h2");
1925           heading.textContent = process;
1926           s.appendChild(heading);
1927           const table = GenericTable.render(data, headings);
1928           s.appendChild(table);
1929           eventsDiv.appendChild(s);
1930           let separator = document.createElement("div");
1931           separator.classList.add("clearfix");
1932           eventsDiv.appendChild(separator);
1933         }
1934       }
1935     } else {
1936       // handle archived ping
1937       for (const process of Object.keys(aPayload.events)) {
1938         let data = process;
1939         if (data && Object.keys(data).length) {
1940           hasData = true;
1941           let s = GenericSubsection.renderSubsectionHeader(
1942             process,
1943             true,
1944             "events-section"
1945           );
1946           let heading = document.createElement("h2");
1947           heading.textContent = process;
1948           s.appendChild(heading);
1949           const table = GenericTable.render(data, headings);
1950           eventsDiv.appendChild(table);
1951           let separator = document.createElement("div");
1952           separator.classList.add("clearfix");
1953           eventsDiv.appendChild(separator);
1954         }
1955       }
1956     }
1957     setHasData("events-section", hasData);
1958   },
1962  * Helper function for showing either the toggle element or "No data collected" message for a section
1964  * @param aSectionID ID of the section element that needs to be changed
1965  * @param aHasData true (default) indicates that toggle should be displayed
1966  */
1967 function setHasData(aSectionID, aHasData) {
1968   let sectionElement = document.getElementById(aSectionID);
1969   sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
1971   // Display or Hide the section in the sidebar
1972   let sectionCategory = document.querySelector(
1973     ".category[value=" + aSectionID + "]"
1974   );
1975   sectionCategory.classList[aHasData ? "add" : "remove"]("has-data");
1979  * Sets l10n attributes based on the Telemetry Server Owner pref.
1980  */
1981 function setupServerOwnerBranding() {
1982   let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
1983   const elements = [
1984     [document.getElementById("page-subtitle"), "about-telemetry-page-subtitle"],
1985   ];
1986   for (const [elt, l10nName] of elements) {
1987     document.l10n.setAttributes(elt, l10nName, {
1988       telemetryServerOwner: serverOwner,
1989     });
1990   }
1994  * Display the store selector if we are on one
1995  * of the whitelisted sections
1996  */
1997 function displayStoresSelector(selectedSection) {
1998   let whitelist = [
1999     "scalars-section",
2000     "keyed-scalars-section",
2001     "histograms-section",
2002     "keyed-histograms-section",
2003   ];
2004   let stores = document.getElementById("stores");
2005   stores.hidden = !whitelist.includes(selectedSection);
2006   let storesLabel = document.getElementById("storesLabel");
2007   storesLabel.hidden = !whitelist.includes(selectedSection);
2010 function refreshSearch() {
2011   removeSearchSectionTitles();
2012   let selectedSection = document
2013     .querySelector(".category.selected")
2014     .getAttribute("value");
2015   let search = document.getElementById("search");
2016   if (!Search.blacklist.includes(selectedSection)) {
2017     Search.search(search.value);
2018   }
2021 function adjustSearchState() {
2022   removeSearchSectionTitles();
2023   let selectedSection = document
2024     .querySelector(".category.selected")
2025     .getAttribute("value");
2026   let search = document.getElementById("search");
2027   search.value = "";
2028   search.hidden = Search.blacklist.includes(selectedSection);
2029   document.getElementById("no-search-results").classList.add("hidden");
2030   Search.search(""); // reinitialize search state.
2033 function removeSearchSectionTitles() {
2034   for (let sectionTitleDiv of Array.from(
2035     document.getElementsByClassName("search-section-title")
2036   )) {
2037     sectionTitleDiv.remove();
2038   }
2041 function adjustSection() {
2042   let selectedCategory = document.querySelector(".category.selected");
2043   if (!selectedCategory.classList.contains("has-data")) {
2044     PingPicker._showStructuredPingData();
2045   }
2048 function adjustHeaderState(title = null) {
2049   let selected = document.querySelector(".category.selected .category-name");
2050   let selectedTitle = selected.textContent.trim();
2051   let sectionTitle = document.getElementById("sectionTitle");
2052   if (title !== null) {
2053     document.l10n.setAttributes(
2054       sectionTitle,
2055       "about-telemetry-results-for-search",
2056       { searchTerms: title }
2057     );
2058   } else {
2059     sectionTitle.textContent = selectedTitle;
2060   }
2061   let search = document.getElementById("search");
2062   if (selected.parentElement.id === "category-home") {
2063     document.l10n.setAttributes(
2064       search,
2065       "about-telemetry-filter-all-placeholder"
2066     );
2067   } else {
2068     document.l10n.setAttributes(search, "about-telemetry-filter-placeholder", {
2069       selectedTitle,
2070     });
2071   }
2075  * Change the url according to the current section displayed
2076  * e.g about:telemetry#general-data
2077  */
2078 function changeUrlPath(selectedSection, subSection) {
2079   if (subSection) {
2080     let hash = window.location.hash.split("_")[0] + "_" + selectedSection;
2081     window.location.hash = hash;
2082   } else {
2083     window.location.hash = selectedSection.replace("-section", "-tab");
2084   }
2088  * Change the url according to the current search text
2089  */
2090 function changeUrlSearch(searchText) {
2091   let currentHash = window.location.hash;
2092   let hashWithoutSearch = currentHash.split(Search.HASH_SEARCH)[0];
2093   let hash = "";
2095   if (!currentHash && !searchText) {
2096     return;
2097   }
2098   if (!currentHash.includes(Search.HASH_SEARCH) && hashWithoutSearch) {
2099     hashWithoutSearch += "_";
2100   }
2101   if (searchText) {
2102     hash =
2103       hashWithoutSearch + Search.HASH_SEARCH + searchText.replace(/ /g, "+");
2104   } else if (hashWithoutSearch) {
2105     hash = hashWithoutSearch.slice(0, hashWithoutSearch.length - 1);
2106   }
2108   window.location.hash = hash;
2112  * Change the section displayed
2113  */
2114 function show(selected) {
2115   let selectedValue = selected.getAttribute("value");
2116   if (selectedValue === "raw-json-viewer") {
2117     openJsonInFirefoxJsonViewer(JSON.stringify(gPingData, null, 2));
2118     return;
2119   }
2121   let selected_section = document.getElementById(selectedValue);
2122   let subsections = selected_section.querySelectorAll(".sub-section");
2123   if (selected.classList.contains("has-subsection")) {
2124     for (let subsection of selected.children) {
2125       subsection.classList.remove("selected");
2126     }
2127   }
2128   if (subsections) {
2129     for (let subsection of subsections) {
2130       subsection.hidden = false;
2131     }
2132   }
2134   let current_button = document.querySelector(".category.selected");
2135   if (current_button == selected) {
2136     return;
2137   }
2138   current_button.classList.remove("selected");
2139   selected.classList.add("selected");
2141   document.querySelectorAll("section").forEach(section => {
2142     section.classList.remove("active");
2143   });
2144   selected_section.classList.add("active");
2146   adjustHeaderState();
2147   displayStoresSelector(selectedValue);
2148   adjustSearchState();
2149   changeUrlPath(selectedValue);
2152 function showSubSection(selected) {
2153   if (!selected) {
2154     return;
2155   }
2156   let current_selection = document.querySelector(
2157     ".category-subsection.selected"
2158   );
2159   if (current_selection) {
2160     current_selection.classList.remove("selected");
2161   }
2162   selected.classList.add("selected");
2164   let section = document.getElementById(selected.getAttribute("value"));
2165   section.parentElement.childNodes.forEach(element => {
2166     element.hidden = true;
2167   });
2168   section.hidden = false;
2170   let title =
2171     selected.parentElement.querySelector(".category-name").textContent;
2172   let subsection = selected.textContent;
2173   document.getElementById("sectionTitle").textContent =
2174     title + " - " + subsection;
2175   changeUrlPath(subsection, true);
2179  * Initializes load/unload, pref change and mouse-click listeners
2180  */
2181 function setupListeners() {
2182   Settings.attachObservers();
2183   PingPicker.attachObservers();
2184   RawPayloadData.attachObservers();
2186   let menu = document.getElementById("categories");
2187   menu.addEventListener("click", e => {
2188     if (e.target && e.target.parentNode == menu) {
2189       show(e.target);
2190     }
2191   });
2193   let search = document.getElementById("search");
2194   search.addEventListener("input", Search.searchHandler);
2196   document
2197     .getElementById("late-writes-fetch-symbols")
2198     .addEventListener("click", function () {
2199       if (!gPingData) {
2200         return;
2201       }
2203       let lateWrites = gPingData.payload.lateWrites;
2204       let req = new SymbolicationRequest(
2205         "late-writes",
2206         LateWritesSingleton.renderHeader,
2207         lateWrites.memoryMap,
2208         lateWrites.stacks
2209       );
2210       req.fetchSymbols();
2211     });
2213   document
2214     .getElementById("late-writes-hide-symbols")
2215     .addEventListener("click", function () {
2216       if (!gPingData) {
2217         return;
2218       }
2220       LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
2221     });
2224 // Restores the sections states
2225 function urlSectionRestore(hash) {
2226   if (hash) {
2227     let section = hash.replace("-tab", "-section");
2228     let subsection = section.split("_")[1];
2229     section = section.split("_")[0];
2230     let category = document.querySelector(".category[value=" + section + "]");
2231     if (category) {
2232       show(category);
2233       if (subsection) {
2234         let selector =
2235           ".category-subsection[value=" + section + "-" + subsection + "]";
2236         let subcategory = document.querySelector(selector);
2237         showSubSection(subcategory);
2238       }
2239     }
2240   }
2243 // Restore sections states and search terms
2244 function urlStateRestore() {
2245   let hash = window.location.hash;
2246   let searchQuery = "";
2247   if (hash) {
2248     hash = hash.slice(1);
2249     if (hash.includes(Search.HASH_SEARCH)) {
2250       searchQuery = hash.split(Search.HASH_SEARCH)[1].replace(/[+]/g, " ");
2251       hash = hash.split(Search.HASH_SEARCH)[0];
2252     }
2253     urlSectionRestore(hash);
2254   }
2255   if (searchQuery) {
2256     let search = document.getElementById("search");
2257     search.value = searchQuery;
2258   }
2261 function openJsonInFirefoxJsonViewer(json) {
2262   json = unescape(encodeURIComponent(json));
2263   try {
2264     window.open("data:application/json;base64," + btoa(json));
2265   } catch (e) {
2266     show(document.querySelector(".category[value=raw-payload-section]"));
2267   }
2270 function onLoad() {
2271   window.removeEventListener("load", onLoad);
2272   // Set the text in the page header and elsewhere that needs the server owner.
2273   setupServerOwnerBranding();
2275   // Set up event listeners
2276   setupListeners();
2278   // Render settings.
2279   Settings.render();
2281   adjustHeaderState();
2283   urlStateRestore();
2285   // Update ping data when async Telemetry init is finished.
2286   Telemetry.asyncFetchTelemetryData(async () => {
2287     await PingPicker.update();
2288   });
2291 var LateWritesSingleton = {
2292   renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
2293     StackRenderer.renderHeader(
2294       "late-writes",
2295       "about-telemetry-late-writes-title",
2296       { lateWriteCount: aIndex + 1 }
2297     );
2298   },
2300   renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
2301     let hasData = !!(
2302       lateWrites &&
2303       lateWrites.stacks &&
2304       lateWrites.stacks.length
2305     );
2306     setHasData("late-writes-section", hasData);
2307     if (!hasData) {
2308       return;
2309     }
2311     let stacks = lateWrites.stacks;
2312     let memoryMap = lateWrites.memoryMap;
2313     StackRenderer.renderStacks(
2314       "late-writes",
2315       stacks,
2316       memoryMap,
2317       LateWritesSingleton.renderHeader
2318     );
2319   },
2322 class HistogramSection extends Section {
2323   /**
2324    * Return data from the current ping
2325    */
2326   static dataFiltering(payload, selectedStore, process) {
2327     return payload[selectedStore][process].histograms;
2328   }
2330   /**
2331    * Return data from an archived ping
2332    */
2333   static archivePingDataFiltering(payload, process) {
2334     if (process === "parent") {
2335       return payload.histograms;
2336     }
2337     return payload.processes[process].histograms;
2338   }
2340   static renderData(data, div) {
2341     for (let [hName, hgram] of Object.entries(data)) {
2342       Histogram.render(div, hName, hgram, { unpacked: true });
2343     }
2344   }
2346   static render(aPayload) {
2347     const divName = "histograms";
2348     const section = "histograms-section";
2349     this.renderSection(divName, section, aPayload);
2350   }
2353 class KeyedHistogramSection extends Section {
2354   /**
2355    * Return data from the current ping
2356    */
2357   static dataFiltering(payload, selectedStore, process) {
2358     return payload[selectedStore][process].keyedHistograms;
2359   }
2361   /**
2362    * Return data from an archived ping
2363    */
2364   static archivePingDataFiltering(payload, process) {
2365     if (process === "parent") {
2366       return payload.keyedHistograms;
2367     }
2368     return payload.processes[process].keyedHistograms;
2369   }
2371   static renderData(data, div) {
2372     for (let [id, keyed] of Object.entries(data)) {
2373       KeyedHistogram.render(div, id, keyed, { unpacked: true });
2374     }
2375   }
2377   static render(aPayload) {
2378     const divName = "keyed-histograms";
2379     const section = "keyed-histograms-section";
2380     this.renderSection(divName, section, aPayload);
2381   }
2384 var SessionInformation = {
2385   render(aPayload) {
2386     let infoSection = document.getElementById("session-info");
2387     removeAllChildNodes(infoSection);
2389     let hasData = !!Object.keys(aPayload.info).length;
2390     setHasData("session-info-section", hasData);
2392     if (hasData) {
2393       const table = GenericTable.render(explodeObject(aPayload.info));
2394       infoSection.appendChild(table);
2395     }
2396   },
2399 var SimpleMeasurements = {
2400   render(aPayload) {
2401     let simpleSection = document.getElementById("simple-measurements");
2402     removeAllChildNodes(simpleSection);
2404     let simpleMeasurements = this.sortStartupMilestones(
2405       aPayload.simpleMeasurements
2406     );
2407     let hasData = !!Object.keys(simpleMeasurements).length;
2408     setHasData("simple-measurements-section", hasData);
2410     if (hasData) {
2411       const table = GenericTable.render(explodeObject(simpleMeasurements));
2412       simpleSection.appendChild(table);
2413     }
2414   },
2416   /**
2417    * Helper function for sorting the startup milestones in the Simple Measurements
2418    * section into temporal order.
2419    *
2420    * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
2421    * @return Sorted measurements
2422    */
2423   sortStartupMilestones(aSimpleMeasurements) {
2424     const telemetryTimestamps = TelemetryTimestamps.get();
2425     let startupEvents = Services.startup.getStartupInfo();
2426     delete startupEvents.process;
2428     function keyIsMilestone(k) {
2429       return k in startupEvents || k in telemetryTimestamps;
2430     }
2432     let sortedKeys = Object.keys(aSimpleMeasurements);
2434     // Sort the measurements, with startup milestones at the front + ordered by time
2435     sortedKeys.sort(function keyCompare(keyA, keyB) {
2436       let isKeyAMilestone = keyIsMilestone(keyA);
2437       let isKeyBMilestone = keyIsMilestone(keyB);
2439       // First order by startup vs non-startup measurement
2440       if (isKeyAMilestone && !isKeyBMilestone) {
2441         return -1;
2442       }
2443       if (!isKeyAMilestone && isKeyBMilestone) {
2444         return 1;
2445       }
2446       // Don't change order of non-startup measurements
2447       if (!isKeyAMilestone && !isKeyBMilestone) {
2448         return 0;
2449       }
2451       // If both keys are startup measurements, order them by value
2452       return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
2453     });
2455     // Insert measurements into a result object in sort-order
2456     let result = {};
2457     for (let key of sortedKeys) {
2458       result[key] = aSimpleMeasurements[key];
2459     }
2461     return result;
2462   },
2466  * Render stores options
2467  */
2468 function renderStoreList(payload) {
2469   let storeSelect = document.getElementById("stores");
2470   let storesLabel = document.getElementById("storesLabel");
2471   removeAllChildNodes(storeSelect);
2473   if (!("stores" in payload)) {
2474     storeSelect.classList.add("hidden");
2475     storesLabel.classList.add("hidden");
2476     return;
2477   }
2479   storeSelect.classList.remove("hidden");
2480   storesLabel.classList.remove("hidden");
2481   storeSelect.disabled = false;
2483   for (let store of Object.keys(payload.stores)) {
2484     let option = document.createElement("option");
2485     option.appendChild(document.createTextNode(store));
2486     option.setAttribute("value", store);
2487     // Select main store by default
2488     if (store === "main") {
2489       option.selected = true;
2490     }
2491     storeSelect.appendChild(option);
2492   }
2496  * Return the selected store
2497  */
2498 function getSelectedStore() {
2499   let storeSelect = document.getElementById("stores");
2500   let storeSelectedOption = storeSelect.selectedOptions.item(0);
2501   let selectedStore =
2502     storeSelectedOption !== null
2503       ? storeSelectedOption.getAttribute("value")
2504       : undefined;
2505   return selectedStore;
2508 function togglePingSections(isMainPing) {
2509   // We always show the sections that are "common" to all pings.
2510   let commonSections = new Set([
2511     "heading",
2512     "home-section",
2513     "general-data-section",
2514     "environment-data-section",
2515     "raw-json-viewer",
2516   ]);
2518   let elements = document.querySelectorAll(".category");
2519   for (let section of elements) {
2520     if (commonSections.has(section.getAttribute("value"))) {
2521       continue;
2522     }
2523     // Only show the raw payload for non main ping.
2524     if (section.getAttribute("value") == "raw-payload-section") {
2525       section.classList.toggle("has-data", !isMainPing);
2526     } else {
2527       section.classList.toggle("has-data", isMainPing);
2528     }
2529   }
2532 function displayPingData(ping, updatePayloadList = false) {
2533   gPingData = ping;
2534   try {
2535     PingPicker.render();
2536     displayRichPingData(ping, updatePayloadList);
2537     adjustSection();
2538     refreshSearch();
2539   } catch (err) {
2540     console.log(err);
2541     PingPicker._showRawPingData();
2542   }
2545 function displayRichPingData(ping, updatePayloadList) {
2546   // Update the payload list and store lists
2547   if (updatePayloadList) {
2548     renderStoreList(ping.payload);
2549   }
2551   // Show general data.
2552   GeneralData.render(ping);
2554   // Show environment data.
2555   EnvironmentData.render(ping);
2557   RawPayloadData.render(ping);
2559   // We have special rendering code for the payloads from "main" and "event" pings.
2560   // For any other pings we just render the raw JSON payload.
2561   let isMainPing = ping.type == "main" || ping.type == "saved-session";
2562   let isEventPing = ping.type == "event";
2563   togglePingSections(isMainPing);
2565   if (isEventPing) {
2566     // Copy the payload, so we don't modify the raw representation
2567     // Ensure we always have at least the parent process.
2568     let payload = { processes: { parent: {} } };
2569     for (let process of Object.keys(ping.payload.events)) {
2570       payload.processes[process] = {
2571         events: ping.payload.events[process],
2572       };
2573     }
2575     // We transformed the actual payload, let's reload the store list if necessary.
2576     if (updatePayloadList) {
2577       renderStoreList(payload);
2578     }
2580     // Show event data.
2581     Events.render(payload);
2582     return;
2583   }
2585   if (!isMainPing) {
2586     return;
2587   }
2589   // Show slow SQL stats
2590   SlowSQL.render(ping);
2592   // Render Addon details.
2593   AddonDetails.render(ping);
2595   let payload = ping.payload;
2596   // Show basic session info gathered
2597   SessionInformation.render(payload);
2599   // Show scalar data.
2600   Scalars.render(payload);
2601   KeyedScalars.render(payload);
2603   // Show histogram data
2604   HistogramSection.render(payload);
2606   // Show keyed histogram data
2607   KeyedHistogramSection.render(payload);
2609   // Show event data.
2610   Events.render(payload);
2612   LateWritesSingleton.renderLateWrites(payload.lateWrites);
2614   // Show simple measurements
2615   SimpleMeasurements.render(payload);
2618 window.addEventListener("load", onLoad);