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", () => {
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 TelemetryController.ensureInitialized().then(() =>
356 this._doUpdateCurrentPingData()
360 _doUpdateCurrentPingData() {
361 const subsession = document.getElementById("show-subsession-data").checked;
362 let ping = TelemetryController.getCurrentPingData(subsession);
367 let stores = Telemetry.getAllStores();
369 histograms: Telemetry.getSnapshotForHistograms,
370 keyedHistograms: Telemetry.getSnapshotForKeyedHistograms,
371 scalars: Telemetry.getSnapshotForScalars,
372 keyedScalars: Telemetry.getSnapshotForKeyedScalars,
376 for (const [name, fn] of Object.entries(getData)) {
377 for (const store of stores) {
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] = {};
389 data[store][process][name] = measurement[process];
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)) {
399 delete data.keyedScalars;
400 delete data.histograms;
401 delete data.keyedHistograms;
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,
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")
419 displayPingData(ping, true);
422 _updateArchivedPingData() {
423 let id = this._getSelectedPingId();
424 let res = Promise.resolve();
426 res = TelemetryArchive.promiseArchivedPingById(id).then(ping =>
427 displayPingData(ping, true)
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.
437 this._archivedPings = pingList;
438 // Render the archive data.
439 this._renderPingList();
440 // Update the displayed ping.
441 await this._updateArchivedPingData();
445 let pingSelector = document.getElementById("choose-ping-id");
446 Array.from(pingSelector.children).forEach(child =>
447 removeAllChildNodes(child)
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, {
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);
480 pingSelector.children[2].appendChild(option);
483 this._renderPingTypes(pingTypes);
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);
497 _movePingIndex(offset) {
498 if (this.viewCurrentPingData) {
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
513 pingList = this._archivedPings.slice(newIndex);
515 pingList = this._archivedPings.slice(0, newIndex);
519 let ping = pingList.find(p => {
520 return type == this.TYPE_ALL || p.type == type;
524 this.selectPing(ping);
525 this._updateArchivedPingData();
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;
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;
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;
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;
559 this._updateArchivedPingData();
562 _getSelectedPingName() {
563 let pingSelector = document.getElementById("choose-ping-id");
564 let selected = pingSelector.selectedOptions.item(0);
565 return selected.dataset.date;
568 _getSelectedPingType() {
569 let pingSelector = document.getElementById("choose-ping-id");
570 let selected = pingSelector.selectedOptions.item(0);
571 return selected.dataset.type;
574 _getSelectedPingId() {
575 let pingSelector = document.getElementById("choose-ping-id");
576 let selected = pingSelector.selectedOptions.item(0);
577 return selected.getAttribute("value");
581 show(document.getElementById("category-raw"));
584 _showStructuredPingData() {
585 show(document.getElementById("category-home"));
591 * Renders the general data
594 setHasData("general-data-section", true);
595 let generalDataSection = document.getElementById("general-data");
596 removeAllChildNodes(generalDataSection);
599 "about-telemetry-names-header",
600 "about-telemetry-values-header",
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);
612 var EnvironmentData = {
614 * Renders the environment data
617 let dataDiv = document.getElementById("environment-data");
618 removeAllChildNodes(dataDiv);
619 const hasData = !!ping.environment;
620 setHasData("environment-data-section", hasData);
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
632 this.createAddonSection(dataDiv, ping);
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);
651 addonSection.appendChild(table);
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);
662 appendAddonID(table, addonID) {
663 this.appendRow(table, "id", addonID);
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);
673 appendAddonSubsectionTitle(section, table) {
674 let caption = document.createElement("caption");
675 caption.appendChild(document.createTextNode(section));
676 table.appendChild(caption);
679 createAddonSection(dataDiv, ping) {
680 if (!ping || !("environment" in ping) || !("addons" in ping.environment)) {
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,
694 let hasAddonData = !!Object.keys(ping.environment.addons).length;
695 let s = GenericSubsection.renderSubsectionHeader(
698 "environment-data-section"
700 s.appendChild(addonSection);
701 dataDiv.appendChild(s);
704 appendRow(table, id, value) {
705 let row = document.createElement("tr");
707 this.appendColumn(row, "td", id);
708 this.appendColumn(row, "td", value);
709 table.appendChild(row);
712 * Helper function for appending a column to the data table.
714 * @param aRowElement Parent row element
715 * @param aColType Column's tag name
716 * @param aColText Column contents
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);
728 * Render slow SQL statistics
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.
738 PingPicker.viewCurrentPingData &&
739 Preferences.get(PREF_DEBUG_SLOW_SQL, false);
740 let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
742 setHasData("slow-sql-section", false);
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);
757 setHasData("slow-sql-section", true);
759 document.getElementById("sql-warning").hidden = false;
762 let slowSqlDiv = document.getElementById("slow-sql-tables");
763 removeAllChildNodes(slowSqlDiv);
766 if (mainThreadCount > 0) {
767 let table = document.createElement("table");
768 this.renderTableHeader(table, "main");
769 this.renderTable(table, mainThread);
770 slowSqlDiv.appendChild(table);
774 if (otherThreadCount > 0) {
775 let table = document.createElement("table");
776 this.renderTableHeader(table, "other");
777 this.renderTable(table, otherThreads);
778 slowSqlDiv.appendChild(table);
783 * Creates a header row for a Slow SQL table
784 * Tabs & newlines added to cells to make it easier to copy-paste.
786 * @param aTable Parent table element
787 * @param aTitle Table's title
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");
795 if (threadType == "other") {
796 document.l10n.setAttributes(caption, "about-telemetry-slow-sql-other");
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"
805 document.l10n.setAttributes(
806 this.appendColumn(headings, "th"),
807 "about-telemetry-slow-sql-average"
809 document.l10n.setAttributes(
810 this.appendColumn(headings, "th"),
811 "about-telemetry-slow-sql-statement"
813 aTable.appendChild(headings);
817 * Fills out the table body
818 * Tabs & newlines added to cells to make it easier to copy-paste.
820 * @param aTable Parent table element
821 * @param aSql SQL stats object
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);
838 * Helper function for appending a column to a Slow SQL table.
840 * @param aRowElement Parent row element
841 * @param aColType Column's tag name
842 * @param aColText Column contents
844 appendColumn: function SlowSQL_appendColumn(
849 let colElement = document.createElement(aColType);
851 let colTextElement = document.createTextNode(aColText);
852 colElement.appendChild(colTextElement);
854 aRowElement.appendChild(colElement);
859 var StackRenderer = {
861 * Outputs the memory map associated with this hang report
863 * @param aDiv Output div
865 renderMemoryMap: async function StackRenderer_renderMemoryMap(
869 let memoryMapTitleElement = document.createElement("span");
870 document.l10n.setAttributes(
871 memoryMapTitleElement,
872 "about-telemetry-memory-map-title"
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"));
882 aDiv.appendChild(document.createElement("br"));
886 * Outputs the raw PCs from the hang's stack
888 * @param aDiv Output div
889 * @param aStack Array of PCs from the hang stack
891 renderStack: function StackRenderer_renderStack(aDiv, aStack) {
892 let stackTitleElement = document.createElement("span");
893 document.l10n.setAttributes(
895 "about-telemetry-stack-title"
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"));
904 renderStacks: function StackRenderer_renderStacks(
910 let div = document.getElementById(aPrefix);
911 removeAllChildNodes(div);
913 let fetchE = document.getElementById(aPrefix + "-fetch-symbols");
915 fetchE.hidden = false;
917 let hideE = document.getElementById(aPrefix + "-hide-symbols");
922 if (!aStacks.length) {
926 setHasData(aPrefix + "-section", true);
928 this.renderMemoryMap(div, aMemoryMap);
930 for (let i = 0; i < aStacks.length; ++i) {
931 let stack = aStacks[i];
933 this.renderStack(div, stack);
938 * Renders the title of the stack: e.g. "Late Write #1" or
939 * "Hang Report #1 (6 seconds)".
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.
945 renderHeader: function StackRenderer_renderHeader(
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"));
962 var RawPayloadData = {
964 * Renders the raw pyaload.
967 setHasData("raw-payload-section", true);
968 let pre = document.getElementById("raw-payload-data");
969 pre.textContent = JSON.stringify(aPing.payload, null, 2);
974 .getElementById("payload-json-viewer")
975 .addEventListener("click", () => {
976 openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2));
981 function SymbolicationRequest(
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.
998 SymbolicationRequest.prototype.handleSymbolResponse =
999 async function SymbolicationRequest_handleSymbolResponse() {
1000 if (this.symbolRequest.readyState != 4) {
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"
1014 if (this.symbolRequest.status != 200) {
1015 div.appendChild(document.createTextNode(errorMessage));
1019 let jsonResponse = {};
1021 jsonResponse = JSON.parse(this.symbolRequest.responseText);
1023 div.appendChild(document.createTextNode(errorMessage));
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"));
1035 div.appendChild(document.createElement("br"));
1039 * Send a request to the symbolication server to symbolicate this stack.
1041 SymbolicationRequest.prototype.fetchSymbols =
1042 function SymbolicationRequest_fetchSymbols() {
1043 let symbolServerURI = Preferences.get(
1044 PREF_SYMBOL_SERVER_URI,
1045 DEFAULT_SYMBOL_SERVER_URI
1048 memoryMap: this.memoryMap,
1049 stacks: this.stacks,
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);
1066 * Renders a single Telemetry histogram
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
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,
1096 document.l10n.setAttributes(
1098 "about-telemetry-histogram-stats",
1103 hgram.values.reverse();
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",
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);
1124 outerDiv.appendChild(copyButton);
1126 aParent.appendChild(outerDiv);
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.
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 => [
1154 values: labelledValues,
1155 pretty_average: average,
1165 * Return a non-negative, logarithmic representation of a non-negative number.
1166 * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
1168 * @param aNumber Non-negative number
1170 getLogValue(aNumber) {
1171 return Math.max(0, Math.log10(aNumber) + 1);
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.
1179 * @param aDiv Outer parent div
1180 * @param aHgram The histogram data
1181 * @param aOptions Object with render options (@see #render)
1183 renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
1185 // If the last label is not the longest string, alignment will break a little
1187 if (aHgram.values.length) {
1188 labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
1190 let maxBarValue = aOptions.exponential
1191 ? this.getLogValue(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>
1201 " ".repeat(Math.max(0, labelPadTo - label.length)) +
1202 label + // Right-aligned label
1204 "#".repeat(Math.round((MAX_BAR_CHARS * barValue) / maxBarValue)) + // Bar
1208 Math.round((100 * value) / aHgram.sample_count) +
1211 // Construct the HTML labels + bars
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");
1234 barDiv.appendChild(document.createTextNode(label));
1236 aDiv.appendChild(barDiv);
1239 return text.substr(EOL.length); // Trim the EOL before the first line
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
1259 isPassRegex(subject, filter) {
1260 return filter.test(subject);
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(" ");
1273 isPassFunc = this.isPassRegex;
1274 var r = filter.match(/^\/(.*)\/(i?)$/);
1276 filter = RegExp(r[1], r[2]);
1278 // Incomplete or bad RegExp - always no match
1279 isPassFunc = function () {
1284 return [isPassFunc, filter];
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") {
1297 for (let cell of element.children) {
1298 let subject = needLowerCase
1299 ? cell.textContent.toLowerCase()
1301 element.hidden = !isPassFunc(subject, filter);
1302 if (!element.hidden) {
1303 if (allElementHidden) {
1304 allElementHidden = false;
1306 // Don't need to check the rest of this row.
1311 // Unhide the first row:
1312 if (!allElementHidden) {
1313 table.rows[0].hidden = false;
1315 return allElementHidden;
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;
1330 return allElementHidden;
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;
1350 allKeyedElementsHidden = false;
1353 if (allElementsHidden && !allKeyedElementsHidden) {
1354 allElementsHidden = false;
1356 keyedElement.key.hidden = allKeyedElementsHidden;
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;
1366 return allElementsHidden;
1370 if (this.idleTimeout) {
1371 clearTimeout(this.idleTimeout);
1373 this.idleTimeout = setTimeout(
1374 () => Search.search(e.target.value),
1379 search(text, sectionParam = null) {
1380 let section = sectionParam;
1382 let sectionId = document
1383 .querySelector(".category.selected")
1384 .getAttribute("value");
1385 section = document.getElementById(sectionId);
1387 if (Search.blacklist.includes(section.id)) {
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);
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 });
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 });
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;
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 });
1431 noSearchResults = this.filterKeyedElements(keyedSubSections, text);
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;
1442 changeUrlSearch(text);
1444 if (!sectionParam) {
1445 // If we are not searching in all section.
1446 this.updateNoResults(text, noSearchResults);
1448 return noSearchResults;
1451 updateNoResults(text, noSearchResults) {
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(
1461 "about-telemetry-no-search-results-all",
1462 { searchTerms: text }
1465 let sectionName = section.textContent.trim();
1467 ? document.l10n.setAttributes(
1469 "about-telemetry-no-data-to-display",
1472 : document.l10n.setAttributes(
1474 "about-telemetry-no-search-results",
1475 { sectionName, currentSearchText: text }
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");
1491 changeUrlSearch(text);
1492 removeSearchSectionTitles();
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");
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`
1511 let sectionDataDiv = document.querySelector(
1512 `#${section.id}.has-data.active .data`
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;
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;
1527 this.updateNoResults(text, noSearchResults);
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
1537 function RenderObject(aObject) {
1539 if (Array.isArray(aObject)) {
1540 if (!aObject.length) {
1543 output = "[" + JSON.stringify(aObject[0]);
1544 for (let i = 1; i < aObject.length; i++) {
1545 output += ", " + JSON.stringify(aObject[i]);
1547 return output + "]";
1549 let keys = Object.keys(aObject);
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]]);
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);
1571 subCategory.appendChild(document.createTextNode(title));
1572 category.appendChild(subCategory);
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);
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");
1590 section.classList.add("has-subdata");
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);
1609 deleteAllSubSections() {
1610 let subsections = document.querySelectorAll(".category-subsection");
1611 subsections.forEach(el => {
1612 el.parentElement.removeChild(el);
1617 var GenericTable = {
1618 // Returns a table with key and value headers
1620 return ["about-telemetry-keys-header", "about-telemetry-values-header"];
1624 * Returns a n-column table.
1625 * @param rows An array of arrays, each containing data to render
1627 * @param headings The column header strings.
1629 render(rows, headings = this.defaultHeadings()) {
1630 let table = document.createElement("table");
1631 this.renderHeader(table, headings);
1632 this.renderBody(table, rows);
1637 * Create the table header.
1638 * Tabs & newlines added to cells to make it easier to copy-paste.
1640 * @param table Table element
1641 * @param headings Array of column header strings.
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);
1655 * Create the table body
1656 * Tabs & newlines added to cells to make it easier to copy-paste.
1658 * @param table Table element
1659 * @param rows An array of arrays, each containing data to render
1662 renderBody(table, rows) {
1663 for (let row of rows) {
1664 row = row.map(value => {
1665 // use .valueOf() to unbox Number, String, etc. objects
1668 typeof value == "object" &&
1669 typeof value.valueOf() == "object"
1671 return RenderObject(value);
1676 let newRow = document.createElement("tr");
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);
1690 var KeyedHistogram = {
1691 render(parent, id, keyedHistogram) {
1692 let outerDiv = document.createElement("div");
1693 outerDiv.className = "keyed-histogram";
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);
1705 parent.appendChild(outerDiv);
1710 var AddonDetails = {
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.
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);
1725 for (let provider in addonDetails) {
1726 let providerSection = document.createElement("caption");
1727 document.l10n.setAttributes(
1729 "about-telemetry-addon-provider",
1730 { addonProvider: provider }
1732 let headingStrings = [
1733 "about-telemetry-addon-table-id",
1734 "about-telemetry-addon-table-details",
1736 let table = GenericTable.render(
1737 explodeObject(addonDetails[provider]),
1740 table.appendChild(providerSection);
1741 addonSection.appendChild(table);
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", {
1754 s.appendChild(heading);
1756 this.renderData(data, s);
1759 let separator = document.createElement("div");
1760 separator.classList.add("clearfix");
1761 div.appendChild(separator);
1766 * Make parent process the first one, content process the second
1767 * then sort processes alphabetically
1769 static processesComparator(a, b) {
1770 if (a === "parent" || (a === "content" && b !== "parent")) {
1772 } else if (b === "parent" || b === "content") {
1785 static renderSection(divName, section, aPayload) {
1786 let div = document.getElementById(divName);
1787 removeAllChildNodes(div);
1790 let hasData = false;
1791 let selectedStore = getSelectedStore();
1793 let payload = aPayload.stores;
1795 let isCurrentPayload = !!payload;
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);
1810 setHasData(section, hasData);
1814 class Scalars extends Section {
1816 * Return data from the current ping
1818 static dataFiltering(payload, selectedStore, process) {
1819 return payload[selectedStore][process].scalars;
1823 * Return data from an archived ping
1825 static archivePingDataFiltering(payload, process) {
1826 return payload.processes[process].scalars;
1829 static renderData(data, div) {
1830 const scalarsHeadings = [
1831 "about-telemetry-names-header",
1832 "about-telemetry-values-header",
1834 let scalarsTable = GenericTable.render(
1835 explodeObject(data),
1838 div.appendChild(scalarsTable);
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.
1845 static render(aPayload) {
1846 const divName = "scalars";
1847 const section = "scalars-section";
1848 this.renderSection(divName, section, aPayload);
1852 class KeyedScalars extends Section {
1854 * Return data from the current ping
1856 static dataFiltering(payload, selectedStore, process) {
1857 return payload[selectedStore][process].keyedScalars;
1861 * Return data from an archived ping
1863 static archivePingDataFiltering(payload, process) {
1864 return payload.processes[process].keyedScalars;
1867 static renderData(data, div) {
1868 const scalarsHeadings = [
1869 "about-telemetry-names-header",
1870 "about-telemetry-values-header",
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]),
1886 container.appendChild(table);
1887 div.appendChild(container);
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.
1895 static render(aPayload) {
1896 const divName = "keyed-scalars";
1897 const section = "keyed-scalars-section";
1898 this.renderSection(divName, section, aPayload);
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.
1908 let eventsDiv = document.getElementById("events");
1909 removeAllChildNodes(eventsDiv);
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",
1918 let payload = aPayload.processes;
1919 let hasData = false;
1921 for (const process of Object.keys(aPayload.processes)) {
1922 let data = aPayload.processes[process].events;
1923 if (data && Object.keys(data).length) {
1925 let s = GenericSubsection.renderSubsectionHeader(
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);
1942 // handle archived ping
1943 for (const process of Object.keys(aPayload.events)) {
1945 if (data && Object.keys(data).length) {
1947 let s = GenericSubsection.renderSubsectionHeader(
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);
1963 setHasData("events-section", hasData);
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
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 + "]"
1981 sectionCategory.classList[aHasData ? "add" : "remove"]("has-data");
1985 * Sets l10n attributes based on the Telemetry Server Owner pref.
1987 function setupServerOwnerBranding() {
1988 let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
1990 [document.getElementById("page-subtitle"), "about-telemetry-page-subtitle"],
1992 for (const [elt, l10nName] of elements) {
1993 document.l10n.setAttributes(elt, l10nName, {
1994 telemetryServerOwner: serverOwner,
2000 * Display the store selector if we are on one
2001 * of the whitelisted sections
2003 function displayStoresSelector(selectedSection) {
2006 "keyed-scalars-section",
2007 "histograms-section",
2008 "keyed-histograms-section",
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);
2027 function adjustSearchState() {
2028 removeSearchSectionTitles();
2029 let selectedSection = document
2030 .querySelector(".category.selected")
2031 .getAttribute("value");
2032 let search = document.getElementById("search");
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")
2043 sectionTitleDiv.remove();
2047 function adjustSection() {
2048 let selectedCategory = document.querySelector(".category.selected");
2049 if (!selectedCategory.classList.contains("has-data")) {
2050 PingPicker._showStructuredPingData();
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(
2061 "about-telemetry-results-for-search",
2062 { searchTerms: title }
2065 sectionTitle.textContent = selectedTitle;
2067 let search = document.getElementById("search");
2068 if (selected.parentElement.id === "category-home") {
2069 document.l10n.setAttributes(
2071 "about-telemetry-filter-all-placeholder"
2074 document.l10n.setAttributes(search, "about-telemetry-filter-placeholder", {
2081 * Change the url according to the current section displayed
2082 * e.g about:telemetry#general-data
2084 function changeUrlPath(selectedSection, subSection) {
2086 let hash = window.location.hash.split("_")[0] + "_" + selectedSection;
2087 window.location.hash = hash;
2089 window.location.hash = selectedSection.replace("-section", "-tab");
2094 * Change the url according to the current search text
2096 function changeUrlSearch(searchText) {
2097 let currentHash = window.location.hash;
2098 let hashWithoutSearch = currentHash.split(Search.HASH_SEARCH)[0];
2101 if (!currentHash && !searchText) {
2104 if (!currentHash.includes(Search.HASH_SEARCH) && hashWithoutSearch) {
2105 hashWithoutSearch += "_";
2109 hashWithoutSearch + Search.HASH_SEARCH + searchText.replace(/ /g, "+");
2110 } else if (hashWithoutSearch) {
2111 hash = hashWithoutSearch.slice(0, hashWithoutSearch.length - 1);
2114 window.location.hash = hash;
2118 * Change the section displayed
2120 function show(selected) {
2121 let selectedValue = selected.getAttribute("value");
2122 if (selectedValue === "raw-json-viewer") {
2123 openJsonInFirefoxJsonViewer(JSON.stringify(gPingData, null, 2));
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");
2135 for (let subsection of subsections) {
2136 subsection.hidden = false;
2140 let current_button = document.querySelector(".category.selected");
2141 if (current_button == selected) {
2144 current_button.classList.remove("selected");
2145 selected.classList.add("selected");
2147 document.querySelectorAll("section").forEach(section => {
2148 section.classList.remove("active");
2150 selected_section.classList.add("active");
2152 adjustHeaderState();
2153 displayStoresSelector(selectedValue);
2154 adjustSearchState();
2155 changeUrlPath(selectedValue);
2158 function showSubSection(selected) {
2162 let current_selection = document.querySelector(
2163 ".category-subsection.selected"
2165 if (current_selection) {
2166 current_selection.classList.remove("selected");
2168 selected.classList.add("selected");
2170 let section = document.getElementById(selected.getAttribute("value"));
2171 section.parentElement.childNodes.forEach(element => {
2172 element.hidden = true;
2174 section.hidden = false;
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
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) {
2199 let search = document.getElementById("search");
2200 search.addEventListener("input", Search.searchHandler);
2203 .getElementById("late-writes-fetch-symbols")
2204 .addEventListener("click", function () {
2209 let lateWrites = gPingData.payload.lateWrites;
2210 let req = new SymbolicationRequest(
2212 LateWritesSingleton.renderHeader,
2213 lateWrites.memoryMap,
2220 .getElementById("late-writes-hide-symbols")
2221 .addEventListener("click", function () {
2226 LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
2230 // Restores the sections states
2231 function urlSectionRestore(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 + "]");
2241 ".category-subsection[value=" + section + "-" + subsection + "]";
2242 let subcategory = document.querySelector(selector);
2243 showSubSection(subcategory);
2249 // Restore sections states and search terms
2250 function urlStateRestore() {
2251 let hash = window.location.hash;
2252 let searchQuery = "";
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];
2259 urlSectionRestore(hash);
2262 let search = document.getElementById("search");
2263 search.value = searchQuery;
2267 function openJsonInFirefoxJsonViewer(json) {
2268 json = unescape(encodeURIComponent(json));
2270 window.open("data:application/json;base64," + btoa(json));
2272 show(document.querySelector(".category[value=raw-payload-section]"));
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
2287 adjustHeaderState();
2291 // Update ping data when async Telemetry init is finished.
2292 Telemetry.asyncFetchTelemetryData(async () => {
2293 await PingPicker.update();
2297 var LateWritesSingleton = {
2298 renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
2299 StackRenderer.renderHeader(
2301 "about-telemetry-late-writes-title",
2302 { lateWriteCount: aIndex + 1 }
2306 renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
2309 lateWrites.stacks &&
2310 lateWrites.stacks.length
2312 setHasData("late-writes-section", hasData);
2317 let stacks = lateWrites.stacks;
2318 let memoryMap = lateWrites.memoryMap;
2319 StackRenderer.renderStacks(
2323 LateWritesSingleton.renderHeader
2328 class HistogramSection extends Section {
2330 * Return data from the current ping
2332 static dataFiltering(payload, selectedStore, process) {
2333 return payload[selectedStore][process].histograms;
2337 * Return data from an archived ping
2339 static archivePingDataFiltering(payload, process) {
2340 if (process === "parent") {
2341 return payload.histograms;
2343 return payload.processes[process].histograms;
2346 static renderData(data, div) {
2347 for (let [hName, hgram] of Object.entries(data)) {
2348 Histogram.render(div, hName, hgram, { unpacked: true });
2352 static render(aPayload) {
2353 const divName = "histograms";
2354 const section = "histograms-section";
2355 this.renderSection(divName, section, aPayload);
2359 class KeyedHistogramSection extends Section {
2361 * Return data from the current ping
2363 static dataFiltering(payload, selectedStore, process) {
2364 return payload[selectedStore][process].keyedHistograms;
2368 * Return data from an archived ping
2370 static archivePingDataFiltering(payload, process) {
2371 if (process === "parent") {
2372 return payload.keyedHistograms;
2374 return payload.processes[process].keyedHistograms;
2377 static renderData(data, div) {
2378 for (let [id, keyed] of Object.entries(data)) {
2379 KeyedHistogram.render(div, id, keyed, { unpacked: true });
2383 static render(aPayload) {
2384 const divName = "keyed-histograms";
2385 const section = "keyed-histograms-section";
2386 this.renderSection(divName, section, aPayload);
2390 var SessionInformation = {
2392 let infoSection = document.getElementById("session-info");
2393 removeAllChildNodes(infoSection);
2395 let hasData = !!Object.keys(aPayload.info).length;
2396 setHasData("session-info-section", hasData);
2399 const table = GenericTable.render(explodeObject(aPayload.info));
2400 infoSection.appendChild(table);
2405 var SimpleMeasurements = {
2407 let simpleSection = document.getElementById("simple-measurements");
2408 removeAllChildNodes(simpleSection);
2410 let simpleMeasurements = this.sortStartupMilestones(
2411 aPayload.simpleMeasurements
2413 let hasData = !!Object.keys(simpleMeasurements).length;
2414 setHasData("simple-measurements-section", hasData);
2417 const table = GenericTable.render(explodeObject(simpleMeasurements));
2418 simpleSection.appendChild(table);
2423 * Helper function for sorting the startup milestones in the Simple Measurements
2424 * section into temporal order.
2426 * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
2427 * @return Sorted measurements
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;
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) {
2449 if (!isKeyAMilestone && isKeyBMilestone) {
2452 // Don't change order of non-startup measurements
2453 if (!isKeyAMilestone && !isKeyBMilestone) {
2457 // If both keys are startup measurements, order them by value
2458 return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
2461 // Insert measurements into a result object in sort-order
2463 for (let key of sortedKeys) {
2464 result[key] = aSimpleMeasurements[key];
2472 * Render stores options
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");
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;
2497 storeSelect.appendChild(option);
2502 * Return the selected store
2504 function getSelectedStore() {
2505 let storeSelect = document.getElementById("stores");
2506 let storeSelectedOption = storeSelect.selectedOptions.item(0);
2508 storeSelectedOption !== null
2509 ? storeSelectedOption.getAttribute("value")
2511 return selectedStore;
2514 function togglePingSections(isMainPing) {
2515 // We always show the sections that are "common" to all pings.
2516 let commonSections = new Set([
2519 "general-data-section",
2520 "environment-data-section",
2524 let elements = document.querySelectorAll(".category");
2525 for (let section of elements) {
2526 if (commonSections.has(section.getAttribute("value"))) {
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);
2533 section.classList.toggle("has-data", isMainPing);
2538 function displayPingData(ping, updatePayloadList = false) {
2541 PingPicker.render();
2542 displayRichPingData(ping, updatePayloadList);
2547 PingPicker._showRawPingData();
2551 function displayRichPingData(ping, updatePayloadList) {
2552 // Update the payload list and store lists
2553 if (updatePayloadList) {
2554 renderStoreList(ping.payload);
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);
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],
2581 // We transformed the actual payload, let's reload the store list if necessary.
2582 if (updatePayloadList) {
2583 renderStoreList(payload);
2587 Events.render(payload);
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);
2616 Events.render(payload);
2618 LateWritesSingleton.renderLateWrites(payload.lateWrites);
2620 // Show simple measurements
2621 SimpleMeasurements.render(payload);
2624 window.addEventListener("load", onLoad);