Bug 1904048 - Add prototype for ImageBitmap's BindingJSObjectMallocBytes. r=aosmond
[gecko.git] / toolkit / content / aboutTelemetry.js
blob9cf3c3320d3afe0dec955567b7ff8ec9f66110a4
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", () => {
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     TelemetryController.ensureInitialized().then(() =>
356       this._doUpdateCurrentPingData()
357     );
358   },
360   _doUpdateCurrentPingData() {
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     displayPingData(ping, true);
420   },
422   _updateArchivedPingData() {
423     let id = this._getSelectedPingId();
424     let res = Promise.resolve();
425     if (id) {
426       res = TelemetryArchive.promiseArchivedPingById(id).then(ping =>
427         displayPingData(ping, true)
428       );
429     }
430     return res;
431   },
433   async _updateArchivedPingList(pingList) {
434     // The archived ping list is sorted in ascending timestamp order,
435     // but descending is more practical for the operations we do here.
436     pingList.reverse();
437     this._archivedPings = pingList;
438     // Render the archive data.
439     this._renderPingList();
440     // Update the displayed ping.
441     await this._updateArchivedPingData();
442   },
444   _renderPingList() {
445     let pingSelector = document.getElementById("choose-ping-id");
446     Array.from(pingSelector.children).forEach(child =>
447       removeAllChildNodes(child)
448     );
450     let pingTypes = new Set();
451     pingTypes.add(this.TYPE_ALL);
453     const today = new Date();
454     today.setHours(0, 0, 0, 0);
455     const yesterday = new Date(today);
456     yesterday.setDate(today.getDate() - 1);
458     for (let p of this._archivedPings) {
459       pingTypes.add(p.type);
460       const pingDate = new Date(p.timestampCreated);
461       const datetimeText = new Services.intl.DateTimeFormat(undefined, {
462         dateStyle: "short",
463         timeStyle: "medium",
464       }).format(pingDate);
465       const pingName = `${datetimeText}, ${p.type}`;
467       let option = document.createElement("option");
468       let content = document.createTextNode(pingName);
469       option.appendChild(content);
470       option.setAttribute("value", p.id);
471       option.dataset.type = p.type;
472       option.dataset.date = datetimeText;
474       pingDate.setHours(0, 0, 0, 0);
475       if (pingDate.getTime() === today.getTime()) {
476         pingSelector.children[0].appendChild(option);
477       } else if (pingDate.getTime() === yesterday.getTime()) {
478         pingSelector.children[1].appendChild(option);
479       } else {
480         pingSelector.children[2].appendChild(option);
481       }
482     }
483     this._renderPingTypes(pingTypes);
484   },
486   _renderPingTypes(pingTypes) {
487     let pingTypeSelector = document.getElementById("choose-ping-type");
488     removeAllChildNodes(pingTypeSelector);
489     pingTypes.forEach(type => {
490       let option = document.createElement("option");
491       option.appendChild(document.createTextNode(type));
492       option.setAttribute("value", type);
493       pingTypeSelector.appendChild(option);
494     });
495   },
497   _movePingIndex(offset) {
498     if (this.viewCurrentPingData) {
499       return;
500     }
501     let typeSelector = document.getElementById("choose-ping-type");
502     let type = typeSelector.selectedOptions.item(0).value;
504     let id = this._getSelectedPingId();
505     let index = this._archivedPings.findIndex(p => p.id == id);
506     let newIndex = Math.min(
507       Math.max(0, index + offset),
508       this._archivedPings.length - 1
509     );
511     let pingList;
512     if (offset > 0) {
513       pingList = this._archivedPings.slice(newIndex);
514     } else {
515       pingList = this._archivedPings.slice(0, newIndex);
516       pingList.reverse();
517     }
519     let ping = pingList.find(p => {
520       return type == this.TYPE_ALL || p.type == type;
521     });
523     if (ping) {
524       this.selectPing(ping);
525       this._updateArchivedPingData();
526     }
527   },
529   selectPing(ping) {
530     let pingSelector = document.getElementById("choose-ping-id");
531     // Use some() to break if we find the ping.
532     Array.from(pingSelector.children).some(group => {
533       return Array.from(group.children).some(option => {
534         if (option.value == ping.id) {
535           option.selected = true;
536           return true;
537         }
538         return false;
539       });
540     });
541   },
543   filterDisplayedPings() {
544     let pingSelector = document.getElementById("choose-ping-id");
545     let typeSelector = document.getElementById("choose-ping-type");
546     let type = typeSelector.selectedOptions.item(0).value;
547     let first = true;
548     Array.from(pingSelector.children).forEach(group => {
549       Array.from(group.children).forEach(option => {
550         if (first && option.dataset.type == type) {
551           option.selected = true;
552           first = false;
553         }
554         option.hidden = type != this.TYPE_ALL && option.dataset.type != type;
555         // Arrow keys should only iterate over visible options
556         option.disabled = option.hidden;
557       });
558     });
559     this._updateArchivedPingData();
560   },
562   _getSelectedPingName() {
563     let pingSelector = document.getElementById("choose-ping-id");
564     let selected = pingSelector.selectedOptions.item(0);
565     return selected.dataset.date;
566   },
568   _getSelectedPingType() {
569     let pingSelector = document.getElementById("choose-ping-id");
570     let selected = pingSelector.selectedOptions.item(0);
571     return selected.dataset.type;
572   },
574   _getSelectedPingId() {
575     let pingSelector = document.getElementById("choose-ping-id");
576     let selected = pingSelector.selectedOptions.item(0);
577     return selected.getAttribute("value");
578   },
580   _showRawPingData() {
581     show(document.getElementById("category-raw"));
582   },
584   _showStructuredPingData() {
585     show(document.getElementById("category-home"));
586   },
589 var GeneralData = {
590   /**
591    * Renders the general data
592    */
593   render(aPing) {
594     setHasData("general-data-section", true);
595     let generalDataSection = document.getElementById("general-data");
596     removeAllChildNodes(generalDataSection);
598     const headings = [
599       "about-telemetry-names-header",
600       "about-telemetry-values-header",
601     ];
603     // The payload & environment parts are handled by other renderers.
604     let ignoreSections = ["payload", "environment"];
605     let data = explodeObject(filterObject(aPing, ignoreSections));
607     const table = GenericTable.render(data, headings);
608     generalDataSection.appendChild(table);
609   },
612 var EnvironmentData = {
613   /**
614    * Renders the environment data
615    */
616   render(ping) {
617     let dataDiv = document.getElementById("environment-data");
618     removeAllChildNodes(dataDiv);
619     const hasData = !!ping.environment;
620     setHasData("environment-data-section", hasData);
621     if (!hasData) {
622       return;
623     }
625     let ignore = ["addons"];
626     let env = filterObject(ping.environment, ignore);
627     let sections = sectionalizeObject(env);
628     GenericSubsection.render(sections, dataDiv, "environment-data-section");
630     // We use specialized rendering here to make the addon and plugin listings
631     // more readable.
632     this.createAddonSection(dataDiv, ping);
633   },
635   renderAddonsObject(addonObj, addonSection, sectionTitle) {
636     let table = document.createElement("table");
637     table.setAttribute("id", sectionTitle);
638     this.appendAddonSubsectionTitle(sectionTitle, table);
640     for (let id of Object.keys(addonObj)) {
641       let addon = addonObj[id];
642       this.appendHeadingName(table, addon.name || id);
643       this.appendAddonID(table, id);
644       let data = explodeObject(addon);
646       for (let [key, value] of data) {
647         this.appendRow(table, key, value);
648       }
649     }
651     addonSection.appendChild(table);
652   },
654   renderKeyValueObject(addonObj, addonSection, sectionTitle) {
655     let data = explodeObject(addonObj);
656     let table = GenericTable.render(data);
657     table.setAttribute("class", sectionTitle);
658     this.appendAddonSubsectionTitle(sectionTitle, table);
659     addonSection.appendChild(table);
660   },
662   appendAddonID(table, addonID) {
663     this.appendRow(table, "id", addonID);
664   },
666   appendHeadingName(table, name) {
667     let headings = document.createElement("tr");
668     this.appendColumn(headings, "th", name);
669     headings.cells[0].colSpan = 2;
670     table.appendChild(headings);
671   },
673   appendAddonSubsectionTitle(section, table) {
674     let caption = document.createElement("caption");
675     caption.appendChild(document.createTextNode(section));
676     table.appendChild(caption);
677   },
679   createAddonSection(dataDiv, ping) {
680     if (!ping || !("environment" in ping) || !("addons" in ping.environment)) {
681       return;
682     }
683     let addonSection = document.createElement("div");
684     addonSection.setAttribute("class", "subsection-data subdata");
685     let addons = ping.environment.addons;
686     this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
687     this.renderKeyValueObject(addons.theme, addonSection, "theme");
688     this.renderAddonsObject(
689       addons.activeGMPlugins,
690       addonSection,
691       "activeGMPlugins"
692     );
694     let hasAddonData = !!Object.keys(ping.environment.addons).length;
695     let s = GenericSubsection.renderSubsectionHeader(
696       "addons",
697       hasAddonData,
698       "environment-data-section"
699     );
700     s.appendChild(addonSection);
701     dataDiv.appendChild(s);
702   },
704   appendRow(table, id, value) {
705     let row = document.createElement("tr");
706     row.id = id;
707     this.appendColumn(row, "td", id);
708     this.appendColumn(row, "td", value);
709     table.appendChild(row);
710   },
711   /**
712    * Helper function for appending a column to the data table.
713    *
714    * @param aRowElement Parent row element
715    * @param aColType Column's tag name
716    * @param aColText Column contents
717    */
718   appendColumn(aRowElement, aColType, aColText) {
719     let colElement = document.createElement(aColType);
720     let colTextElement = document.createTextNode(aColText);
721     colElement.appendChild(colTextElement);
722     aRowElement.appendChild(colElement);
723   },
726 var SlowSQL = {
727   /**
728    * Render slow SQL statistics
729    */
730   render: function SlowSQL_render(aPing) {
731     // We can add the debug SQL data to the current ping later.
732     // However, we need to be careful to never send that debug data
733     // out due to privacy concerns.
734     // We want to show the actual ping data for archived pings,
735     // so skip this there.
737     let debugSlowSql =
738       PingPicker.viewCurrentPingData &&
739       Preferences.get(PREF_DEBUG_SLOW_SQL, false);
740     let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
741     if (!slowSql) {
742       setHasData("slow-sql-section", false);
743       return;
744     }
746     let { mainThread, otherThreads } = debugSlowSql
747       ? Telemetry.debugSlowSQL
748       : aPing.payload.slowSQL;
750     let mainThreadCount = Object.keys(mainThread).length;
751     let otherThreadCount = Object.keys(otherThreads).length;
752     if (mainThreadCount == 0 && otherThreadCount == 0) {
753       setHasData("slow-sql-section", false);
754       return;
755     }
757     setHasData("slow-sql-section", true);
758     if (debugSlowSql) {
759       document.getElementById("sql-warning").hidden = false;
760     }
762     let slowSqlDiv = document.getElementById("slow-sql-tables");
763     removeAllChildNodes(slowSqlDiv);
765     // Main thread
766     if (mainThreadCount > 0) {
767       let table = document.createElement("table");
768       this.renderTableHeader(table, "main");
769       this.renderTable(table, mainThread);
770       slowSqlDiv.appendChild(table);
771     }
773     // Other threads
774     if (otherThreadCount > 0) {
775       let table = document.createElement("table");
776       this.renderTableHeader(table, "other");
777       this.renderTable(table, otherThreads);
778       slowSqlDiv.appendChild(table);
779     }
780   },
782   /**
783    * Creates a header row for a Slow SQL table
784    * Tabs & newlines added to cells to make it easier to copy-paste.
785    *
786    * @param aTable Parent table element
787    * @param aTitle Table's title
788    */
789   renderTableHeader: function SlowSQL_renderTableHeader(aTable, threadType) {
790     let caption = document.createElement("caption");
791     if (threadType == "main") {
792       document.l10n.setAttributes(caption, "about-telemetry-slow-sql-main");
793     }
795     if (threadType == "other") {
796       document.l10n.setAttributes(caption, "about-telemetry-slow-sql-other");
797     }
798     aTable.appendChild(caption);
800     let headings = document.createElement("tr");
801     document.l10n.setAttributes(
802       this.appendColumn(headings, "th"),
803       "about-telemetry-slow-sql-hits"
804     );
805     document.l10n.setAttributes(
806       this.appendColumn(headings, "th"),
807       "about-telemetry-slow-sql-average"
808     );
809     document.l10n.setAttributes(
810       this.appendColumn(headings, "th"),
811       "about-telemetry-slow-sql-statement"
812     );
813     aTable.appendChild(headings);
814   },
816   /**
817    * Fills out the table body
818    * Tabs & newlines added to cells to make it easier to copy-paste.
819    *
820    * @param aTable Parent table element
821    * @param aSql SQL stats object
822    */
823   renderTable: function SlowSQL_renderTable(aTable, aSql) {
824     for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) {
825       let averageTime = totalTime / hitCount;
827       let sqlRow = document.createElement("tr");
829       this.appendColumn(sqlRow, "td", hitCount + "\t");
830       this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
831       this.appendColumn(sqlRow, "td", sql + "\n");
833       aTable.appendChild(sqlRow);
834     }
835   },
837   /**
838    * Helper function for appending a column to a Slow SQL table.
839    *
840    * @param aRowElement Parent row element
841    * @param aColType Column's tag name
842    * @param aColText Column contents
843    */
844   appendColumn: function SlowSQL_appendColumn(
845     aRowElement,
846     aColType,
847     aColText = ""
848   ) {
849     let colElement = document.createElement(aColType);
850     if (aColText) {
851       let colTextElement = document.createTextNode(aColText);
852       colElement.appendChild(colTextElement);
853     }
854     aRowElement.appendChild(colElement);
855     return colElement;
856   },
859 var StackRenderer = {
860   /**
861    * Outputs the memory map associated with this hang report
862    *
863    * @param aDiv Output div
864    */
865   renderMemoryMap: async function StackRenderer_renderMemoryMap(
866     aDiv,
867     memoryMap
868   ) {
869     let memoryMapTitleElement = document.createElement("span");
870     document.l10n.setAttributes(
871       memoryMapTitleElement,
872       "about-telemetry-memory-map-title"
873     );
874     aDiv.appendChild(memoryMapTitleElement);
875     aDiv.appendChild(document.createElement("br"));
877     for (let currentModule of memoryMap) {
878       aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
879       aDiv.appendChild(document.createElement("br"));
880     }
882     aDiv.appendChild(document.createElement("br"));
883   },
885   /**
886    * Outputs the raw PCs from the hang's stack
887    *
888    * @param aDiv Output div
889    * @param aStack Array of PCs from the hang stack
890    */
891   renderStack: function StackRenderer_renderStack(aDiv, aStack) {
892     let stackTitleElement = document.createElement("span");
893     document.l10n.setAttributes(
894       stackTitleElement,
895       "about-telemetry-stack-title"
896     );
897     aDiv.appendChild(stackTitleElement);
898     let stackText = " " + aStack.join(" ");
899     aDiv.appendChild(document.createTextNode(stackText));
901     aDiv.appendChild(document.createElement("br"));
902     aDiv.appendChild(document.createElement("br"));
903   },
904   renderStacks: function StackRenderer_renderStacks(
905     aPrefix,
906     aStacks,
907     aMemoryMap,
908     aRenderHeader
909   ) {
910     let div = document.getElementById(aPrefix);
911     removeAllChildNodes(div);
913     let fetchE = document.getElementById(aPrefix + "-fetch-symbols");
914     if (fetchE) {
915       fetchE.hidden = false;
916     }
917     let hideE = document.getElementById(aPrefix + "-hide-symbols");
918     if (hideE) {
919       hideE.hidden = true;
920     }
922     if (!aStacks.length) {
923       return;
924     }
926     setHasData(aPrefix + "-section", true);
928     this.renderMemoryMap(div, aMemoryMap);
930     for (let i = 0; i < aStacks.length; ++i) {
931       let stack = aStacks[i];
932       aRenderHeader(i);
933       this.renderStack(div, stack);
934     }
935   },
937   /**
938    * Renders the title of the stack: e.g. "Late Write #1" or
939    * "Hang Report #1 (6 seconds)".
940    *
941    * @param aDivId The id of the div to append the header to.
942    * @param aL10nId The l10n id of the message to use for the title.
943    * @param aL10nArgs The l10n args for the provided message id.
944    */
945   renderHeader: function StackRenderer_renderHeader(
946     aDivId,
947     aL10nId,
948     aL10nArgs
949   ) {
950     let div = document.getElementById(aDivId);
952     let titleElement = document.createElement("span");
953     titleElement.className = "stack-title";
955     document.l10n.setAttributes(titleElement, aL10nId, aL10nArgs);
957     div.appendChild(titleElement);
958     div.appendChild(document.createElement("br"));
959   },
962 var RawPayloadData = {
963   /**
964    * Renders the raw pyaload.
965    */
966   render(aPing) {
967     setHasData("raw-payload-section", true);
968     let pre = document.getElementById("raw-payload-data");
969     pre.textContent = JSON.stringify(aPing.payload, null, 2);
970   },
972   attachObservers() {
973     document
974       .getElementById("payload-json-viewer")
975       .addEventListener("click", () => {
976         openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2));
977       });
978   },
981 function SymbolicationRequest(
982   aPrefix,
983   aRenderHeader,
984   aMemoryMap,
985   aStacks,
986   aDurations = null
987 ) {
988   this.prefix = aPrefix;
989   this.renderHeader = aRenderHeader;
990   this.memoryMap = aMemoryMap;
991   this.stacks = aStacks;
992   this.durations = aDurations;
995  * A callback for onreadystatechange. It replaces the numeric stack with
996  * the symbolicated one returned by the symbolication server.
997  */
998 SymbolicationRequest.prototype.handleSymbolResponse =
999   async function SymbolicationRequest_handleSymbolResponse() {
1000     if (this.symbolRequest.readyState != 4) {
1001       return;
1002     }
1004     let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
1005     fetchElement.hidden = true;
1006     let hideElement = document.getElementById(this.prefix + "-hide-symbols");
1007     hideElement.hidden = false;
1008     let div = document.getElementById(this.prefix);
1009     removeAllChildNodes(div);
1010     let errorMessage = await document.l10n.formatValue(
1011       "about-telemetry-error-fetching-symbols"
1012     );
1014     if (this.symbolRequest.status != 200) {
1015       div.appendChild(document.createTextNode(errorMessage));
1016       return;
1017     }
1019     let jsonResponse = {};
1020     try {
1021       jsonResponse = JSON.parse(this.symbolRequest.responseText);
1022     } catch (e) {
1023       div.appendChild(document.createTextNode(errorMessage));
1024       return;
1025     }
1027     for (let i = 0; i < jsonResponse.length; ++i) {
1028       let stack = jsonResponse[i];
1029       this.renderHeader(i, this.durations);
1031       for (let symbol of stack) {
1032         div.appendChild(document.createTextNode(symbol));
1033         div.appendChild(document.createElement("br"));
1034       }
1035       div.appendChild(document.createElement("br"));
1036     }
1037   };
1039  * Send a request to the symbolication server to symbolicate this stack.
1040  */
1041 SymbolicationRequest.prototype.fetchSymbols =
1042   function SymbolicationRequest_fetchSymbols() {
1043     let symbolServerURI = Preferences.get(
1044       PREF_SYMBOL_SERVER_URI,
1045       DEFAULT_SYMBOL_SERVER_URI
1046     );
1047     let request = {
1048       memoryMap: this.memoryMap,
1049       stacks: this.stacks,
1050       version: 3,
1051     };
1052     let requestJSON = JSON.stringify(request);
1054     this.symbolRequest = new XMLHttpRequest();
1055     this.symbolRequest.open("POST", symbolServerURI, true);
1056     this.symbolRequest.setRequestHeader("Content-type", "application/json");
1057     this.symbolRequest.setRequestHeader("Content-length", requestJSON.length);
1058     this.symbolRequest.setRequestHeader("Connection", "close");
1059     this.symbolRequest.onreadystatechange =
1060       this.handleSymbolResponse.bind(this);
1061     this.symbolRequest.send(requestJSON);
1062   };
1064 var Histogram = {
1065   /**
1066    * Renders a single Telemetry histogram
1067    *
1068    * @param aParent Parent element
1069    * @param aName Histogram name
1070    * @param aHgram Histogram information
1071    * @param aOptions Object with render options
1072    *                 * exponential: bars follow logarithmic scale
1073    */
1074   render: function Histogram_render(aParent, aName, aHgram, aOptions) {
1075     let options = aOptions || {};
1076     let hgram = this.processHistogram(aHgram, aName);
1078     let outerDiv = document.createElement("div");
1079     outerDiv.className = "histogram";
1080     outerDiv.id = aName;
1082     let divTitle = document.createElement("div");
1083     divTitle.classList.add("histogram-title");
1084     divTitle.appendChild(document.createTextNode(aName));
1085     outerDiv.appendChild(divTitle);
1087     let divStats = document.createElement("div");
1088     divStats.classList.add("histogram-stats");
1090     let histogramStatsArgs = {
1091       sampleCount: hgram.sample_count,
1092       prettyAverage: hgram.pretty_average,
1093       sum: hgram.sum,
1094     };
1096     document.l10n.setAttributes(
1097       divStats,
1098       "about-telemetry-histogram-stats",
1099       histogramStatsArgs
1100     );
1102     if (isRTL()) {
1103       hgram.values.reverse();
1104     }
1106     let textData = this.renderValues(outerDiv, hgram, options);
1108     // The 'Copy' button contains the textual data, copied to clipboard on click
1109     let copyButton = document.createElement("button");
1110     copyButton.className = "copy-node";
1111     document.l10n.setAttributes(copyButton, "about-telemetry-histogram-copy");
1113     copyButton.addEventListener("click", async function () {
1114       let divStatsString = await document.l10n.formatValue(
1115         "about-telemetry-histogram-stats",
1116         histogramStatsArgs
1117       );
1118       copyButton.histogramText =
1119         aName + EOL + divStatsString + EOL + EOL + textData;
1120       Cc["@mozilla.org/widget/clipboardhelper;1"]
1121         .getService(Ci.nsIClipboardHelper)
1122         .copyString(this.histogramText);
1123     });
1124     outerDiv.appendChild(copyButton);
1126     aParent.appendChild(outerDiv);
1127     return outerDiv;
1128   },
1130   processHistogram(aHgram) {
1131     const values = Object.keys(aHgram.values).map(k => aHgram.values[k]);
1132     if (!values.length) {
1133       // If we have no values collected for this histogram, just return
1134       // zero values so we still render it.
1135       return {
1136         values: [],
1137         pretty_average: 0,
1138         max: 0,
1139         sample_count: 0,
1140         sum: 0,
1141       };
1142     }
1144     const sample_count = values.reduceRight((a, b) => a + b);
1145     const average = Math.round((aHgram.sum * 10) / sample_count) / 10;
1146     const max_value = Math.max(...values);
1148     const labelledValues = Object.keys(aHgram.values).map(k => [
1149       Number(k),
1150       aHgram.values[k],
1151     ]);
1153     let result = {
1154       values: labelledValues,
1155       pretty_average: average,
1156       max: max_value,
1157       sample_count,
1158       sum: aHgram.sum,
1159     };
1161     return result;
1162   },
1164   /**
1165    * Return a non-negative, logarithmic representation of a non-negative number.
1166    * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
1167    *
1168    * @param aNumber Non-negative number
1169    */
1170   getLogValue(aNumber) {
1171     return Math.max(0, Math.log10(aNumber) + 1);
1172   },
1174   /**
1175    * Create histogram HTML bars, also returns a textual representation
1176    * Both aMaxValue and aSumValues must be positive.
1177    * Values are assumed to use 0 as baseline.
1178    *
1179    * @param aDiv Outer parent div
1180    * @param aHgram The histogram data
1181    * @param aOptions Object with render options (@see #render)
1182    */
1183   renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
1184     let text = "";
1185     // If the last label is not the longest string, alignment will break a little
1186     let labelPadTo = 0;
1187     if (aHgram.values.length) {
1188       labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
1189     }
1190     let maxBarValue = aOptions.exponential
1191       ? this.getLogValue(aHgram.max)
1192       : aHgram.max;
1194     for (let [label, value] of aHgram.values) {
1195       label = String(label);
1196       let barValue = aOptions.exponential ? this.getLogValue(value) : value;
1198       // Create a text representation: <right-aligned-label> |<bar-of-#><value>  <percentage>
1199       text +=
1200         EOL +
1201         " ".repeat(Math.max(0, labelPadTo - label.length)) +
1202         label + // Right-aligned label
1203         " |" +
1204         "#".repeat(Math.round((MAX_BAR_CHARS * barValue) / maxBarValue)) + // Bar
1205         "  " +
1206         value + // Value
1207         "  " +
1208         Math.round((100 * value) / aHgram.sample_count) +
1209         "%"; // Percentage
1211       // Construct the HTML labels + bars
1212       let belowEm =
1213         Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
1214       let aboveEm = MAX_BAR_HEIGHT - belowEm;
1216       let barDiv = document.createElement("div");
1217       barDiv.className = "bar";
1218       barDiv.style.paddingTop = aboveEm + "em";
1220       // Add value label or an nbsp if no value
1221       barDiv.appendChild(document.createTextNode(value ? value : "\u00A0"));
1223       // Create the blue bar
1224       let bar = document.createElement("div");
1225       bar.className = "bar-inner";
1226       bar.style.height = belowEm + "em";
1227       barDiv.appendChild(bar);
1229       // Add a special class to move the text down to prevent text overlap
1230       if (label.length > 3) {
1231         bar.classList.add("long-label");
1232       }
1233       // Add bucket label
1234       barDiv.appendChild(document.createTextNode(label));
1236       aDiv.appendChild(barDiv);
1237     }
1239     return text.substr(EOL.length); // Trim the EOL before the first line
1240   },
1243 var Search = {
1244   HASH_SEARCH: "search=",
1246   // A list of ids of sections that do not support search.
1247   blacklist: ["late-writes-section", "raw-payload-section"],
1249   // Pass if: all non-empty array items match (case-sensitive)
1250   isPassText(subject, filter) {
1251     for (let item of filter) {
1252       if (item.length && !subject.includes(item)) {
1253         return false; // mismatch and not a spurious space
1254       }
1255     }
1256     return true;
1257   },
1259   isPassRegex(subject, filter) {
1260     return filter.test(subject);
1261   },
1263   chooseFilter(filterText) {
1264     let filter = filterText.toString();
1265     // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
1266     let isPassFunc; // filter function, set once, then applied to all elements
1267     filter = filter.trim();
1268     if (filter[0] != "/") {
1269       // Plain text: case insensitive, AND if multi-string
1270       isPassFunc = this.isPassText;
1271       filter = filter.toLowerCase().split(" ");
1272     } else {
1273       isPassFunc = this.isPassRegex;
1274       var r = filter.match(/^\/(.*)\/(i?)$/);
1275       try {
1276         filter = RegExp(r[1], r[2]);
1277       } catch (e) {
1278         // Incomplete or bad RegExp - always no match
1279         isPassFunc = function () {
1280           return false;
1281         };
1282       }
1283     }
1284     return [isPassFunc, filter];
1285   },
1287   filterTextRows(table, filterText) {
1288     let [isPassFunc, filter] = this.chooseFilter(filterText);
1289     let allElementHidden = true;
1291     let needLowerCase = isPassFunc === this.isPassText;
1292     let elements = table.rows;
1293     for (let element of elements) {
1294       if (element.firstChild.nodeName == "th") {
1295         continue;
1296       }
1297       for (let cell of element.children) {
1298         let subject = needLowerCase
1299           ? cell.textContent.toLowerCase()
1300           : cell.textContent;
1301         element.hidden = !isPassFunc(subject, filter);
1302         if (!element.hidden) {
1303           if (allElementHidden) {
1304             allElementHidden = false;
1305           }
1306           // Don't need to check the rest of this row.
1307           break;
1308         }
1309       }
1310     }
1311     // Unhide the first row:
1312     if (!allElementHidden) {
1313       table.rows[0].hidden = false;
1314     }
1315     return allElementHidden;
1316   },
1318   filterElements(elements, filterText) {
1319     let [isPassFunc, filter] = this.chooseFilter(filterText);
1320     let allElementHidden = true;
1322     let needLowerCase = isPassFunc === this.isPassText;
1323     for (let element of elements) {
1324       let subject = needLowerCase ? element.id.toLowerCase() : element.id;
1325       element.hidden = !isPassFunc(subject, filter);
1326       if (allElementHidden && !element.hidden) {
1327         allElementHidden = false;
1328       }
1329     }
1330     return allElementHidden;
1331   },
1333   filterKeyedElements(keyedElements, filterText) {
1334     let [isPassFunc, filter] = this.chooseFilter(filterText);
1335     let allElementsHidden = true;
1337     let needLowerCase = isPassFunc === this.isPassText;
1338     keyedElements.forEach(keyedElement => {
1339       let subject = needLowerCase
1340         ? keyedElement.key.id.toLowerCase()
1341         : keyedElement.key.id;
1342       if (!isPassFunc(subject, filter)) {
1343         // If the keyedHistogram's name is not matched
1344         let allKeyedElementsHidden = true;
1345         for (let element of keyedElement.datas) {
1346           let subject = needLowerCase ? element.id.toLowerCase() : element.id;
1347           let match = isPassFunc(subject, filter);
1348           element.hidden = !match;
1349           if (match) {
1350             allKeyedElementsHidden = false;
1351           }
1352         }
1353         if (allElementsHidden && !allKeyedElementsHidden) {
1354           allElementsHidden = false;
1355         }
1356         keyedElement.key.hidden = allKeyedElementsHidden;
1357       } else {
1358         // If the keyedHistogram's name is matched
1359         allElementsHidden = false;
1360         keyedElement.key.hidden = false;
1361         for (let element of keyedElement.datas) {
1362           element.hidden = false;
1363         }
1364       }
1365     });
1366     return allElementsHidden;
1367   },
1369   searchHandler(e) {
1370     if (this.idleTimeout) {
1371       clearTimeout(this.idleTimeout);
1372     }
1373     this.idleTimeout = setTimeout(
1374       () => Search.search(e.target.value),
1375       FILTER_IDLE_TIMEOUT
1376     );
1377   },
1379   search(text, sectionParam = null) {
1380     let section = sectionParam;
1381     if (!section) {
1382       let sectionId = document
1383         .querySelector(".category.selected")
1384         .getAttribute("value");
1385       section = document.getElementById(sectionId);
1386     }
1387     if (Search.blacklist.includes(section.id)) {
1388       return false;
1389     }
1390     let noSearchResults = true;
1391     // In the home section, we search all other sections:
1392     if (section.id === "home-section") {
1393       return this.homeSearch(text);
1394     }
1396     if (section.id === "histograms-section") {
1397       let histograms = section.getElementsByClassName("histogram");
1398       noSearchResults = this.filterElements(histograms, text);
1399     } else if (section.id === "keyed-histograms-section") {
1400       let keyedElements = [];
1401       let keyedHistograms = section.getElementsByClassName("keyed-histogram");
1402       for (let key of keyedHistograms) {
1403         let datas = key.getElementsByClassName("histogram");
1404         keyedElements.push({ key, datas });
1405       }
1406       noSearchResults = this.filterKeyedElements(keyedElements, text);
1407     } else if (section.id === "keyed-scalars-section") {
1408       let keyedElements = [];
1409       let keyedScalars = section.getElementsByClassName("keyed-scalar");
1410       for (let key of keyedScalars) {
1411         let datas = key.querySelector("table").rows;
1412         keyedElements.push({ key, datas });
1413       }
1414       noSearchResults = this.filterKeyedElements(keyedElements, text);
1415     } else if (section.matches(".text-search")) {
1416       let tables = section.querySelectorAll("table");
1417       for (let table of tables) {
1418         // If we unhide anything, flip noSearchResults to
1419         // false so we don't show the "no results" bits.
1420         if (!this.filterTextRows(table, text)) {
1421           noSearchResults = false;
1422         }
1423       }
1424     } else if (section.querySelector(".sub-section")) {
1425       let keyedSubSections = [];
1426       let subsections = section.querySelectorAll(".sub-section");
1427       for (let section of subsections) {
1428         let datas = section.querySelector("table").rows;
1429         keyedSubSections.push({ key: section, datas });
1430       }
1431       noSearchResults = this.filterKeyedElements(keyedSubSections, text);
1432     } else {
1433       let tables = section.querySelectorAll("table");
1434       for (let table of tables) {
1435         noSearchResults = this.filterElements(table.rows, text);
1436         if (table.caption) {
1437           table.caption.hidden = noSearchResults;
1438         }
1439       }
1440     }
1442     changeUrlSearch(text);
1444     if (!sectionParam) {
1445       // If we are not searching in all section.
1446       this.updateNoResults(text, noSearchResults);
1447     }
1448     return noSearchResults;
1449   },
1451   updateNoResults(text, noSearchResults) {
1452     document
1453       .getElementById("no-search-results")
1454       .classList.toggle("hidden", !noSearchResults);
1455     if (noSearchResults) {
1456       let section = document.querySelector(".category.selected > span");
1457       let searchResultsText = document.getElementById("no-search-results-text");
1458       if (section.parentElement.id === "category-home") {
1459         document.l10n.setAttributes(
1460           searchResultsText,
1461           "about-telemetry-no-search-results-all",
1462           { searchTerms: text }
1463         );
1464       } else {
1465         let sectionName = section.textContent.trim();
1466         text === ""
1467           ? document.l10n.setAttributes(
1468               searchResultsText,
1469               "about-telemetry-no-data-to-display",
1470               { sectionName }
1471             )
1472           : document.l10n.setAttributes(
1473               searchResultsText,
1474               "about-telemetry-no-search-results",
1475               { sectionName, currentSearchText: text }
1476             );
1477       }
1478     }
1479   },
1481   resetHome() {
1482     document.getElementById("main").classList.remove("search");
1483     document.getElementById("no-search-results").classList.add("hidden");
1484     adjustHeaderState();
1485     Array.from(document.querySelectorAll("section")).forEach(section => {
1486       section.classList.toggle("active", section.id == "home-section");
1487     });
1488   },
1490   homeSearch(text) {
1491     changeUrlSearch(text);
1492     removeSearchSectionTitles();
1493     if (text === "") {
1494       this.resetHome();
1495       return;
1496     }
1497     document.getElementById("main").classList.add("search");
1498     adjustHeaderState(text);
1499     let noSearchResults = true;
1500     Array.from(document.querySelectorAll("section")).forEach(section => {
1501       if (section.id == "home-section" || section.id == "raw-payload-section") {
1502         section.classList.remove("active");
1503         return;
1504       }
1505       section.classList.add("active");
1506       let sectionHidden = this.search(text, section);
1507       if (!sectionHidden) {
1508         let sectionTitle = document.querySelector(
1509           `.category[value="${section.id}"] .category-name`
1510         ).textContent;
1511         let sectionDataDiv = document.querySelector(
1512           `#${section.id}.has-data.active .data`
1513         );
1514         let titleDiv = document.createElement("h1");
1515         titleDiv.classList.add("data", "search-section-title");
1516         titleDiv.textContent = sectionTitle;
1517         section.insertBefore(titleDiv, sectionDataDiv);
1518         noSearchResults = false;
1519       } else {
1520         // Hide all subsections if the section is hidden
1521         let subsections = section.querySelectorAll(".sub-section");
1522         for (let subsection of subsections) {
1523           subsection.hidden = true;
1524         }
1525       }
1526     });
1527     this.updateNoResults(text, noSearchResults);
1528   },
1532  * Helper function to render JS objects with white space between top level elements
1533  * so that they look better in the browser
1534  * @param   aObject JavaScript object or array to render
1535  * @return  String
1536  */
1537 function RenderObject(aObject) {
1538   let output = "";
1539   if (Array.isArray(aObject)) {
1540     if (!aObject.length) {
1541       return "[]";
1542     }
1543     output = "[" + JSON.stringify(aObject[0]);
1544     for (let i = 1; i < aObject.length; i++) {
1545       output += ", " + JSON.stringify(aObject[i]);
1546     }
1547     return output + "]";
1548   }
1549   let keys = Object.keys(aObject);
1550   if (!keys.length) {
1551     return "{}";
1552   }
1553   output = '{"' + keys[0] + '":\u00A0' + JSON.stringify(aObject[keys[0]]);
1554   for (let i = 1; i < keys.length; i++) {
1555     output += ', "' + keys[i] + '":\u00A0' + JSON.stringify(aObject[keys[i]]);
1556   }
1557   return output + "}";
1560 var GenericSubsection = {
1561   addSubSectionToSidebar(id, title) {
1562     let category = document.querySelector("#categories > [value=" + id + "]");
1563     category.classList.add("has-subsection");
1564     let subCategory = document.createElement("div");
1565     subCategory.classList.add("category-subsection");
1566     subCategory.setAttribute("value", id + "-" + title);
1567     subCategory.addEventListener("click", ev => {
1568       let section = ev.target;
1569       showSubSection(section);
1570     });
1571     subCategory.appendChild(document.createTextNode(title));
1572     category.appendChild(subCategory);
1573   },
1575   render(data, dataDiv, sectionID) {
1576     for (let [title, sectionData] of data) {
1577       let hasData = sectionData.size > 0;
1578       let s = this.renderSubsectionHeader(title, hasData, sectionID);
1579       s.appendChild(this.renderSubsectionData(title, sectionData));
1580       dataDiv.appendChild(s);
1581     }
1582   },
1584   renderSubsectionHeader(title, hasData, sectionID) {
1585     this.addSubSectionToSidebar(sectionID, title);
1586     let section = document.createElement("div");
1587     section.setAttribute("id", sectionID + "-" + title);
1588     section.classList.add("sub-section");
1589     if (hasData) {
1590       section.classList.add("has-subdata");
1591     }
1592     return section;
1593   },
1595   renderSubsectionData(title, data) {
1596     // Create data container
1597     let dataDiv = document.createElement("div");
1598     dataDiv.setAttribute("class", "subsection-data subdata");
1599     // Instanciate the data
1600     let table = GenericTable.render(data);
1601     let caption = document.createElement("caption");
1602     caption.textContent = title;
1603     table.appendChild(caption);
1604     dataDiv.appendChild(table);
1606     return dataDiv;
1607   },
1609   deleteAllSubSections() {
1610     let subsections = document.querySelectorAll(".category-subsection");
1611     subsections.forEach(el => {
1612       el.parentElement.removeChild(el);
1613     });
1614   },
1617 var GenericTable = {
1618   // Returns a table with key and value headers
1619   defaultHeadings() {
1620     return ["about-telemetry-keys-header", "about-telemetry-values-header"];
1621   },
1623   /**
1624    * Returns a n-column table.
1625    * @param rows An array of arrays, each containing data to render
1626    *             for one row.
1627    * @param headings The column header strings.
1628    */
1629   render(rows, headings = this.defaultHeadings()) {
1630     let table = document.createElement("table");
1631     this.renderHeader(table, headings);
1632     this.renderBody(table, rows);
1633     return table;
1634   },
1636   /**
1637    * Create the table header.
1638    * Tabs & newlines added to cells to make it easier to copy-paste.
1639    *
1640    * @param table Table element
1641    * @param headings Array of column header strings.
1642    */
1643   renderHeader(table, headings) {
1644     let headerRow = document.createElement("tr");
1645     table.appendChild(headerRow);
1647     for (let i = 0; i < headings.length; ++i) {
1648       let column = document.createElement("th");
1649       document.l10n.setAttributes(column, headings[i]);
1650       headerRow.appendChild(column);
1651     }
1652   },
1654   /**
1655    * Create the table body
1656    * Tabs & newlines added to cells to make it easier to copy-paste.
1657    *
1658    * @param table Table element
1659    * @param rows An array of arrays, each containing data to render
1660    *             for one row.
1661    */
1662   renderBody(table, rows) {
1663     for (let row of rows) {
1664       row = row.map(value => {
1665         // use .valueOf() to unbox Number, String, etc. objects
1666         if (
1667           value &&
1668           typeof value == "object" &&
1669           typeof value.valueOf() == "object"
1670         ) {
1671           return RenderObject(value);
1672         }
1673         return value;
1674       });
1676       let newRow = document.createElement("tr");
1677       newRow.id = row[0];
1678       table.appendChild(newRow);
1680       for (let i = 0; i < row.length; ++i) {
1681         let suffix = i == row.length - 1 ? "\n" : "\t";
1682         let field = document.createElement("td");
1683         field.appendChild(document.createTextNode(row[i] + suffix));
1684         newRow.appendChild(field);
1685       }
1686     }
1687   },
1690 var KeyedHistogram = {
1691   render(parent, id, keyedHistogram) {
1692     let outerDiv = document.createElement("div");
1693     outerDiv.className = "keyed-histogram";
1694     outerDiv.id = id;
1696     let divTitle = document.createElement("div");
1697     divTitle.classList.add("keyed-title");
1698     divTitle.appendChild(document.createTextNode(id));
1699     outerDiv.appendChild(divTitle);
1701     for (let [name, hgram] of Object.entries(keyedHistogram)) {
1702       Histogram.render(outerDiv, name, hgram);
1703     }
1705     parent.appendChild(outerDiv);
1706     return outerDiv;
1707   },
1710 var AddonDetails = {
1711   /**
1712    * Render the addon details section as a series of headers followed by key/value tables
1713    * @param aPing A ping object to render the data from.
1714    */
1715   render(aPing) {
1716     let addonSection = document.getElementById("addon-details");
1717     removeAllChildNodes(addonSection);
1718     let addonDetails = aPing.payload.addonDetails;
1719     const hasData = addonDetails && !!Object.keys(addonDetails).length;
1720     setHasData("addon-details-section", hasData);
1721     if (!hasData) {
1722       return;
1723     }
1725     for (let provider in addonDetails) {
1726       let providerSection = document.createElement("caption");
1727       document.l10n.setAttributes(
1728         providerSection,
1729         "about-telemetry-addon-provider",
1730         { addonProvider: provider }
1731       );
1732       let headingStrings = [
1733         "about-telemetry-addon-table-id",
1734         "about-telemetry-addon-table-details",
1735       ];
1736       let table = GenericTable.render(
1737         explodeObject(addonDetails[provider]),
1738         headingStrings
1739       );
1740       table.appendChild(providerSection);
1741       addonSection.appendChild(table);
1742     }
1743   },
1746 class Section {
1747   static renderContent(data, process, div, section) {
1748     if (data && Object.keys(data).length) {
1749       let s = GenericSubsection.renderSubsectionHeader(process, true, section);
1750       let heading = document.createElement("h2");
1751       document.l10n.setAttributes(heading, "about-telemetry-process", {
1752         process,
1753       });
1754       s.appendChild(heading);
1756       this.renderData(data, s);
1758       div.appendChild(s);
1759       let separator = document.createElement("div");
1760       separator.classList.add("clearfix");
1761       div.appendChild(separator);
1762     }
1763   }
1765   /**
1766    * Make parent process the first one, content process the second
1767    * then sort processes alphabetically
1768    */
1769   static processesComparator(a, b) {
1770     if (a === "parent" || (a === "content" && b !== "parent")) {
1771       return -1;
1772     } else if (b === "parent" || b === "content") {
1773       return 1;
1774     } else if (a < b) {
1775       return -1;
1776     } else if (a > b) {
1777       return 1;
1778     }
1779     return 0;
1780   }
1782   /**
1783    * Render sections
1784    */
1785   static renderSection(divName, section, aPayload) {
1786     let div = document.getElementById(divName);
1787     removeAllChildNodes(div);
1789     let data = {};
1790     let hasData = false;
1791     let selectedStore = getSelectedStore();
1793     let payload = aPayload.stores;
1795     let isCurrentPayload = !!payload;
1797     // Sort processes
1798     let sortedProcesses = isCurrentPayload
1799       ? Object.keys(payload[selectedStore]).sort(this.processesComparator)
1800       : Object.keys(aPayload.processes).sort(this.processesComparator);
1802     // Render content by process
1803     for (const process of sortedProcesses) {
1804       data = isCurrentPayload
1805         ? this.dataFiltering(payload, selectedStore, process)
1806         : this.archivePingDataFiltering(aPayload, process);
1807       hasData = hasData || !ObjectUtils.isEmpty(data);
1808       this.renderContent(data, process, div, section, this.renderData);
1809     }
1810     setHasData(section, hasData);
1811   }
1814 class Scalars extends Section {
1815   /**
1816    * Return data from the current ping
1817    */
1818   static dataFiltering(payload, selectedStore, process) {
1819     return payload[selectedStore][process].scalars;
1820   }
1822   /**
1823    * Return data from an archived ping
1824    */
1825   static archivePingDataFiltering(payload, process) {
1826     return payload.processes[process].scalars;
1827   }
1829   static renderData(data, div) {
1830     const scalarsHeadings = [
1831       "about-telemetry-names-header",
1832       "about-telemetry-values-header",
1833     ];
1834     let scalarsTable = GenericTable.render(
1835       explodeObject(data),
1836       scalarsHeadings
1837     );
1838     div.appendChild(scalarsTable);
1839   }
1841   /**
1842    * Render the scalar data - if present - from the payload in a simple key-value table.
1843    * @param aPayload A payload object to render the data from.
1844    */
1845   static render(aPayload) {
1846     const divName = "scalars";
1847     const section = "scalars-section";
1848     this.renderSection(divName, section, aPayload);
1849   }
1852 class KeyedScalars extends Section {
1853   /**
1854    * Return data from the current ping
1855    */
1856   static dataFiltering(payload, selectedStore, process) {
1857     return payload[selectedStore][process].keyedScalars;
1858   }
1860   /**
1861    * Return data from an archived ping
1862    */
1863   static archivePingDataFiltering(payload, process) {
1864     return payload.processes[process].keyedScalars;
1865   }
1867   static renderData(data, div) {
1868     const scalarsHeadings = [
1869       "about-telemetry-names-header",
1870       "about-telemetry-values-header",
1871     ];
1872     for (let scalarId in data) {
1873       // Add the name of the scalar.
1874       let container = document.createElement("div");
1875       container.classList.add("keyed-scalar");
1876       container.id = scalarId;
1877       let scalarNameSection = document.createElement("p");
1878       scalarNameSection.classList.add("keyed-title");
1879       scalarNameSection.appendChild(document.createTextNode(scalarId));
1880       container.appendChild(scalarNameSection);
1881       // Populate the section with the key-value pairs from the scalar.
1882       const table = GenericTable.render(
1883         explodeObject(data[scalarId]),
1884         scalarsHeadings
1885       );
1886       container.appendChild(table);
1887       div.appendChild(container);
1888     }
1889   }
1891   /**
1892    * Render the keyed scalar data - if present - from the payload in a simple key-value table.
1893    * @param aPayload A payload object to render the data from.
1894    */
1895   static render(aPayload) {
1896     const divName = "keyed-scalars";
1897     const section = "keyed-scalars-section";
1898     this.renderSection(divName, section, aPayload);
1899   }
1902 var Events = {
1903   /**
1904    * Render the event data - if present - from the payload in a simple table.
1905    * @param aPayload A payload object to render the data from.
1906    */
1907   render(aPayload) {
1908     let eventsDiv = document.getElementById("events");
1909     removeAllChildNodes(eventsDiv);
1910     const headings = [
1911       "about-telemetry-time-stamp-header",
1912       "about-telemetry-category-header",
1913       "about-telemetry-method-header",
1914       "about-telemetry-object-header",
1915       "about-telemetry-values-header",
1916       "about-telemetry-extra-header",
1917     ];
1918     let payload = aPayload.processes;
1919     let hasData = false;
1920     if (payload) {
1921       for (const process of Object.keys(aPayload.processes)) {
1922         let data = aPayload.processes[process].events;
1923         if (data && Object.keys(data).length) {
1924           hasData = true;
1925           let s = GenericSubsection.renderSubsectionHeader(
1926             process,
1927             true,
1928             "events-section"
1929           );
1930           let heading = document.createElement("h2");
1931           heading.textContent = process;
1932           s.appendChild(heading);
1933           const table = GenericTable.render(data, headings);
1934           s.appendChild(table);
1935           eventsDiv.appendChild(s);
1936           let separator = document.createElement("div");
1937           separator.classList.add("clearfix");
1938           eventsDiv.appendChild(separator);
1939         }
1940       }
1941     } else {
1942       // handle archived ping
1943       for (const process of Object.keys(aPayload.events)) {
1944         let data = process;
1945         if (data && Object.keys(data).length) {
1946           hasData = true;
1947           let s = GenericSubsection.renderSubsectionHeader(
1948             process,
1949             true,
1950             "events-section"
1951           );
1952           let heading = document.createElement("h2");
1953           heading.textContent = process;
1954           s.appendChild(heading);
1955           const table = GenericTable.render(data, headings);
1956           eventsDiv.appendChild(table);
1957           let separator = document.createElement("div");
1958           separator.classList.add("clearfix");
1959           eventsDiv.appendChild(separator);
1960         }
1961       }
1962     }
1963     setHasData("events-section", hasData);
1964   },
1968  * Helper function for showing either the toggle element or "No data collected" message for a section
1970  * @param aSectionID ID of the section element that needs to be changed
1971  * @param aHasData true (default) indicates that toggle should be displayed
1972  */
1973 function setHasData(aSectionID, aHasData) {
1974   let sectionElement = document.getElementById(aSectionID);
1975   sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
1977   // Display or Hide the section in the sidebar
1978   let sectionCategory = document.querySelector(
1979     ".category[value=" + aSectionID + "]"
1980   );
1981   sectionCategory.classList[aHasData ? "add" : "remove"]("has-data");
1985  * Sets l10n attributes based on the Telemetry Server Owner pref.
1986  */
1987 function setupServerOwnerBranding() {
1988   let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
1989   const elements = [
1990     [document.getElementById("page-subtitle"), "about-telemetry-page-subtitle"],
1991   ];
1992   for (const [elt, l10nName] of elements) {
1993     document.l10n.setAttributes(elt, l10nName, {
1994       telemetryServerOwner: serverOwner,
1995     });
1996   }
2000  * Display the store selector if we are on one
2001  * of the whitelisted sections
2002  */
2003 function displayStoresSelector(selectedSection) {
2004   let whitelist = [
2005     "scalars-section",
2006     "keyed-scalars-section",
2007     "histograms-section",
2008     "keyed-histograms-section",
2009   ];
2010   let stores = document.getElementById("stores");
2011   stores.hidden = !whitelist.includes(selectedSection);
2012   let storesLabel = document.getElementById("storesLabel");
2013   storesLabel.hidden = !whitelist.includes(selectedSection);
2016 function refreshSearch() {
2017   removeSearchSectionTitles();
2018   let selectedSection = document
2019     .querySelector(".category.selected")
2020     .getAttribute("value");
2021   let search = document.getElementById("search");
2022   if (!Search.blacklist.includes(selectedSection)) {
2023     Search.search(search.value);
2024   }
2027 function adjustSearchState() {
2028   removeSearchSectionTitles();
2029   let selectedSection = document
2030     .querySelector(".category.selected")
2031     .getAttribute("value");
2032   let search = document.getElementById("search");
2033   search.value = "";
2034   search.hidden = Search.blacklist.includes(selectedSection);
2035   document.getElementById("no-search-results").classList.add("hidden");
2036   Search.search(""); // reinitialize search state.
2039 function removeSearchSectionTitles() {
2040   for (let sectionTitleDiv of Array.from(
2041     document.getElementsByClassName("search-section-title")
2042   )) {
2043     sectionTitleDiv.remove();
2044   }
2047 function adjustSection() {
2048   let selectedCategory = document.querySelector(".category.selected");
2049   if (!selectedCategory.classList.contains("has-data")) {
2050     PingPicker._showStructuredPingData();
2051   }
2054 function adjustHeaderState(title = null) {
2055   let selected = document.querySelector(".category.selected .category-name");
2056   let selectedTitle = selected.textContent.trim();
2057   let sectionTitle = document.getElementById("sectionTitle");
2058   if (title !== null) {
2059     document.l10n.setAttributes(
2060       sectionTitle,
2061       "about-telemetry-results-for-search",
2062       { searchTerms: title }
2063     );
2064   } else {
2065     sectionTitle.textContent = selectedTitle;
2066   }
2067   let search = document.getElementById("search");
2068   if (selected.parentElement.id === "category-home") {
2069     document.l10n.setAttributes(
2070       search,
2071       "about-telemetry-filter-all-placeholder"
2072     );
2073   } else {
2074     document.l10n.setAttributes(search, "about-telemetry-filter-placeholder", {
2075       selectedTitle,
2076     });
2077   }
2081  * Change the url according to the current section displayed
2082  * e.g about:telemetry#general-data
2083  */
2084 function changeUrlPath(selectedSection, subSection) {
2085   if (subSection) {
2086     let hash = window.location.hash.split("_")[0] + "_" + selectedSection;
2087     window.location.hash = hash;
2088   } else {
2089     window.location.hash = selectedSection.replace("-section", "-tab");
2090   }
2094  * Change the url according to the current search text
2095  */
2096 function changeUrlSearch(searchText) {
2097   let currentHash = window.location.hash;
2098   let hashWithoutSearch = currentHash.split(Search.HASH_SEARCH)[0];
2099   let hash = "";
2101   if (!currentHash && !searchText) {
2102     return;
2103   }
2104   if (!currentHash.includes(Search.HASH_SEARCH) && hashWithoutSearch) {
2105     hashWithoutSearch += "_";
2106   }
2107   if (searchText) {
2108     hash =
2109       hashWithoutSearch + Search.HASH_SEARCH + searchText.replace(/ /g, "+");
2110   } else if (hashWithoutSearch) {
2111     hash = hashWithoutSearch.slice(0, hashWithoutSearch.length - 1);
2112   }
2114   window.location.hash = hash;
2118  * Change the section displayed
2119  */
2120 function show(selected) {
2121   let selectedValue = selected.getAttribute("value");
2122   if (selectedValue === "raw-json-viewer") {
2123     openJsonInFirefoxJsonViewer(JSON.stringify(gPingData, null, 2));
2124     return;
2125   }
2127   let selected_section = document.getElementById(selectedValue);
2128   let subsections = selected_section.querySelectorAll(".sub-section");
2129   if (selected.classList.contains("has-subsection")) {
2130     for (let subsection of selected.children) {
2131       subsection.classList.remove("selected");
2132     }
2133   }
2134   if (subsections) {
2135     for (let subsection of subsections) {
2136       subsection.hidden = false;
2137     }
2138   }
2140   let current_button = document.querySelector(".category.selected");
2141   if (current_button == selected) {
2142     return;
2143   }
2144   current_button.classList.remove("selected");
2145   selected.classList.add("selected");
2147   document.querySelectorAll("section").forEach(section => {
2148     section.classList.remove("active");
2149   });
2150   selected_section.classList.add("active");
2152   adjustHeaderState();
2153   displayStoresSelector(selectedValue);
2154   adjustSearchState();
2155   changeUrlPath(selectedValue);
2158 function showSubSection(selected) {
2159   if (!selected) {
2160     return;
2161   }
2162   let current_selection = document.querySelector(
2163     ".category-subsection.selected"
2164   );
2165   if (current_selection) {
2166     current_selection.classList.remove("selected");
2167   }
2168   selected.classList.add("selected");
2170   let section = document.getElementById(selected.getAttribute("value"));
2171   section.parentElement.childNodes.forEach(element => {
2172     element.hidden = true;
2173   });
2174   section.hidden = false;
2176   let title =
2177     selected.parentElement.querySelector(".category-name").textContent;
2178   let subsection = selected.textContent;
2179   document.getElementById("sectionTitle").textContent =
2180     title + " - " + subsection;
2181   changeUrlPath(subsection, true);
2185  * Initializes load/unload, pref change and mouse-click listeners
2186  */
2187 function setupListeners() {
2188   Settings.attachObservers();
2189   PingPicker.attachObservers();
2190   RawPayloadData.attachObservers();
2192   let menu = document.getElementById("categories");
2193   menu.addEventListener("click", e => {
2194     if (e.target && e.target.parentNode == menu) {
2195       show(e.target);
2196     }
2197   });
2199   let search = document.getElementById("search");
2200   search.addEventListener("input", Search.searchHandler);
2202   document
2203     .getElementById("late-writes-fetch-symbols")
2204     .addEventListener("click", function () {
2205       if (!gPingData) {
2206         return;
2207       }
2209       let lateWrites = gPingData.payload.lateWrites;
2210       let req = new SymbolicationRequest(
2211         "late-writes",
2212         LateWritesSingleton.renderHeader,
2213         lateWrites.memoryMap,
2214         lateWrites.stacks
2215       );
2216       req.fetchSymbols();
2217     });
2219   document
2220     .getElementById("late-writes-hide-symbols")
2221     .addEventListener("click", function () {
2222       if (!gPingData) {
2223         return;
2224       }
2226       LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
2227     });
2230 // Restores the sections states
2231 function urlSectionRestore(hash) {
2232   if (hash) {
2233     let section = hash.replace("-tab", "-section");
2234     let subsection = section.split("_")[1];
2235     section = section.split("_")[0];
2236     let category = document.querySelector(".category[value=" + section + "]");
2237     if (category) {
2238       show(category);
2239       if (subsection) {
2240         let selector =
2241           ".category-subsection[value=" + section + "-" + subsection + "]";
2242         let subcategory = document.querySelector(selector);
2243         showSubSection(subcategory);
2244       }
2245     }
2246   }
2249 // Restore sections states and search terms
2250 function urlStateRestore() {
2251   let hash = window.location.hash;
2252   let searchQuery = "";
2253   if (hash) {
2254     hash = hash.slice(1);
2255     if (hash.includes(Search.HASH_SEARCH)) {
2256       searchQuery = hash.split(Search.HASH_SEARCH)[1].replace(/[+]/g, " ");
2257       hash = hash.split(Search.HASH_SEARCH)[0];
2258     }
2259     urlSectionRestore(hash);
2260   }
2261   if (searchQuery) {
2262     let search = document.getElementById("search");
2263     search.value = searchQuery;
2264   }
2267 function openJsonInFirefoxJsonViewer(json) {
2268   json = unescape(encodeURIComponent(json));
2269   try {
2270     window.open("data:application/json;base64," + btoa(json));
2271   } catch (e) {
2272     show(document.querySelector(".category[value=raw-payload-section]"));
2273   }
2276 function onLoad() {
2277   window.removeEventListener("load", onLoad);
2278   // Set the text in the page header and elsewhere that needs the server owner.
2279   setupServerOwnerBranding();
2281   // Set up event listeners
2282   setupListeners();
2284   // Render settings.
2285   Settings.render();
2287   adjustHeaderState();
2289   urlStateRestore();
2291   // Update ping data when async Telemetry init is finished.
2292   Telemetry.asyncFetchTelemetryData(async () => {
2293     await PingPicker.update();
2294   });
2297 var LateWritesSingleton = {
2298   renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
2299     StackRenderer.renderHeader(
2300       "late-writes",
2301       "about-telemetry-late-writes-title",
2302       { lateWriteCount: aIndex + 1 }
2303     );
2304   },
2306   renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
2307     let hasData = !!(
2308       lateWrites &&
2309       lateWrites.stacks &&
2310       lateWrites.stacks.length
2311     );
2312     setHasData("late-writes-section", hasData);
2313     if (!hasData) {
2314       return;
2315     }
2317     let stacks = lateWrites.stacks;
2318     let memoryMap = lateWrites.memoryMap;
2319     StackRenderer.renderStacks(
2320       "late-writes",
2321       stacks,
2322       memoryMap,
2323       LateWritesSingleton.renderHeader
2324     );
2325   },
2328 class HistogramSection extends Section {
2329   /**
2330    * Return data from the current ping
2331    */
2332   static dataFiltering(payload, selectedStore, process) {
2333     return payload[selectedStore][process].histograms;
2334   }
2336   /**
2337    * Return data from an archived ping
2338    */
2339   static archivePingDataFiltering(payload, process) {
2340     if (process === "parent") {
2341       return payload.histograms;
2342     }
2343     return payload.processes[process].histograms;
2344   }
2346   static renderData(data, div) {
2347     for (let [hName, hgram] of Object.entries(data)) {
2348       Histogram.render(div, hName, hgram, { unpacked: true });
2349     }
2350   }
2352   static render(aPayload) {
2353     const divName = "histograms";
2354     const section = "histograms-section";
2355     this.renderSection(divName, section, aPayload);
2356   }
2359 class KeyedHistogramSection extends Section {
2360   /**
2361    * Return data from the current ping
2362    */
2363   static dataFiltering(payload, selectedStore, process) {
2364     return payload[selectedStore][process].keyedHistograms;
2365   }
2367   /**
2368    * Return data from an archived ping
2369    */
2370   static archivePingDataFiltering(payload, process) {
2371     if (process === "parent") {
2372       return payload.keyedHistograms;
2373     }
2374     return payload.processes[process].keyedHistograms;
2375   }
2377   static renderData(data, div) {
2378     for (let [id, keyed] of Object.entries(data)) {
2379       KeyedHistogram.render(div, id, keyed, { unpacked: true });
2380     }
2381   }
2383   static render(aPayload) {
2384     const divName = "keyed-histograms";
2385     const section = "keyed-histograms-section";
2386     this.renderSection(divName, section, aPayload);
2387   }
2390 var SessionInformation = {
2391   render(aPayload) {
2392     let infoSection = document.getElementById("session-info");
2393     removeAllChildNodes(infoSection);
2395     let hasData = !!Object.keys(aPayload.info).length;
2396     setHasData("session-info-section", hasData);
2398     if (hasData) {
2399       const table = GenericTable.render(explodeObject(aPayload.info));
2400       infoSection.appendChild(table);
2401     }
2402   },
2405 var SimpleMeasurements = {
2406   render(aPayload) {
2407     let simpleSection = document.getElementById("simple-measurements");
2408     removeAllChildNodes(simpleSection);
2410     let simpleMeasurements = this.sortStartupMilestones(
2411       aPayload.simpleMeasurements
2412     );
2413     let hasData = !!Object.keys(simpleMeasurements).length;
2414     setHasData("simple-measurements-section", hasData);
2416     if (hasData) {
2417       const table = GenericTable.render(explodeObject(simpleMeasurements));
2418       simpleSection.appendChild(table);
2419     }
2420   },
2422   /**
2423    * Helper function for sorting the startup milestones in the Simple Measurements
2424    * section into temporal order.
2425    *
2426    * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
2427    * @return Sorted measurements
2428    */
2429   sortStartupMilestones(aSimpleMeasurements) {
2430     const telemetryTimestamps = TelemetryTimestamps.get();
2431     let startupEvents = Services.startup.getStartupInfo();
2432     delete startupEvents.process;
2434     function keyIsMilestone(k) {
2435       return k in startupEvents || k in telemetryTimestamps;
2436     }
2438     let sortedKeys = Object.keys(aSimpleMeasurements);
2440     // Sort the measurements, with startup milestones at the front + ordered by time
2441     sortedKeys.sort(function keyCompare(keyA, keyB) {
2442       let isKeyAMilestone = keyIsMilestone(keyA);
2443       let isKeyBMilestone = keyIsMilestone(keyB);
2445       // First order by startup vs non-startup measurement
2446       if (isKeyAMilestone && !isKeyBMilestone) {
2447         return -1;
2448       }
2449       if (!isKeyAMilestone && isKeyBMilestone) {
2450         return 1;
2451       }
2452       // Don't change order of non-startup measurements
2453       if (!isKeyAMilestone && !isKeyBMilestone) {
2454         return 0;
2455       }
2457       // If both keys are startup measurements, order them by value
2458       return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
2459     });
2461     // Insert measurements into a result object in sort-order
2462     let result = {};
2463     for (let key of sortedKeys) {
2464       result[key] = aSimpleMeasurements[key];
2465     }
2467     return result;
2468   },
2472  * Render stores options
2473  */
2474 function renderStoreList(payload) {
2475   let storeSelect = document.getElementById("stores");
2476   let storesLabel = document.getElementById("storesLabel");
2477   removeAllChildNodes(storeSelect);
2479   if (!("stores" in payload)) {
2480     storeSelect.classList.add("hidden");
2481     storesLabel.classList.add("hidden");
2482     return;
2483   }
2485   storeSelect.classList.remove("hidden");
2486   storesLabel.classList.remove("hidden");
2487   storeSelect.disabled = false;
2489   for (let store of Object.keys(payload.stores)) {
2490     let option = document.createElement("option");
2491     option.appendChild(document.createTextNode(store));
2492     option.setAttribute("value", store);
2493     // Select main store by default
2494     if (store === "main") {
2495       option.selected = true;
2496     }
2497     storeSelect.appendChild(option);
2498   }
2502  * Return the selected store
2503  */
2504 function getSelectedStore() {
2505   let storeSelect = document.getElementById("stores");
2506   let storeSelectedOption = storeSelect.selectedOptions.item(0);
2507   let selectedStore =
2508     storeSelectedOption !== null
2509       ? storeSelectedOption.getAttribute("value")
2510       : undefined;
2511   return selectedStore;
2514 function togglePingSections(isMainPing) {
2515   // We always show the sections that are "common" to all pings.
2516   let commonSections = new Set([
2517     "heading",
2518     "home-section",
2519     "general-data-section",
2520     "environment-data-section",
2521     "raw-json-viewer",
2522   ]);
2524   let elements = document.querySelectorAll(".category");
2525   for (let section of elements) {
2526     if (commonSections.has(section.getAttribute("value"))) {
2527       continue;
2528     }
2529     // Only show the raw payload for non main ping.
2530     if (section.getAttribute("value") == "raw-payload-section") {
2531       section.classList.toggle("has-data", !isMainPing);
2532     } else {
2533       section.classList.toggle("has-data", isMainPing);
2534     }
2535   }
2538 function displayPingData(ping, updatePayloadList = false) {
2539   gPingData = ping;
2540   try {
2541     PingPicker.render();
2542     displayRichPingData(ping, updatePayloadList);
2543     adjustSection();
2544     refreshSearch();
2545   } catch (err) {
2546     console.log(err);
2547     PingPicker._showRawPingData();
2548   }
2551 function displayRichPingData(ping, updatePayloadList) {
2552   // Update the payload list and store lists
2553   if (updatePayloadList) {
2554     renderStoreList(ping.payload);
2555   }
2557   // Show general data.
2558   GeneralData.render(ping);
2560   // Show environment data.
2561   EnvironmentData.render(ping);
2563   RawPayloadData.render(ping);
2565   // We have special rendering code for the payloads from "main" and "event" pings.
2566   // For any other pings we just render the raw JSON payload.
2567   let isMainPing = ping.type == "main" || ping.type == "saved-session";
2568   let isEventPing = ping.type == "event";
2569   togglePingSections(isMainPing);
2571   if (isEventPing) {
2572     // Copy the payload, so we don't modify the raw representation
2573     // Ensure we always have at least the parent process.
2574     let payload = { processes: { parent: {} } };
2575     for (let process of Object.keys(ping.payload.events)) {
2576       payload.processes[process] = {
2577         events: ping.payload.events[process],
2578       };
2579     }
2581     // We transformed the actual payload, let's reload the store list if necessary.
2582     if (updatePayloadList) {
2583       renderStoreList(payload);
2584     }
2586     // Show event data.
2587     Events.render(payload);
2588     return;
2589   }
2591   if (!isMainPing) {
2592     return;
2593   }
2595   // Show slow SQL stats
2596   SlowSQL.render(ping);
2598   // Render Addon details.
2599   AddonDetails.render(ping);
2601   let payload = ping.payload;
2602   // Show basic session info gathered
2603   SessionInformation.render(payload);
2605   // Show scalar data.
2606   Scalars.render(payload);
2607   KeyedScalars.render(payload);
2609   // Show histogram data
2610   HistogramSection.render(payload);
2612   // Show keyed histogram data
2613   KeyedHistogramSection.render(payload);
2615   // Show event data.
2616   Events.render(payload);
2618   LateWritesSingleton.renderLateWrites(payload.lateWrites);
2620   // Show simple measurements
2621   SimpleMeasurements.render(payload);
2624 window.addEventListener("load", onLoad);