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/. */
7 const { BrowserUtils } = ChromeUtils.importESModule(
8 "resource://gre/modules/BrowserUtils.sys.mjs"
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",
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.
53 // Cached value of document's RTL mode
54 var documentRTLMode = "";
57 * Helper function for determining whether the document direction is RTL.
58 * Caches result of check on first invocation.
61 if (!documentRTLMode) {
62 documentRTLMode = window.getComputedStyle(document.body).direction;
64 return documentRTLMode == "rtl";
67 function isFlatArray(obj) {
68 if (!Array.isArray(obj)) {
71 return !obj.some(e => typeof e == "object");
75 * This is a helper function for explodeObject.
77 function flattenObject(obj, map, path, array) {
78 for (let k of Object.keys(obj)) {
79 let newPath = [...path, array ? "[" + k + "]" : 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(", ") + "]");
86 flattenObject(v, map, newPath, Array.isArray(v));
92 * This turns a JSON object into a "flat" stringified form.
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"]).
97 function explodeObject(obj) {
99 flattenObject(obj, map, []);
103 function filterObject(obj, filterOut) {
105 for (let k of Object.keys(obj)) {
106 if (!filterOut.includes(k)) {
114 * This turns a JSON object into a "flat" stringified form, separated into top-level sections.
116 * For an object like:
119 * c: {d: "2", e: {f: "3"}}
121 * it returns a Map of the form:
123 * ["a", Map(["b","1"])],
124 * ["c", Map([["d", "2"], ["e.f", "3"]])]
127 function sectionalizeObject(obj) {
129 for (let k of Object.keys(obj)) {
130 map.set(k, explodeObject(obj[k]));
136 * Obtain the main DOMWindow for the current context.
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
149 * This may return null if we can't find the browser chrome window.
151 function getMainWindowWithPreferencesPane() {
152 let mainWindow = getMainWindow();
153 if (mainWindow && "openPreferences" in mainWindow) {
160 * Remove all child nodes of a document node.
162 function removeAllChildNodes(node) {
163 while (node.hasChildNodes()) {
164 node.removeChild(node.lastChild);
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"
178 EventDispatcher.instance.sendRequest({
179 type: "Settings:Show",
180 resource: "preferences_privacy",
183 // Show the data choices preferences on desktop.
184 let mainWindow = getMainWindowWithPreferencesPane();
185 mainWindow.openPreferences("privacy-reports");
193 * Updates the button & text at the top of the page to reflect Telemetry state.
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(
204 "about-telemetry-settings-explanation",
205 { channel, uploadcase }
208 this.attachObservers();
213 viewCurrentPingData: null,
214 _archivedPings: null,
218 let pingSourceElements = document.getElementsByName("choose-ping-source");
219 for (let el of pingSourceElements) {
220 el.addEventListener("change", () => this.onPingSourceChanged());
223 let displays = document.getElementsByName("choose-ping-display");
224 for (let el of displays) {
225 el.addEventListener("change", () => this.onPingDisplayChanged());
229 .getElementById("show-subsession-data")
230 .addEventListener("change", () => {
231 this._updateCurrentPingData();
234 document.getElementById("choose-ping-id").addEventListener("change", () => {
235 this._updateArchivedPingData();
238 .getElementById("choose-ping-type")
239 .addEventListener("change", () => {
240 this.filterDisplayedPings();
244 .getElementById("newer-ping")
245 .addEventListener("click", () => this._movePingIndex(-1));
247 .getElementById("older-ping")
248 .addEventListener("click", () => this._movePingIndex(1));
250 let pingPickerNeedHide = false;
251 let pingPicker = document.getElementById("ping-picker");
252 pingPicker.addEventListener(
254 () => (pingPickerNeedHide = false)
256 pingPicker.addEventListener(
258 () => (pingPickerNeedHide = true)
260 document.addEventListener("click", ev => {
261 if (pingPickerNeedHide) {
262 pingPicker.classList.add("hidden");
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");
273 pingPicker.classList.remove("hidden");
274 event.stopPropagation();
280 onPingSourceChanged() {
284 onPingDisplayChanged() {
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(
305 "about-telemetry-ping-details",
306 { timestamp: pingTypeText, name: pingName }
309 // Change sidebar heading text.
310 controls.classList.add("hidden");
311 document.l10n.setAttributes(
313 "about-telemetry-current-data-sidebar"
315 // Change home page text.
316 document.l10n.setAttributes(
318 "about-telemetry-data-details-current"
322 GenericSubsection.deleteAllSubSections();
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"
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();
347 document.getElementById("current-ping-picker").hidden = true;
348 await this._updateArchivedPingList(archivedPingList);
349 document.getElementById("archived-ping-picker").hidden = false;
354 _updateCurrentPingData() {
355 const subsession = document.getElementById("show-subsession-data").checked;
356 let ping = TelemetryController.getCurrentPingData(subsession);
361 let stores = Telemetry.getAllStores();
363 histograms: Telemetry.getSnapshotForHistograms,
364 keyedHistograms: Telemetry.getSnapshotForKeyedHistograms,
365 scalars: Telemetry.getSnapshotForScalars,
366 keyedScalars: Telemetry.getSnapshotForKeyedScalars,
370 for (const [name, fn] of Object.entries(getData)) {
371 for (const store of stores) {
375 let measurement = fn(store, /* clear */ false, /* filterTest */ true);
376 let processes = Object.keys(measurement);
378 for (const process of processes) {
379 if (!data[store][process]) {
380 data[store][process] = {};
383 data[store][process][name] = measurement[process];
387 ping.payload.stores = data;
389 // Delete the unused data from the payload of the current ping.
390 // It's included in the above `stores` attribute.
391 for (const data of Object.values(ping.payload.processes)) {
393 delete data.keyedScalars;
394 delete data.histograms;
395 delete data.keyedHistograms;
397 delete ping.payload.histograms;
398 delete ping.payload.keyedHistograms;
400 // augment ping payload with event telemetry
401 let eventSnapshot = Telemetry.snapshotEvents(
402 Telemetry.DATASET_PRERELEASE_CHANNELS,
405 for (let process of Object.keys(eventSnapshot)) {
406 if (process in ping.payload.processes) {
407 ping.payload.processes[process].events = eventSnapshot[process].filter(
408 e => !e[1].startsWith("telemetry.test")
413 displayPingData(ping, true);
416 _updateArchivedPingData() {
417 let id = this._getSelectedPingId();
418 let res = Promise.resolve();
420 res = TelemetryArchive.promiseArchivedPingById(id).then(ping =>
421 displayPingData(ping, true)
427 async _updateArchivedPingList(pingList) {
428 // The archived ping list is sorted in ascending timestamp order,
429 // but descending is more practical for the operations we do here.
431 this._archivedPings = pingList;
432 // Render the archive data.
433 this._renderPingList();
434 // Update the displayed ping.
435 await this._updateArchivedPingData();
439 let pingSelector = document.getElementById("choose-ping-id");
440 Array.from(pingSelector.children).forEach(child =>
441 removeAllChildNodes(child)
444 let pingTypes = new Set();
445 pingTypes.add(this.TYPE_ALL);
447 const today = new Date();
448 today.setHours(0, 0, 0, 0);
449 const yesterday = new Date(today);
450 yesterday.setDate(today.getDate() - 1);
452 for (let p of this._archivedPings) {
453 pingTypes.add(p.type);
454 const pingDate = new Date(p.timestampCreated);
455 const datetimeText = new Services.intl.DateTimeFormat(undefined, {
459 const pingName = `${datetimeText}, ${p.type}`;
461 let option = document.createElement("option");
462 let content = document.createTextNode(pingName);
463 option.appendChild(content);
464 option.setAttribute("value", p.id);
465 option.dataset.type = p.type;
466 option.dataset.date = datetimeText;
468 pingDate.setHours(0, 0, 0, 0);
469 if (pingDate.getTime() === today.getTime()) {
470 pingSelector.children[0].appendChild(option);
471 } else if (pingDate.getTime() === yesterday.getTime()) {
472 pingSelector.children[1].appendChild(option);
474 pingSelector.children[2].appendChild(option);
477 this._renderPingTypes(pingTypes);
480 _renderPingTypes(pingTypes) {
481 let pingTypeSelector = document.getElementById("choose-ping-type");
482 removeAllChildNodes(pingTypeSelector);
483 pingTypes.forEach(type => {
484 let option = document.createElement("option");
485 option.appendChild(document.createTextNode(type));
486 option.setAttribute("value", type);
487 pingTypeSelector.appendChild(option);
491 _movePingIndex(offset) {
492 if (this.viewCurrentPingData) {
495 let typeSelector = document.getElementById("choose-ping-type");
496 let type = typeSelector.selectedOptions.item(0).value;
498 let id = this._getSelectedPingId();
499 let index = this._archivedPings.findIndex(p => p.id == id);
500 let newIndex = Math.min(
501 Math.max(0, index + offset),
502 this._archivedPings.length - 1
507 pingList = this._archivedPings.slice(newIndex);
509 pingList = this._archivedPings.slice(0, newIndex);
513 let ping = pingList.find(p => {
514 return type == this.TYPE_ALL || p.type == type;
518 this.selectPing(ping);
519 this._updateArchivedPingData();
524 let pingSelector = document.getElementById("choose-ping-id");
525 // Use some() to break if we find the ping.
526 Array.from(pingSelector.children).some(group => {
527 return Array.from(group.children).some(option => {
528 if (option.value == ping.id) {
529 option.selected = true;
537 filterDisplayedPings() {
538 let pingSelector = document.getElementById("choose-ping-id");
539 let typeSelector = document.getElementById("choose-ping-type");
540 let type = typeSelector.selectedOptions.item(0).value;
542 Array.from(pingSelector.children).forEach(group => {
543 Array.from(group.children).forEach(option => {
544 if (first && option.dataset.type == type) {
545 option.selected = true;
548 option.hidden = type != this.TYPE_ALL && option.dataset.type != type;
549 // Arrow keys should only iterate over visible options
550 option.disabled = option.hidden;
553 this._updateArchivedPingData();
556 _getSelectedPingName() {
557 let pingSelector = document.getElementById("choose-ping-id");
558 let selected = pingSelector.selectedOptions.item(0);
559 return selected.dataset.date;
562 _getSelectedPingType() {
563 let pingSelector = document.getElementById("choose-ping-id");
564 let selected = pingSelector.selectedOptions.item(0);
565 return selected.dataset.type;
568 _getSelectedPingId() {
569 let pingSelector = document.getElementById("choose-ping-id");
570 let selected = pingSelector.selectedOptions.item(0);
571 return selected.getAttribute("value");
575 show(document.getElementById("category-raw"));
578 _showStructuredPingData() {
579 show(document.getElementById("category-home"));
585 * Renders the general data
588 setHasData("general-data-section", true);
589 let generalDataSection = document.getElementById("general-data");
590 removeAllChildNodes(generalDataSection);
593 "about-telemetry-names-header",
594 "about-telemetry-values-header",
597 // The payload & environment parts are handled by other renderers.
598 let ignoreSections = ["payload", "environment"];
599 let data = explodeObject(filterObject(aPing, ignoreSections));
601 const table = GenericTable.render(data, headings);
602 generalDataSection.appendChild(table);
606 var EnvironmentData = {
608 * Renders the environment data
611 let dataDiv = document.getElementById("environment-data");
612 removeAllChildNodes(dataDiv);
613 const hasData = !!ping.environment;
614 setHasData("environment-data-section", hasData);
619 let ignore = ["addons"];
620 let env = filterObject(ping.environment, ignore);
621 let sections = sectionalizeObject(env);
622 GenericSubsection.render(sections, dataDiv, "environment-data-section");
624 // We use specialized rendering here to make the addon and plugin listings
626 this.createAddonSection(dataDiv, ping);
629 renderAddonsObject(addonObj, addonSection, sectionTitle) {
630 let table = document.createElement("table");
631 table.setAttribute("id", sectionTitle);
632 this.appendAddonSubsectionTitle(sectionTitle, table);
634 for (let id of Object.keys(addonObj)) {
635 let addon = addonObj[id];
636 this.appendHeadingName(table, addon.name || id);
637 this.appendAddonID(table, id);
638 let data = explodeObject(addon);
640 for (let [key, value] of data) {
641 this.appendRow(table, key, value);
645 addonSection.appendChild(table);
648 renderKeyValueObject(addonObj, addonSection, sectionTitle) {
649 let data = explodeObject(addonObj);
650 let table = GenericTable.render(data);
651 table.setAttribute("class", sectionTitle);
652 this.appendAddonSubsectionTitle(sectionTitle, table);
653 addonSection.appendChild(table);
656 appendAddonID(table, addonID) {
657 this.appendRow(table, "id", addonID);
660 appendHeadingName(table, name) {
661 let headings = document.createElement("tr");
662 this.appendColumn(headings, "th", name);
663 headings.cells[0].colSpan = 2;
664 table.appendChild(headings);
667 appendAddonSubsectionTitle(section, table) {
668 let caption = document.createElement("caption");
669 caption.appendChild(document.createTextNode(section));
670 table.appendChild(caption);
673 createAddonSection(dataDiv, ping) {
674 if (!ping || !("environment" in ping) || !("addons" in ping.environment)) {
677 let addonSection = document.createElement("div");
678 addonSection.setAttribute("class", "subsection-data subdata");
679 let addons = ping.environment.addons;
680 this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
681 this.renderKeyValueObject(addons.theme, addonSection, "theme");
682 this.renderAddonsObject(
683 addons.activeGMPlugins,
688 let hasAddonData = !!Object.keys(ping.environment.addons).length;
689 let s = GenericSubsection.renderSubsectionHeader(
692 "environment-data-section"
694 s.appendChild(addonSection);
695 dataDiv.appendChild(s);
698 appendRow(table, id, value) {
699 let row = document.createElement("tr");
701 this.appendColumn(row, "td", id);
702 this.appendColumn(row, "td", value);
703 table.appendChild(row);
706 * Helper function for appending a column to the data table.
708 * @param aRowElement Parent row element
709 * @param aColType Column's tag name
710 * @param aColText Column contents
712 appendColumn(aRowElement, aColType, aColText) {
713 let colElement = document.createElement(aColType);
714 let colTextElement = document.createTextNode(aColText);
715 colElement.appendChild(colTextElement);
716 aRowElement.appendChild(colElement);
722 * Render slow SQL statistics
724 render: function SlowSQL_render(aPing) {
725 // We can add the debug SQL data to the current ping later.
726 // However, we need to be careful to never send that debug data
727 // out due to privacy concerns.
728 // We want to show the actual ping data for archived pings,
729 // so skip this there.
732 PingPicker.viewCurrentPingData &&
733 Preferences.get(PREF_DEBUG_SLOW_SQL, false);
734 let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
736 setHasData("slow-sql-section", false);
740 let { mainThread, otherThreads } = debugSlowSql
741 ? Telemetry.debugSlowSQL
742 : aPing.payload.slowSQL;
744 let mainThreadCount = Object.keys(mainThread).length;
745 let otherThreadCount = Object.keys(otherThreads).length;
746 if (mainThreadCount == 0 && otherThreadCount == 0) {
747 setHasData("slow-sql-section", false);
751 setHasData("slow-sql-section", true);
753 document.getElementById("sql-warning").hidden = false;
756 let slowSqlDiv = document.getElementById("slow-sql-tables");
757 removeAllChildNodes(slowSqlDiv);
760 if (mainThreadCount > 0) {
761 let table = document.createElement("table");
762 this.renderTableHeader(table, "main");
763 this.renderTable(table, mainThread);
764 slowSqlDiv.appendChild(table);
768 if (otherThreadCount > 0) {
769 let table = document.createElement("table");
770 this.renderTableHeader(table, "other");
771 this.renderTable(table, otherThreads);
772 slowSqlDiv.appendChild(table);
777 * Creates a header row for a Slow SQL table
778 * Tabs & newlines added to cells to make it easier to copy-paste.
780 * @param aTable Parent table element
781 * @param aTitle Table's title
783 renderTableHeader: function SlowSQL_renderTableHeader(aTable, threadType) {
784 let caption = document.createElement("caption");
785 if (threadType == "main") {
786 document.l10n.setAttributes(caption, "about-telemetry-slow-sql-main");
789 if (threadType == "other") {
790 document.l10n.setAttributes(caption, "about-telemetry-slow-sql-other");
792 aTable.appendChild(caption);
794 let headings = document.createElement("tr");
795 document.l10n.setAttributes(
796 this.appendColumn(headings, "th"),
797 "about-telemetry-slow-sql-hits"
799 document.l10n.setAttributes(
800 this.appendColumn(headings, "th"),
801 "about-telemetry-slow-sql-average"
803 document.l10n.setAttributes(
804 this.appendColumn(headings, "th"),
805 "about-telemetry-slow-sql-statement"
807 aTable.appendChild(headings);
811 * Fills out the table body
812 * Tabs & newlines added to cells to make it easier to copy-paste.
814 * @param aTable Parent table element
815 * @param aSql SQL stats object
817 renderTable: function SlowSQL_renderTable(aTable, aSql) {
818 for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) {
819 let averageTime = totalTime / hitCount;
821 let sqlRow = document.createElement("tr");
823 this.appendColumn(sqlRow, "td", hitCount + "\t");
824 this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
825 this.appendColumn(sqlRow, "td", sql + "\n");
827 aTable.appendChild(sqlRow);
832 * Helper function for appending a column to a Slow SQL table.
834 * @param aRowElement Parent row element
835 * @param aColType Column's tag name
836 * @param aColText Column contents
838 appendColumn: function SlowSQL_appendColumn(
843 let colElement = document.createElement(aColType);
845 let colTextElement = document.createTextNode(aColText);
846 colElement.appendChild(colTextElement);
848 aRowElement.appendChild(colElement);
853 var StackRenderer = {
855 * Outputs the memory map associated with this hang report
857 * @param aDiv Output div
859 renderMemoryMap: async function StackRenderer_renderMemoryMap(
863 let memoryMapTitleElement = document.createElement("span");
864 document.l10n.setAttributes(
865 memoryMapTitleElement,
866 "about-telemetry-memory-map-title"
868 aDiv.appendChild(memoryMapTitleElement);
869 aDiv.appendChild(document.createElement("br"));
871 for (let currentModule of memoryMap) {
872 aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
873 aDiv.appendChild(document.createElement("br"));
876 aDiv.appendChild(document.createElement("br"));
880 * Outputs the raw PCs from the hang's stack
882 * @param aDiv Output div
883 * @param aStack Array of PCs from the hang stack
885 renderStack: function StackRenderer_renderStack(aDiv, aStack) {
886 let stackTitleElement = document.createElement("span");
887 document.l10n.setAttributes(
889 "about-telemetry-stack-title"
891 aDiv.appendChild(stackTitleElement);
892 let stackText = " " + aStack.join(" ");
893 aDiv.appendChild(document.createTextNode(stackText));
895 aDiv.appendChild(document.createElement("br"));
896 aDiv.appendChild(document.createElement("br"));
898 renderStacks: function StackRenderer_renderStacks(
904 let div = document.getElementById(aPrefix);
905 removeAllChildNodes(div);
907 let fetchE = document.getElementById(aPrefix + "-fetch-symbols");
909 fetchE.hidden = false;
911 let hideE = document.getElementById(aPrefix + "-hide-symbols");
916 if (!aStacks.length) {
920 setHasData(aPrefix + "-section", true);
922 this.renderMemoryMap(div, aMemoryMap);
924 for (let i = 0; i < aStacks.length; ++i) {
925 let stack = aStacks[i];
927 this.renderStack(div, stack);
932 * Renders the title of the stack: e.g. "Late Write #1" or
933 * "Hang Report #1 (6 seconds)".
935 * @param aDivId The id of the div to append the header to.
936 * @param aL10nId The l10n id of the message to use for the title.
937 * @param aL10nArgs The l10n args for the provided message id.
939 renderHeader: function StackRenderer_renderHeader(
944 let div = document.getElementById(aDivId);
946 let titleElement = document.createElement("span");
947 titleElement.className = "stack-title";
949 document.l10n.setAttributes(titleElement, aL10nId, aL10nArgs);
951 div.appendChild(titleElement);
952 div.appendChild(document.createElement("br"));
956 var RawPayloadData = {
958 * Renders the raw pyaload.
961 setHasData("raw-payload-section", true);
962 let pre = document.getElementById("raw-payload-data");
963 pre.textContent = JSON.stringify(aPing.payload, null, 2);
968 .getElementById("payload-json-viewer")
969 .addEventListener("click", e => {
970 openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2));
975 function SymbolicationRequest(
982 this.prefix = aPrefix;
983 this.renderHeader = aRenderHeader;
984 this.memoryMap = aMemoryMap;
985 this.stacks = aStacks;
986 this.durations = aDurations;
989 * A callback for onreadystatechange. It replaces the numeric stack with
990 * the symbolicated one returned by the symbolication server.
992 SymbolicationRequest.prototype.handleSymbolResponse =
993 async function SymbolicationRequest_handleSymbolResponse() {
994 if (this.symbolRequest.readyState != 4) {
998 let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
999 fetchElement.hidden = true;
1000 let hideElement = document.getElementById(this.prefix + "-hide-symbols");
1001 hideElement.hidden = false;
1002 let div = document.getElementById(this.prefix);
1003 removeAllChildNodes(div);
1004 let errorMessage = await document.l10n.formatValue(
1005 "about-telemetry-error-fetching-symbols"
1008 if (this.symbolRequest.status != 200) {
1009 div.appendChild(document.createTextNode(errorMessage));
1013 let jsonResponse = {};
1015 jsonResponse = JSON.parse(this.symbolRequest.responseText);
1017 div.appendChild(document.createTextNode(errorMessage));
1021 for (let i = 0; i < jsonResponse.length; ++i) {
1022 let stack = jsonResponse[i];
1023 this.renderHeader(i, this.durations);
1025 for (let symbol of stack) {
1026 div.appendChild(document.createTextNode(symbol));
1027 div.appendChild(document.createElement("br"));
1029 div.appendChild(document.createElement("br"));
1033 * Send a request to the symbolication server to symbolicate this stack.
1035 SymbolicationRequest.prototype.fetchSymbols =
1036 function SymbolicationRequest_fetchSymbols() {
1037 let symbolServerURI = Preferences.get(
1038 PREF_SYMBOL_SERVER_URI,
1039 DEFAULT_SYMBOL_SERVER_URI
1042 memoryMap: this.memoryMap,
1043 stacks: this.stacks,
1046 let requestJSON = JSON.stringify(request);
1048 this.symbolRequest = new XMLHttpRequest();
1049 this.symbolRequest.open("POST", symbolServerURI, true);
1050 this.symbolRequest.setRequestHeader("Content-type", "application/json");
1051 this.symbolRequest.setRequestHeader("Content-length", requestJSON.length);
1052 this.symbolRequest.setRequestHeader("Connection", "close");
1053 this.symbolRequest.onreadystatechange =
1054 this.handleSymbolResponse.bind(this);
1055 this.symbolRequest.send(requestJSON);
1060 * Renders a single Telemetry histogram
1062 * @param aParent Parent element
1063 * @param aName Histogram name
1064 * @param aHgram Histogram information
1065 * @param aOptions Object with render options
1066 * * exponential: bars follow logarithmic scale
1068 render: function Histogram_render(aParent, aName, aHgram, aOptions) {
1069 let options = aOptions || {};
1070 let hgram = this.processHistogram(aHgram, aName);
1072 let outerDiv = document.createElement("div");
1073 outerDiv.className = "histogram";
1074 outerDiv.id = aName;
1076 let divTitle = document.createElement("div");
1077 divTitle.classList.add("histogram-title");
1078 divTitle.appendChild(document.createTextNode(aName));
1079 outerDiv.appendChild(divTitle);
1081 let divStats = document.createElement("div");
1082 divStats.classList.add("histogram-stats");
1084 let histogramStatsArgs = {
1085 sampleCount: hgram.sample_count,
1086 prettyAverage: hgram.pretty_average,
1090 document.l10n.setAttributes(
1092 "about-telemetry-histogram-stats",
1097 hgram.values.reverse();
1100 let textData = this.renderValues(outerDiv, hgram, options);
1102 // The 'Copy' button contains the textual data, copied to clipboard on click
1103 let copyButton = document.createElement("button");
1104 copyButton.className = "copy-node";
1105 document.l10n.setAttributes(copyButton, "about-telemetry-histogram-copy");
1107 copyButton.addEventListener("click", async function () {
1108 let divStatsString = await document.l10n.formatValue(
1109 "about-telemetry-histogram-stats",
1112 copyButton.histogramText =
1113 aName + EOL + divStatsString + EOL + EOL + textData;
1114 Cc["@mozilla.org/widget/clipboardhelper;1"]
1115 .getService(Ci.nsIClipboardHelper)
1116 .copyString(this.histogramText);
1118 outerDiv.appendChild(copyButton);
1120 aParent.appendChild(outerDiv);
1124 processHistogram(aHgram, aName) {
1125 const values = Object.keys(aHgram.values).map(k => aHgram.values[k]);
1126 if (!values.length) {
1127 // If we have no values collected for this histogram, just return
1128 // zero values so we still render it.
1138 const sample_count = values.reduceRight((a, b) => a + b);
1139 const average = Math.round((aHgram.sum * 10) / sample_count) / 10;
1140 const max_value = Math.max(...values);
1142 const labelledValues = Object.keys(aHgram.values).map(k => [
1148 values: labelledValues,
1149 pretty_average: average,
1159 * Return a non-negative, logarithmic representation of a non-negative number.
1160 * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
1162 * @param aNumber Non-negative number
1164 getLogValue(aNumber) {
1165 return Math.max(0, Math.log10(aNumber) + 1);
1169 * Create histogram HTML bars, also returns a textual representation
1170 * Both aMaxValue and aSumValues must be positive.
1171 * Values are assumed to use 0 as baseline.
1173 * @param aDiv Outer parent div
1174 * @param aHgram The histogram data
1175 * @param aOptions Object with render options (@see #render)
1177 renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
1179 // If the last label is not the longest string, alignment will break a little
1181 if (aHgram.values.length) {
1182 labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
1184 let maxBarValue = aOptions.exponential
1185 ? this.getLogValue(aHgram.max)
1188 for (let [label, value] of aHgram.values) {
1189 label = String(label);
1190 let barValue = aOptions.exponential ? this.getLogValue(value) : value;
1192 // Create a text representation: <right-aligned-label> |<bar-of-#><value> <percentage>
1195 " ".repeat(Math.max(0, labelPadTo - label.length)) +
1196 label + // Right-aligned label
1198 "#".repeat(Math.round((MAX_BAR_CHARS * barValue) / maxBarValue)) + // Bar
1202 Math.round((100 * value) / aHgram.sample_count) +
1205 // Construct the HTML labels + bars
1207 Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
1208 let aboveEm = MAX_BAR_HEIGHT - belowEm;
1210 let barDiv = document.createElement("div");
1211 barDiv.className = "bar";
1212 barDiv.style.paddingTop = aboveEm + "em";
1214 // Add value label or an nbsp if no value
1215 barDiv.appendChild(document.createTextNode(value ? value : "\u00A0"));
1217 // Create the blue bar
1218 let bar = document.createElement("div");
1219 bar.className = "bar-inner";
1220 bar.style.height = belowEm + "em";
1221 barDiv.appendChild(bar);
1223 // Add a special class to move the text down to prevent text overlap
1224 if (label.length > 3) {
1225 bar.classList.add("long-label");
1228 barDiv.appendChild(document.createTextNode(label));
1230 aDiv.appendChild(barDiv);
1233 return text.substr(EOL.length); // Trim the EOL before the first line
1238 HASH_SEARCH: "search=",
1240 // A list of ids of sections that do not support search.
1241 blacklist: ["late-writes-section", "raw-payload-section"],
1243 // Pass if: all non-empty array items match (case-sensitive)
1244 isPassText(subject, filter) {
1245 for (let item of filter) {
1246 if (item.length && !subject.includes(item)) {
1247 return false; // mismatch and not a spurious space
1253 isPassRegex(subject, filter) {
1254 return filter.test(subject);
1257 chooseFilter(filterText) {
1258 let filter = filterText.toString();
1259 // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
1260 let isPassFunc; // filter function, set once, then applied to all elements
1261 filter = filter.trim();
1262 if (filter[0] != "/") {
1263 // Plain text: case insensitive, AND if multi-string
1264 isPassFunc = this.isPassText;
1265 filter = filter.toLowerCase().split(" ");
1267 isPassFunc = this.isPassRegex;
1268 var r = filter.match(/^\/(.*)\/(i?)$/);
1270 filter = RegExp(r[1], r[2]);
1272 // Incomplete or bad RegExp - always no match
1273 isPassFunc = function () {
1278 return [isPassFunc, filter];
1281 filterTextRows(table, filterText) {
1282 let [isPassFunc, filter] = this.chooseFilter(filterText);
1283 let allElementHidden = true;
1285 let needLowerCase = isPassFunc === this.isPassText;
1286 let elements = table.rows;
1287 for (let element of elements) {
1288 if (element.firstChild.nodeName == "th") {
1291 for (let cell of element.children) {
1292 let subject = needLowerCase
1293 ? cell.textContent.toLowerCase()
1295 element.hidden = !isPassFunc(subject, filter);
1296 if (!element.hidden) {
1297 if (allElementHidden) {
1298 allElementHidden = false;
1300 // Don't need to check the rest of this row.
1305 // Unhide the first row:
1306 if (!allElementHidden) {
1307 table.rows[0].hidden = false;
1309 return allElementHidden;
1312 filterElements(elements, filterText) {
1313 let [isPassFunc, filter] = this.chooseFilter(filterText);
1314 let allElementHidden = true;
1316 let needLowerCase = isPassFunc === this.isPassText;
1317 for (let element of elements) {
1318 let subject = needLowerCase ? element.id.toLowerCase() : element.id;
1319 element.hidden = !isPassFunc(subject, filter);
1320 if (allElementHidden && !element.hidden) {
1321 allElementHidden = false;
1324 return allElementHidden;
1327 filterKeyedElements(keyedElements, filterText) {
1328 let [isPassFunc, filter] = this.chooseFilter(filterText);
1329 let allElementsHidden = true;
1331 let needLowerCase = isPassFunc === this.isPassText;
1332 keyedElements.forEach(keyedElement => {
1333 let subject = needLowerCase
1334 ? keyedElement.key.id.toLowerCase()
1335 : keyedElement.key.id;
1336 if (!isPassFunc(subject, filter)) {
1337 // If the keyedHistogram's name is not matched
1338 let allKeyedElementsHidden = true;
1339 for (let element of keyedElement.datas) {
1340 let subject = needLowerCase ? element.id.toLowerCase() : element.id;
1341 let match = isPassFunc(subject, filter);
1342 element.hidden = !match;
1344 allKeyedElementsHidden = false;
1347 if (allElementsHidden && !allKeyedElementsHidden) {
1348 allElementsHidden = false;
1350 keyedElement.key.hidden = allKeyedElementsHidden;
1352 // If the keyedHistogram's name is matched
1353 allElementsHidden = false;
1354 keyedElement.key.hidden = false;
1355 for (let element of keyedElement.datas) {
1356 element.hidden = false;
1360 return allElementsHidden;
1364 if (this.idleTimeout) {
1365 clearTimeout(this.idleTimeout);
1367 this.idleTimeout = setTimeout(
1368 () => Search.search(e.target.value),
1373 search(text, sectionParam = null) {
1374 let section = sectionParam;
1376 let sectionId = document
1377 .querySelector(".category.selected")
1378 .getAttribute("value");
1379 section = document.getElementById(sectionId);
1381 if (Search.blacklist.includes(section.id)) {
1384 let noSearchResults = true;
1385 // In the home section, we search all other sections:
1386 if (section.id === "home-section") {
1387 return this.homeSearch(text);
1390 if (section.id === "histograms-section") {
1391 let histograms = section.getElementsByClassName("histogram");
1392 noSearchResults = this.filterElements(histograms, text);
1393 } else if (section.id === "keyed-histograms-section") {
1394 let keyedElements = [];
1395 let keyedHistograms = section.getElementsByClassName("keyed-histogram");
1396 for (let key of keyedHistograms) {
1397 let datas = key.getElementsByClassName("histogram");
1398 keyedElements.push({ key, datas });
1400 noSearchResults = this.filterKeyedElements(keyedElements, text);
1401 } else if (section.id === "keyed-scalars-section") {
1402 let keyedElements = [];
1403 let keyedScalars = section.getElementsByClassName("keyed-scalar");
1404 for (let key of keyedScalars) {
1405 let datas = key.querySelector("table").rows;
1406 keyedElements.push({ key, datas });
1408 noSearchResults = this.filterKeyedElements(keyedElements, text);
1409 } else if (section.matches(".text-search")) {
1410 let tables = section.querySelectorAll("table");
1411 for (let table of tables) {
1412 // If we unhide anything, flip noSearchResults to
1413 // false so we don't show the "no results" bits.
1414 if (!this.filterTextRows(table, text)) {
1415 noSearchResults = false;
1418 } else if (section.querySelector(".sub-section")) {
1419 let keyedSubSections = [];
1420 let subsections = section.querySelectorAll(".sub-section");
1421 for (let section of subsections) {
1422 let datas = section.querySelector("table").rows;
1423 keyedSubSections.push({ key: section, datas });
1425 noSearchResults = this.filterKeyedElements(keyedSubSections, text);
1427 let tables = section.querySelectorAll("table");
1428 for (let table of tables) {
1429 noSearchResults = this.filterElements(table.rows, text);
1430 if (table.caption) {
1431 table.caption.hidden = noSearchResults;
1436 changeUrlSearch(text);
1438 if (!sectionParam) {
1439 // If we are not searching in all section.
1440 this.updateNoResults(text, noSearchResults);
1442 return noSearchResults;
1445 updateNoResults(text, noSearchResults) {
1447 .getElementById("no-search-results")
1448 .classList.toggle("hidden", !noSearchResults);
1449 if (noSearchResults) {
1450 let section = document.querySelector(".category.selected > span");
1451 let searchResultsText = document.getElementById("no-search-results-text");
1452 if (section.parentElement.id === "category-home") {
1453 document.l10n.setAttributes(
1455 "about-telemetry-no-search-results-all",
1456 { searchTerms: text }
1459 let sectionName = section.textContent.trim();
1461 ? document.l10n.setAttributes(
1463 "about-telemetry-no-data-to-display",
1466 : document.l10n.setAttributes(
1468 "about-telemetry-no-search-results",
1469 { sectionName, currentSearchText: text }
1476 document.getElementById("main").classList.remove("search");
1477 document.getElementById("no-search-results").classList.add("hidden");
1478 adjustHeaderState();
1479 Array.from(document.querySelectorAll("section")).forEach(section => {
1480 section.classList.toggle("active", section.id == "home-section");
1485 changeUrlSearch(text);
1486 removeSearchSectionTitles();
1491 document.getElementById("main").classList.add("search");
1492 adjustHeaderState(text);
1493 let noSearchResults = true;
1494 Array.from(document.querySelectorAll("section")).forEach(section => {
1495 if (section.id == "home-section" || section.id == "raw-payload-section") {
1496 section.classList.remove("active");
1499 section.classList.add("active");
1500 let sectionHidden = this.search(text, section);
1501 if (!sectionHidden) {
1502 let sectionTitle = document.querySelector(
1503 `.category[value="${section.id}"] .category-name`
1505 let sectionDataDiv = document.querySelector(
1506 `#${section.id}.has-data.active .data`
1508 let titleDiv = document.createElement("h1");
1509 titleDiv.classList.add("data", "search-section-title");
1510 titleDiv.textContent = sectionTitle;
1511 section.insertBefore(titleDiv, sectionDataDiv);
1512 noSearchResults = false;
1514 // Hide all subsections if the section is hidden
1515 let subsections = section.querySelectorAll(".sub-section");
1516 for (let subsection of subsections) {
1517 subsection.hidden = true;
1521 this.updateNoResults(text, noSearchResults);
1526 * Helper function to render JS objects with white space between top level elements
1527 * so that they look better in the browser
1528 * @param aObject JavaScript object or array to render
1531 function RenderObject(aObject) {
1533 if (Array.isArray(aObject)) {
1534 if (!aObject.length) {
1537 output = "[" + JSON.stringify(aObject[0]);
1538 for (let i = 1; i < aObject.length; i++) {
1539 output += ", " + JSON.stringify(aObject[i]);
1541 return output + "]";
1543 let keys = Object.keys(aObject);
1547 output = '{"' + keys[0] + '":\u00A0' + JSON.stringify(aObject[keys[0]]);
1548 for (let i = 1; i < keys.length; i++) {
1549 output += ', "' + keys[i] + '":\u00A0' + JSON.stringify(aObject[keys[i]]);
1551 return output + "}";
1554 var GenericSubsection = {
1555 addSubSectionToSidebar(id, title) {
1556 let category = document.querySelector("#categories > [value=" + id + "]");
1557 category.classList.add("has-subsection");
1558 let subCategory = document.createElement("div");
1559 subCategory.classList.add("category-subsection");
1560 subCategory.setAttribute("value", id + "-" + title);
1561 subCategory.addEventListener("click", ev => {
1562 let section = ev.target;
1563 showSubSection(section);
1565 subCategory.appendChild(document.createTextNode(title));
1566 category.appendChild(subCategory);
1569 render(data, dataDiv, sectionID) {
1570 for (let [title, sectionData] of data) {
1571 let hasData = sectionData.size > 0;
1572 let s = this.renderSubsectionHeader(title, hasData, sectionID);
1573 s.appendChild(this.renderSubsectionData(title, sectionData));
1574 dataDiv.appendChild(s);
1578 renderSubsectionHeader(title, hasData, sectionID) {
1579 this.addSubSectionToSidebar(sectionID, title);
1580 let section = document.createElement("div");
1581 section.setAttribute("id", sectionID + "-" + title);
1582 section.classList.add("sub-section");
1584 section.classList.add("has-subdata");
1589 renderSubsectionData(title, data) {
1590 // Create data container
1591 let dataDiv = document.createElement("div");
1592 dataDiv.setAttribute("class", "subsection-data subdata");
1593 // Instanciate the data
1594 let table = GenericTable.render(data);
1595 let caption = document.createElement("caption");
1596 caption.textContent = title;
1597 table.appendChild(caption);
1598 dataDiv.appendChild(table);
1603 deleteAllSubSections() {
1604 let subsections = document.querySelectorAll(".category-subsection");
1605 subsections.forEach(el => {
1606 el.parentElement.removeChild(el);
1611 var GenericTable = {
1612 // Returns a table with key and value headers
1614 return ["about-telemetry-keys-header", "about-telemetry-values-header"];
1618 * Returns a n-column table.
1619 * @param rows An array of arrays, each containing data to render
1621 * @param headings The column header strings.
1623 render(rows, headings = this.defaultHeadings()) {
1624 let table = document.createElement("table");
1625 this.renderHeader(table, headings);
1626 this.renderBody(table, rows);
1631 * Create the table header.
1632 * Tabs & newlines added to cells to make it easier to copy-paste.
1634 * @param table Table element
1635 * @param headings Array of column header strings.
1637 renderHeader(table, headings) {
1638 let headerRow = document.createElement("tr");
1639 table.appendChild(headerRow);
1641 for (let i = 0; i < headings.length; ++i) {
1642 let column = document.createElement("th");
1643 document.l10n.setAttributes(column, headings[i]);
1644 headerRow.appendChild(column);
1649 * Create the table body
1650 * Tabs & newlines added to cells to make it easier to copy-paste.
1652 * @param table Table element
1653 * @param rows An array of arrays, each containing data to render
1656 renderBody(table, rows) {
1657 for (let row of rows) {
1658 row = row.map(value => {
1659 // use .valueOf() to unbox Number, String, etc. objects
1662 typeof value == "object" &&
1663 typeof value.valueOf() == "object"
1665 return RenderObject(value);
1670 let newRow = document.createElement("tr");
1672 table.appendChild(newRow);
1674 for (let i = 0; i < row.length; ++i) {
1675 let suffix = i == row.length - 1 ? "\n" : "\t";
1676 let field = document.createElement("td");
1677 field.appendChild(document.createTextNode(row[i] + suffix));
1678 newRow.appendChild(field);
1684 var KeyedHistogram = {
1685 render(parent, id, keyedHistogram) {
1686 let outerDiv = document.createElement("div");
1687 outerDiv.className = "keyed-histogram";
1690 let divTitle = document.createElement("div");
1691 divTitle.classList.add("keyed-title");
1692 divTitle.appendChild(document.createTextNode(id));
1693 outerDiv.appendChild(divTitle);
1695 for (let [name, hgram] of Object.entries(keyedHistogram)) {
1696 Histogram.render(outerDiv, name, hgram);
1699 parent.appendChild(outerDiv);
1704 var AddonDetails = {
1706 * Render the addon details section as a series of headers followed by key/value tables
1707 * @param aPing A ping object to render the data from.
1710 let addonSection = document.getElementById("addon-details");
1711 removeAllChildNodes(addonSection);
1712 let addonDetails = aPing.payload.addonDetails;
1713 const hasData = addonDetails && !!Object.keys(addonDetails).length;
1714 setHasData("addon-details-section", hasData);
1719 for (let provider in addonDetails) {
1720 let providerSection = document.createElement("caption");
1721 document.l10n.setAttributes(
1723 "about-telemetry-addon-provider",
1724 { addonProvider: provider }
1726 let headingStrings = [
1727 "about-telemetry-addon-table-id",
1728 "about-telemetry-addon-table-details",
1730 let table = GenericTable.render(
1731 explodeObject(addonDetails[provider]),
1734 table.appendChild(providerSection);
1735 addonSection.appendChild(table);
1741 static renderContent(data, process, div, section) {
1742 if (data && Object.keys(data).length) {
1743 let s = GenericSubsection.renderSubsectionHeader(process, true, section);
1744 let heading = document.createElement("h2");
1745 document.l10n.setAttributes(heading, "about-telemetry-process", {
1748 s.appendChild(heading);
1750 this.renderData(data, s);
1753 let separator = document.createElement("div");
1754 separator.classList.add("clearfix");
1755 div.appendChild(separator);
1760 * Make parent process the first one, content process the second
1761 * then sort processes alphabetically
1763 static processesComparator(a, b) {
1764 if (a === "parent" || (a === "content" && b !== "parent")) {
1766 } else if (b === "parent" || b === "content") {
1779 static renderSection(divName, section, aPayload) {
1780 let div = document.getElementById(divName);
1781 removeAllChildNodes(div);
1784 let hasData = false;
1785 let selectedStore = getSelectedStore();
1787 let payload = aPayload.stores;
1789 let isCurrentPayload = !!payload;
1792 let sortedProcesses = isCurrentPayload
1793 ? Object.keys(payload[selectedStore]).sort(this.processesComparator)
1794 : Object.keys(aPayload.processes).sort(this.processesComparator);
1796 // Render content by process
1797 for (const process of sortedProcesses) {
1798 data = isCurrentPayload
1799 ? this.dataFiltering(payload, selectedStore, process)
1800 : this.archivePingDataFiltering(aPayload, process);
1801 hasData = hasData || !ObjectUtils.isEmpty(data);
1802 this.renderContent(data, process, div, section, this.renderData);
1804 setHasData(section, hasData);
1808 class Scalars extends Section {
1810 * Return data from the current ping
1812 static dataFiltering(payload, selectedStore, process) {
1813 return payload[selectedStore][process].scalars;
1817 * Return data from an archived ping
1819 static archivePingDataFiltering(payload, process) {
1820 return payload.processes[process].scalars;
1823 static renderData(data, div) {
1824 const scalarsHeadings = [
1825 "about-telemetry-names-header",
1826 "about-telemetry-values-header",
1828 let scalarsTable = GenericTable.render(
1829 explodeObject(data),
1832 div.appendChild(scalarsTable);
1836 * Render the scalar data - if present - from the payload in a simple key-value table.
1837 * @param aPayload A payload object to render the data from.
1839 static render(aPayload) {
1840 const divName = "scalars";
1841 const section = "scalars-section";
1842 this.renderSection(divName, section, aPayload);
1846 class KeyedScalars extends Section {
1848 * Return data from the current ping
1850 static dataFiltering(payload, selectedStore, process) {
1851 return payload[selectedStore][process].keyedScalars;
1855 * Return data from an archived ping
1857 static archivePingDataFiltering(payload, process) {
1858 return payload.processes[process].keyedScalars;
1861 static renderData(data, div) {
1862 const scalarsHeadings = [
1863 "about-telemetry-names-header",
1864 "about-telemetry-values-header",
1866 for (let scalarId in data) {
1867 // Add the name of the scalar.
1868 let container = document.createElement("div");
1869 container.classList.add("keyed-scalar");
1870 container.id = scalarId;
1871 let scalarNameSection = document.createElement("p");
1872 scalarNameSection.classList.add("keyed-title");
1873 scalarNameSection.appendChild(document.createTextNode(scalarId));
1874 container.appendChild(scalarNameSection);
1875 // Populate the section with the key-value pairs from the scalar.
1876 const table = GenericTable.render(
1877 explodeObject(data[scalarId]),
1880 container.appendChild(table);
1881 div.appendChild(container);
1886 * Render the keyed scalar data - if present - from the payload in a simple key-value table.
1887 * @param aPayload A payload object to render the data from.
1889 static render(aPayload) {
1890 const divName = "keyed-scalars";
1891 const section = "keyed-scalars-section";
1892 this.renderSection(divName, section, aPayload);
1898 * Render the event data - if present - from the payload in a simple table.
1899 * @param aPayload A payload object to render the data from.
1902 let eventsDiv = document.getElementById("events");
1903 removeAllChildNodes(eventsDiv);
1905 "about-telemetry-time-stamp-header",
1906 "about-telemetry-category-header",
1907 "about-telemetry-method-header",
1908 "about-telemetry-object-header",
1909 "about-telemetry-values-header",
1910 "about-telemetry-extra-header",
1912 let payload = aPayload.processes;
1913 let hasData = false;
1915 for (const process of Object.keys(aPayload.processes)) {
1916 let data = aPayload.processes[process].events;
1917 if (data && Object.keys(data).length) {
1919 let s = GenericSubsection.renderSubsectionHeader(
1924 let heading = document.createElement("h2");
1925 heading.textContent = process;
1926 s.appendChild(heading);
1927 const table = GenericTable.render(data, headings);
1928 s.appendChild(table);
1929 eventsDiv.appendChild(s);
1930 let separator = document.createElement("div");
1931 separator.classList.add("clearfix");
1932 eventsDiv.appendChild(separator);
1936 // handle archived ping
1937 for (const process of Object.keys(aPayload.events)) {
1939 if (data && Object.keys(data).length) {
1941 let s = GenericSubsection.renderSubsectionHeader(
1946 let heading = document.createElement("h2");
1947 heading.textContent = process;
1948 s.appendChild(heading);
1949 const table = GenericTable.render(data, headings);
1950 eventsDiv.appendChild(table);
1951 let separator = document.createElement("div");
1952 separator.classList.add("clearfix");
1953 eventsDiv.appendChild(separator);
1957 setHasData("events-section", hasData);
1962 * Helper function for showing either the toggle element or "No data collected" message for a section
1964 * @param aSectionID ID of the section element that needs to be changed
1965 * @param aHasData true (default) indicates that toggle should be displayed
1967 function setHasData(aSectionID, aHasData) {
1968 let sectionElement = document.getElementById(aSectionID);
1969 sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
1971 // Display or Hide the section in the sidebar
1972 let sectionCategory = document.querySelector(
1973 ".category[value=" + aSectionID + "]"
1975 sectionCategory.classList[aHasData ? "add" : "remove"]("has-data");
1979 * Sets l10n attributes based on the Telemetry Server Owner pref.
1981 function setupServerOwnerBranding() {
1982 let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
1984 [document.getElementById("page-subtitle"), "about-telemetry-page-subtitle"],
1986 for (const [elt, l10nName] of elements) {
1987 document.l10n.setAttributes(elt, l10nName, {
1988 telemetryServerOwner: serverOwner,
1994 * Display the store selector if we are on one
1995 * of the whitelisted sections
1997 function displayStoresSelector(selectedSection) {
2000 "keyed-scalars-section",
2001 "histograms-section",
2002 "keyed-histograms-section",
2004 let stores = document.getElementById("stores");
2005 stores.hidden = !whitelist.includes(selectedSection);
2006 let storesLabel = document.getElementById("storesLabel");
2007 storesLabel.hidden = !whitelist.includes(selectedSection);
2010 function refreshSearch() {
2011 removeSearchSectionTitles();
2012 let selectedSection = document
2013 .querySelector(".category.selected")
2014 .getAttribute("value");
2015 let search = document.getElementById("search");
2016 if (!Search.blacklist.includes(selectedSection)) {
2017 Search.search(search.value);
2021 function adjustSearchState() {
2022 removeSearchSectionTitles();
2023 let selectedSection = document
2024 .querySelector(".category.selected")
2025 .getAttribute("value");
2026 let search = document.getElementById("search");
2028 search.hidden = Search.blacklist.includes(selectedSection);
2029 document.getElementById("no-search-results").classList.add("hidden");
2030 Search.search(""); // reinitialize search state.
2033 function removeSearchSectionTitles() {
2034 for (let sectionTitleDiv of Array.from(
2035 document.getElementsByClassName("search-section-title")
2037 sectionTitleDiv.remove();
2041 function adjustSection() {
2042 let selectedCategory = document.querySelector(".category.selected");
2043 if (!selectedCategory.classList.contains("has-data")) {
2044 PingPicker._showStructuredPingData();
2048 function adjustHeaderState(title = null) {
2049 let selected = document.querySelector(".category.selected .category-name");
2050 let selectedTitle = selected.textContent.trim();
2051 let sectionTitle = document.getElementById("sectionTitle");
2052 if (title !== null) {
2053 document.l10n.setAttributes(
2055 "about-telemetry-results-for-search",
2056 { searchTerms: title }
2059 sectionTitle.textContent = selectedTitle;
2061 let search = document.getElementById("search");
2062 if (selected.parentElement.id === "category-home") {
2063 document.l10n.setAttributes(
2065 "about-telemetry-filter-all-placeholder"
2068 document.l10n.setAttributes(search, "about-telemetry-filter-placeholder", {
2075 * Change the url according to the current section displayed
2076 * e.g about:telemetry#general-data
2078 function changeUrlPath(selectedSection, subSection) {
2080 let hash = window.location.hash.split("_")[0] + "_" + selectedSection;
2081 window.location.hash = hash;
2083 window.location.hash = selectedSection.replace("-section", "-tab");
2088 * Change the url according to the current search text
2090 function changeUrlSearch(searchText) {
2091 let currentHash = window.location.hash;
2092 let hashWithoutSearch = currentHash.split(Search.HASH_SEARCH)[0];
2095 if (!currentHash && !searchText) {
2098 if (!currentHash.includes(Search.HASH_SEARCH) && hashWithoutSearch) {
2099 hashWithoutSearch += "_";
2103 hashWithoutSearch + Search.HASH_SEARCH + searchText.replace(/ /g, "+");
2104 } else if (hashWithoutSearch) {
2105 hash = hashWithoutSearch.slice(0, hashWithoutSearch.length - 1);
2108 window.location.hash = hash;
2112 * Change the section displayed
2114 function show(selected) {
2115 let selectedValue = selected.getAttribute("value");
2116 if (selectedValue === "raw-json-viewer") {
2117 openJsonInFirefoxJsonViewer(JSON.stringify(gPingData, null, 2));
2121 let selected_section = document.getElementById(selectedValue);
2122 let subsections = selected_section.querySelectorAll(".sub-section");
2123 if (selected.classList.contains("has-subsection")) {
2124 for (let subsection of selected.children) {
2125 subsection.classList.remove("selected");
2129 for (let subsection of subsections) {
2130 subsection.hidden = false;
2134 let current_button = document.querySelector(".category.selected");
2135 if (current_button == selected) {
2138 current_button.classList.remove("selected");
2139 selected.classList.add("selected");
2141 document.querySelectorAll("section").forEach(section => {
2142 section.classList.remove("active");
2144 selected_section.classList.add("active");
2146 adjustHeaderState();
2147 displayStoresSelector(selectedValue);
2148 adjustSearchState();
2149 changeUrlPath(selectedValue);
2152 function showSubSection(selected) {
2156 let current_selection = document.querySelector(
2157 ".category-subsection.selected"
2159 if (current_selection) {
2160 current_selection.classList.remove("selected");
2162 selected.classList.add("selected");
2164 let section = document.getElementById(selected.getAttribute("value"));
2165 section.parentElement.childNodes.forEach(element => {
2166 element.hidden = true;
2168 section.hidden = false;
2171 selected.parentElement.querySelector(".category-name").textContent;
2172 let subsection = selected.textContent;
2173 document.getElementById("sectionTitle").textContent =
2174 title + " - " + subsection;
2175 changeUrlPath(subsection, true);
2179 * Initializes load/unload, pref change and mouse-click listeners
2181 function setupListeners() {
2182 Settings.attachObservers();
2183 PingPicker.attachObservers();
2184 RawPayloadData.attachObservers();
2186 let menu = document.getElementById("categories");
2187 menu.addEventListener("click", e => {
2188 if (e.target && e.target.parentNode == menu) {
2193 let search = document.getElementById("search");
2194 search.addEventListener("input", Search.searchHandler);
2197 .getElementById("late-writes-fetch-symbols")
2198 .addEventListener("click", function () {
2203 let lateWrites = gPingData.payload.lateWrites;
2204 let req = new SymbolicationRequest(
2206 LateWritesSingleton.renderHeader,
2207 lateWrites.memoryMap,
2214 .getElementById("late-writes-hide-symbols")
2215 .addEventListener("click", function () {
2220 LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
2224 // Restores the sections states
2225 function urlSectionRestore(hash) {
2227 let section = hash.replace("-tab", "-section");
2228 let subsection = section.split("_")[1];
2229 section = section.split("_")[0];
2230 let category = document.querySelector(".category[value=" + section + "]");
2235 ".category-subsection[value=" + section + "-" + subsection + "]";
2236 let subcategory = document.querySelector(selector);
2237 showSubSection(subcategory);
2243 // Restore sections states and search terms
2244 function urlStateRestore() {
2245 let hash = window.location.hash;
2246 let searchQuery = "";
2248 hash = hash.slice(1);
2249 if (hash.includes(Search.HASH_SEARCH)) {
2250 searchQuery = hash.split(Search.HASH_SEARCH)[1].replace(/[+]/g, " ");
2251 hash = hash.split(Search.HASH_SEARCH)[0];
2253 urlSectionRestore(hash);
2256 let search = document.getElementById("search");
2257 search.value = searchQuery;
2261 function openJsonInFirefoxJsonViewer(json) {
2262 json = unescape(encodeURIComponent(json));
2264 window.open("data:application/json;base64," + btoa(json));
2266 show(document.querySelector(".category[value=raw-payload-section]"));
2271 window.removeEventListener("load", onLoad);
2272 // Set the text in the page header and elsewhere that needs the server owner.
2273 setupServerOwnerBranding();
2275 // Set up event listeners
2281 adjustHeaderState();
2285 // Update ping data when async Telemetry init is finished.
2286 Telemetry.asyncFetchTelemetryData(async () => {
2287 await PingPicker.update();
2291 var LateWritesSingleton = {
2292 renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
2293 StackRenderer.renderHeader(
2295 "about-telemetry-late-writes-title",
2296 { lateWriteCount: aIndex + 1 }
2300 renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
2303 lateWrites.stacks &&
2304 lateWrites.stacks.length
2306 setHasData("late-writes-section", hasData);
2311 let stacks = lateWrites.stacks;
2312 let memoryMap = lateWrites.memoryMap;
2313 StackRenderer.renderStacks(
2317 LateWritesSingleton.renderHeader
2322 class HistogramSection extends Section {
2324 * Return data from the current ping
2326 static dataFiltering(payload, selectedStore, process) {
2327 return payload[selectedStore][process].histograms;
2331 * Return data from an archived ping
2333 static archivePingDataFiltering(payload, process) {
2334 if (process === "parent") {
2335 return payload.histograms;
2337 return payload.processes[process].histograms;
2340 static renderData(data, div) {
2341 for (let [hName, hgram] of Object.entries(data)) {
2342 Histogram.render(div, hName, hgram, { unpacked: true });
2346 static render(aPayload) {
2347 const divName = "histograms";
2348 const section = "histograms-section";
2349 this.renderSection(divName, section, aPayload);
2353 class KeyedHistogramSection extends Section {
2355 * Return data from the current ping
2357 static dataFiltering(payload, selectedStore, process) {
2358 return payload[selectedStore][process].keyedHistograms;
2362 * Return data from an archived ping
2364 static archivePingDataFiltering(payload, process) {
2365 if (process === "parent") {
2366 return payload.keyedHistograms;
2368 return payload.processes[process].keyedHistograms;
2371 static renderData(data, div) {
2372 for (let [id, keyed] of Object.entries(data)) {
2373 KeyedHistogram.render(div, id, keyed, { unpacked: true });
2377 static render(aPayload) {
2378 const divName = "keyed-histograms";
2379 const section = "keyed-histograms-section";
2380 this.renderSection(divName, section, aPayload);
2384 var SessionInformation = {
2386 let infoSection = document.getElementById("session-info");
2387 removeAllChildNodes(infoSection);
2389 let hasData = !!Object.keys(aPayload.info).length;
2390 setHasData("session-info-section", hasData);
2393 const table = GenericTable.render(explodeObject(aPayload.info));
2394 infoSection.appendChild(table);
2399 var SimpleMeasurements = {
2401 let simpleSection = document.getElementById("simple-measurements");
2402 removeAllChildNodes(simpleSection);
2404 let simpleMeasurements = this.sortStartupMilestones(
2405 aPayload.simpleMeasurements
2407 let hasData = !!Object.keys(simpleMeasurements).length;
2408 setHasData("simple-measurements-section", hasData);
2411 const table = GenericTable.render(explodeObject(simpleMeasurements));
2412 simpleSection.appendChild(table);
2417 * Helper function for sorting the startup milestones in the Simple Measurements
2418 * section into temporal order.
2420 * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
2421 * @return Sorted measurements
2423 sortStartupMilestones(aSimpleMeasurements) {
2424 const telemetryTimestamps = TelemetryTimestamps.get();
2425 let startupEvents = Services.startup.getStartupInfo();
2426 delete startupEvents.process;
2428 function keyIsMilestone(k) {
2429 return k in startupEvents || k in telemetryTimestamps;
2432 let sortedKeys = Object.keys(aSimpleMeasurements);
2434 // Sort the measurements, with startup milestones at the front + ordered by time
2435 sortedKeys.sort(function keyCompare(keyA, keyB) {
2436 let isKeyAMilestone = keyIsMilestone(keyA);
2437 let isKeyBMilestone = keyIsMilestone(keyB);
2439 // First order by startup vs non-startup measurement
2440 if (isKeyAMilestone && !isKeyBMilestone) {
2443 if (!isKeyAMilestone && isKeyBMilestone) {
2446 // Don't change order of non-startup measurements
2447 if (!isKeyAMilestone && !isKeyBMilestone) {
2451 // If both keys are startup measurements, order them by value
2452 return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
2455 // Insert measurements into a result object in sort-order
2457 for (let key of sortedKeys) {
2458 result[key] = aSimpleMeasurements[key];
2466 * Render stores options
2468 function renderStoreList(payload) {
2469 let storeSelect = document.getElementById("stores");
2470 let storesLabel = document.getElementById("storesLabel");
2471 removeAllChildNodes(storeSelect);
2473 if (!("stores" in payload)) {
2474 storeSelect.classList.add("hidden");
2475 storesLabel.classList.add("hidden");
2479 storeSelect.classList.remove("hidden");
2480 storesLabel.classList.remove("hidden");
2481 storeSelect.disabled = false;
2483 for (let store of Object.keys(payload.stores)) {
2484 let option = document.createElement("option");
2485 option.appendChild(document.createTextNode(store));
2486 option.setAttribute("value", store);
2487 // Select main store by default
2488 if (store === "main") {
2489 option.selected = true;
2491 storeSelect.appendChild(option);
2496 * Return the selected store
2498 function getSelectedStore() {
2499 let storeSelect = document.getElementById("stores");
2500 let storeSelectedOption = storeSelect.selectedOptions.item(0);
2502 storeSelectedOption !== null
2503 ? storeSelectedOption.getAttribute("value")
2505 return selectedStore;
2508 function togglePingSections(isMainPing) {
2509 // We always show the sections that are "common" to all pings.
2510 let commonSections = new Set([
2513 "general-data-section",
2514 "environment-data-section",
2518 let elements = document.querySelectorAll(".category");
2519 for (let section of elements) {
2520 if (commonSections.has(section.getAttribute("value"))) {
2523 // Only show the raw payload for non main ping.
2524 if (section.getAttribute("value") == "raw-payload-section") {
2525 section.classList.toggle("has-data", !isMainPing);
2527 section.classList.toggle("has-data", isMainPing);
2532 function displayPingData(ping, updatePayloadList = false) {
2535 PingPicker.render();
2536 displayRichPingData(ping, updatePayloadList);
2541 PingPicker._showRawPingData();
2545 function displayRichPingData(ping, updatePayloadList) {
2546 // Update the payload list and store lists
2547 if (updatePayloadList) {
2548 renderStoreList(ping.payload);
2551 // Show general data.
2552 GeneralData.render(ping);
2554 // Show environment data.
2555 EnvironmentData.render(ping);
2557 RawPayloadData.render(ping);
2559 // We have special rendering code for the payloads from "main" and "event" pings.
2560 // For any other pings we just render the raw JSON payload.
2561 let isMainPing = ping.type == "main" || ping.type == "saved-session";
2562 let isEventPing = ping.type == "event";
2563 togglePingSections(isMainPing);
2566 // Copy the payload, so we don't modify the raw representation
2567 // Ensure we always have at least the parent process.
2568 let payload = { processes: { parent: {} } };
2569 for (let process of Object.keys(ping.payload.events)) {
2570 payload.processes[process] = {
2571 events: ping.payload.events[process],
2575 // We transformed the actual payload, let's reload the store list if necessary.
2576 if (updatePayloadList) {
2577 renderStoreList(payload);
2581 Events.render(payload);
2589 // Show slow SQL stats
2590 SlowSQL.render(ping);
2592 // Render Addon details.
2593 AddonDetails.render(ping);
2595 let payload = ping.payload;
2596 // Show basic session info gathered
2597 SessionInformation.render(payload);
2599 // Show scalar data.
2600 Scalars.render(payload);
2601 KeyedScalars.render(payload);
2603 // Show histogram data
2604 HistogramSection.render(payload);
2606 // Show keyed histogram data
2607 KeyedHistogramSection.render(payload);
2610 Events.render(payload);
2612 LateWritesSingleton.renderLateWrites(payload.lateWrites);
2614 // Show simple measurements
2615 SimpleMeasurements.render(payload);
2618 window.addEventListener("load", onLoad);