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