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.import(
8 "resource://gre/modules/BrowserUtils.jsm"
10 const { TelemetryTimestamps } = ChromeUtils.import(
11 "resource://gre/modules/TelemetryTimestamps.jsm"
13 const { TelemetryController } = ChromeUtils.import(
14 "resource://gre/modules/TelemetryController.jsm"
16 const { TelemetryArchive } = ChromeUtils.import(
17 "resource://gre/modules/TelemetryArchive.jsm"
19 const { TelemetrySend } = ChromeUtils.import(
20 "resource://gre/modules/TelemetrySend.jsm"
23 const { AppConstants } = ChromeUtils.import(
24 "resource://gre/modules/AppConstants.jsm"
26 ChromeUtils.defineModuleGetter(
29 "resource://gre/modules/Preferences.jsm"
31 ChromeUtils.defineModuleGetter(
34 "resource://gre/modules/ObjectUtils.jsm"
37 const Telemetry = Services.telemetry;
39 // Maximum height of a histogram bar (in em for html, in chars for text)
40 const MAX_BAR_HEIGHT = 8;
41 const MAX_BAR_CHARS = 25;
42 const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner";
43 const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
44 const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql";
45 const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl";
46 const DEFAULT_SYMBOL_SERVER_URI =
47 "https://symbolication.services.mozilla.com/symbolicate/v4";
48 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
50 // ms idle before applying the filter (allow uninterrupted typing)
51 const FILTER_IDLE_TIMEOUT = 500;
53 const isWindows = Services.appinfo.OS == "WINNT";
54 const EOL = isWindows ? "\r\n" : "\n";
56 // This is the ping object currently displayed in the page.
59 // Cached value of document's RTL mode
60 var documentRTLMode = "";
63 * Helper function for determining whether the document direction is RTL.
64 * Caches result of check on first invocation.
67 if (!documentRTLMode) {
68 documentRTLMode = window.getComputedStyle(document.body).direction;
70 return documentRTLMode == "rtl";
73 function isFlatArray(obj) {
74 if (!Array.isArray(obj)) {
77 return !obj.some(e => typeof e == "object");
81 * This is a helper function for explodeObject.
83 function flattenObject(obj, map, path, array) {
84 for (let k of Object.keys(obj)) {
85 let newPath = [...path, array ? "[" + k + "]" : k];
87 if (!v || typeof v != "object") {
88 map.set(newPath.join("."), v);
89 } else if (isFlatArray(v)) {
90 map.set(newPath.join("."), "[" + v.join(", ") + "]");
92 flattenObject(v, map, newPath, Array.isArray(v));
98 * This turns a JSON object into a "flat" stringified form.
100 * For an object like {a: "1", b: {c: "2", d: "3"}} it returns a Map of the
101 * form Map(["a","1"], ["b.c", "2"], ["b.d", "3"]).
103 function explodeObject(obj) {
105 flattenObject(obj, map, []);
109 function filterObject(obj, filterOut) {
111 for (let k of Object.keys(obj)) {
112 if (!filterOut.includes(k)) {
120 * This turns a JSON object into a "flat" stringified form, separated into top-level sections.
122 * For an object like:
125 * c: {d: "2", e: {f: "3"}}
127 * it returns a Map of the form:
129 * ["a", Map(["b","1"])],
130 * ["c", Map([["d", "2"], ["e.f", "3"]])]
133 function sectionalizeObject(obj) {
135 for (let k of Object.keys(obj)) {
136 map.set(k, explodeObject(obj[k]));
142 * Obtain the main DOMWindow for the current context.
144 function getMainWindow() {
145 return window.browsingContext.topChromeWindow;
149 * Obtain the DOMWindow that can open a preferences pane.
151 * This is essentially "get the browser chrome window" with the added check
152 * that the supposed browser chrome window is capable of opening a preferences
155 * This may return null if we can't find the browser chrome window.
157 function getMainWindowWithPreferencesPane() {
158 let mainWindow = getMainWindow();
159 if (mainWindow && "openPreferences" in mainWindow) {
166 * Remove all child nodes of a document node.
168 function removeAllChildNodes(node) {
169 while (node.hasChildNodes()) {
170 node.removeChild(node.lastChild);
176 let elements = document.getElementsByClassName("change-data-choices-link");
177 for (let el of elements) {
178 el.parentElement.addEventListener("click", function(event) {
179 if (event.target.localName === "a") {
180 if (AppConstants.platform == "android") {
181 var { EventDispatcher } = ChromeUtils.import(
182 "resource://gre/modules/Messaging.jsm"
184 EventDispatcher.instance.sendRequest({
185 type: "Settings:Show",
186 resource: "preferences_privacy",
189 // Show the data choices preferences on desktop.
190 let mainWindow = getMainWindowWithPreferencesPane();
191 mainWindow.openPreferences("privacy-reports");
199 * Updates the button & text at the top of the page to reflect Telemetry state.
202 let settingsExplanation = document.getElementById("settings-explanation");
203 let extendedEnabled = Services.telemetry.canRecordExtended;
205 let channel = extendedEnabled ? "prerelease" : "release";
206 let uploadcase = TelemetrySend.sendingEnabled() ? "enabled" : "disabled";
208 document.l10n.setAttributes(
210 "about-telemetry-settings-explanation",
211 { channel, uploadcase }
214 this.attachObservers();
219 viewCurrentPingData: null,
220 _archivedPings: null,
224 let pingSourceElements = document.getElementsByName("choose-ping-source");
225 for (let el of pingSourceElements) {
226 el.addEventListener("change", () => this.onPingSourceChanged());
229 let displays = document.getElementsByName("choose-ping-display");
230 for (let el of displays) {
231 el.addEventListener("change", () => this.onPingDisplayChanged());
235 .getElementById("show-subsession-data")
236 .addEventListener("change", () => {
237 this._updateCurrentPingData();
240 document.getElementById("choose-ping-id").addEventListener("change", () => {
241 this._updateArchivedPingData();
244 .getElementById("choose-ping-type")
245 .addEventListener("change", () => {
246 this.filterDisplayedPings();
250 .getElementById("newer-ping")
251 .addEventListener("click", () => this._movePingIndex(-1));
253 .getElementById("older-ping")
254 .addEventListener("click", () => this._movePingIndex(1));
256 let pingPickerNeedHide = false;
257 let pingPicker = document.getElementById("ping-picker");
258 pingPicker.addEventListener(
260 () => (pingPickerNeedHide = false)
262 pingPicker.addEventListener(
264 () => (pingPickerNeedHide = true)
266 document.addEventListener("click", ev => {
267 if (pingPickerNeedHide) {
268 pingPicker.classList.add("hidden");
272 .getElementById("stores")
273 .addEventListener("change", () => displayPingData(gPingData));
274 Array.from(document.querySelectorAll(".change-ping")).forEach(el => {
275 el.addEventListener("click", event => {
276 if (!pingPicker.classList.contains("hidden")) {
277 pingPicker.classList.add("hidden");
279 pingPicker.classList.remove("hidden");
280 event.stopPropagation();
286 onPingSourceChanged() {
290 onPingDisplayChanged() {
295 // Display the type and controls if the ping is not current
296 let pingDate = document.getElementById("ping-date");
297 let pingType = document.getElementById("ping-type");
298 let controls = document.getElementById("controls");
299 let pingExplanation = document.getElementById("ping-explanation");
301 if (!this.viewCurrentPingData) {
302 let pingName = this._getSelectedPingName();
303 // Change sidebar heading text.
304 pingDate.textContent = pingName;
305 pingDate.setAttribute("title", pingName);
306 let pingTypeText = this._getSelectedPingType();
307 controls.classList.remove("hidden");
308 pingType.textContent = pingTypeText;
309 document.l10n.setAttributes(
311 "about-telemetry-ping-details",
312 { timestamp: pingTypeText, name: pingName }
315 // Change sidebar heading text.
316 controls.classList.add("hidden");
317 document.l10n.setAttributes(
319 "about-telemetry-current-data-sidebar"
321 // Change home page text.
322 document.l10n.setAttributes(
324 "about-telemetry-data-details-current"
328 GenericSubsection.deleteAllSubSections();
332 let viewCurrent = document.getElementById("ping-source-current").checked;
333 let currentChanged = viewCurrent !== this.viewCurrentPingData;
334 this.viewCurrentPingData = viewCurrent;
336 // If we have no archived pings, disable the ping archive selection.
337 // This can happen on new profiles or if the ping archive is disabled.
338 let archivedPingList = await TelemetryArchive.promiseArchivedPingList();
339 let sourceArchived = document.getElementById("ping-source-archive");
340 let sourceArchivedContainer = document.getElementById(
341 "ping-source-archive-container"
343 let archivedDisabled = !archivedPingList.length;
344 sourceArchived.disabled = archivedDisabled;
345 sourceArchivedContainer.classList.toggle("disabled", archivedDisabled);
347 if (currentChanged) {
348 if (this.viewCurrentPingData) {
349 document.getElementById("current-ping-picker").hidden = false;
350 document.getElementById("archived-ping-picker").hidden = true;
351 this._updateCurrentPingData();
353 document.getElementById("current-ping-picker").hidden = true;
354 await this._updateArchivedPingList(archivedPingList);
355 document.getElementById("archived-ping-picker").hidden = false;
360 _updateCurrentPingData() {
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 // augment "current ping payload" with origin telemetry
420 const originSnapshot = Telemetry.getOriginSnapshot(false /* clear */);
421 ping.payload.origins = originSnapshot;
423 displayPingData(ping, true);
426 _updateArchivedPingData() {
427 let id = this._getSelectedPingId();
428 let res = Promise.resolve();
430 res = TelemetryArchive.promiseArchivedPingById(id).then(ping =>
431 displayPingData(ping, true)
437 async _updateArchivedPingList(pingList) {
438 // The archived ping list is sorted in ascending timestamp order,
439 // but descending is more practical for the operations we do here.
441 this._archivedPings = pingList;
442 // Render the archive data.
443 this._renderPingList();
444 // Update the displayed ping.
445 await this._updateArchivedPingData();
449 let pingSelector = document.getElementById("choose-ping-id");
450 Array.from(pingSelector.children).forEach(child =>
451 removeAllChildNodes(child)
454 let pingTypes = new Set();
455 pingTypes.add(this.TYPE_ALL);
457 const today = new Date();
458 today.setHours(0, 0, 0, 0);
459 const yesterday = new Date(today);
460 yesterday.setDate(today.getDate() - 1);
462 for (let p of this._archivedPings) {
463 pingTypes.add(p.type);
464 const pingDate = new Date(p.timestampCreated);
465 const datetimeText = new Services.intl.DateTimeFormat(undefined, {
469 const pingName = `${datetimeText}, ${p.type}`;
471 let option = document.createElement("option");
472 let content = document.createTextNode(pingName);
473 option.appendChild(content);
474 option.setAttribute("value", p.id);
475 option.dataset.type = p.type;
476 option.dataset.date = datetimeText;
478 pingDate.setHours(0, 0, 0, 0);
479 if (pingDate.getTime() === today.getTime()) {
480 pingSelector.children[0].appendChild(option);
481 } else if (pingDate.getTime() === yesterday.getTime()) {
482 pingSelector.children[1].appendChild(option);
484 pingSelector.children[2].appendChild(option);
487 this._renderPingTypes(pingTypes);
490 _renderPingTypes(pingTypes) {
491 let pingTypeSelector = document.getElementById("choose-ping-type");
492 removeAllChildNodes(pingTypeSelector);
493 pingTypes.forEach(type => {
494 let option = document.createElement("option");
495 option.appendChild(document.createTextNode(type));
496 option.setAttribute("value", type);
497 pingTypeSelector.appendChild(option);
501 _movePingIndex(offset) {
502 if (this.viewCurrentPingData) {
505 let typeSelector = document.getElementById("choose-ping-type");
506 let type = typeSelector.selectedOptions.item(0).value;
508 let id = this._getSelectedPingId();
509 let index = this._archivedPings.findIndex(p => p.id == id);
510 let newIndex = Math.min(
511 Math.max(0, index + offset),
512 this._archivedPings.length - 1
517 pingList = this._archivedPings.slice(newIndex);
519 pingList = this._archivedPings.slice(0, newIndex);
523 let ping = pingList.find(p => {
524 return type == this.TYPE_ALL || p.type == type;
528 this.selectPing(ping);
529 this._updateArchivedPingData();
534 let pingSelector = document.getElementById("choose-ping-id");
535 // Use some() to break if we find the ping.
536 Array.from(pingSelector.children).some(group => {
537 return Array.from(group.children).some(option => {
538 if (option.value == ping.id) {
539 option.selected = true;
547 filterDisplayedPings() {
548 let pingSelector = document.getElementById("choose-ping-id");
549 let typeSelector = document.getElementById("choose-ping-type");
550 let type = typeSelector.selectedOptions.item(0).value;
552 Array.from(pingSelector.children).forEach(group => {
553 Array.from(group.children).forEach(option => {
554 if (first && option.dataset.type == type) {
555 option.selected = true;
558 option.hidden = type != this.TYPE_ALL && option.dataset.type != type;
559 // Arrow keys should only iterate over visible options
560 option.disabled = option.hidden;
563 this._updateArchivedPingData();
566 _getSelectedPingName() {
567 let pingSelector = document.getElementById("choose-ping-id");
568 let selected = pingSelector.selectedOptions.item(0);
569 return selected.dataset.date;
572 _getSelectedPingType() {
573 let pingSelector = document.getElementById("choose-ping-id");
574 let selected = pingSelector.selectedOptions.item(0);
575 return selected.dataset.type;
578 _getSelectedPingId() {
579 let pingSelector = document.getElementById("choose-ping-id");
580 let selected = pingSelector.selectedOptions.item(0);
581 return selected.getAttribute("value");
585 show(document.getElementById("category-raw"));
588 _showStructuredPingData() {
589 show(document.getElementById("category-home"));
595 * Renders the general data
598 setHasData("general-data-section", true);
599 let generalDataSection = document.getElementById("general-data");
600 removeAllChildNodes(generalDataSection);
603 "about-telemetry-names-header",
604 "about-telemetry-values-header",
607 // The payload & environment parts are handled by other renderers.
608 let ignoreSections = ["payload", "environment"];
609 let data = explodeObject(filterObject(aPing, ignoreSections));
611 const table = GenericTable.render(data, headings);
612 generalDataSection.appendChild(table);
616 var EnvironmentData = {
618 * Renders the environment data
621 let dataDiv = document.getElementById("environment-data");
622 removeAllChildNodes(dataDiv);
623 const hasData = !!ping.environment;
624 setHasData("environment-data-section", hasData);
629 let ignore = ["addons"];
630 let env = filterObject(ping.environment, ignore);
631 let sections = sectionalizeObject(env);
632 GenericSubsection.render(sections, dataDiv, "environment-data-section");
634 // We use specialized rendering here to make the addon and plugin listings
636 this.createAddonSection(dataDiv, ping);
639 renderAddonsObject(addonObj, addonSection, sectionTitle) {
640 let table = document.createElement("table");
641 table.setAttribute("id", sectionTitle);
642 this.appendAddonSubsectionTitle(sectionTitle, table);
644 for (let id of Object.keys(addonObj)) {
645 let addon = addonObj[id];
646 this.appendHeadingName(table, addon.name || id);
647 this.appendAddonID(table, id);
648 let data = explodeObject(addon);
650 for (let [key, value] of data) {
651 this.appendRow(table, key, value);
655 addonSection.appendChild(table);
658 renderKeyValueObject(addonObj, addonSection, sectionTitle) {
659 let data = explodeObject(addonObj);
660 let table = GenericTable.render(data);
661 table.setAttribute("class", sectionTitle);
662 this.appendAddonSubsectionTitle(sectionTitle, table);
663 addonSection.appendChild(table);
666 appendAddonID(table, addonID) {
667 this.appendRow(table, "id", addonID);
670 appendHeadingName(table, name) {
671 let headings = document.createElement("tr");
672 this.appendColumn(headings, "th", name);
673 headings.cells[0].colSpan = 2;
674 table.appendChild(headings);
677 appendAddonSubsectionTitle(section, table) {
678 let caption = document.createElement("caption");
679 caption.appendChild(document.createTextNode(section));
680 table.appendChild(caption);
683 createAddonSection(dataDiv, ping) {
684 if (!ping || !("environment" in ping) || !("addons" in ping.environment)) {
687 let addonSection = document.createElement("div");
688 addonSection.setAttribute("class", "subsection-data subdata");
689 let addons = ping.environment.addons;
690 this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
691 this.renderKeyValueObject(addons.theme, addonSection, "theme");
692 this.renderAddonsObject(
693 addons.activeGMPlugins,
698 let hasAddonData = !!Object.keys(ping.environment.addons).length;
699 let s = GenericSubsection.renderSubsectionHeader(
702 "environment-data-section"
704 s.appendChild(addonSection);
705 dataDiv.appendChild(s);
708 appendRow(table, id, value) {
709 let row = document.createElement("tr");
711 this.appendColumn(row, "td", id);
712 this.appendColumn(row, "td", value);
713 table.appendChild(row);
716 * Helper function for appending a column to the data table.
718 * @param aRowElement Parent row element
719 * @param aColType Column's tag name
720 * @param aColText Column contents
722 appendColumn(aRowElement, aColType, aColText) {
723 let colElement = document.createElement(aColType);
724 let colTextElement = document.createTextNode(aColText);
725 colElement.appendChild(colTextElement);
726 aRowElement.appendChild(colElement);
732 * Render slow SQL statistics
734 render: function SlowSQL_render(aPing) {
735 // We can add the debug SQL data to the current ping later.
736 // However, we need to be careful to never send that debug data
737 // out due to privacy concerns.
738 // We want to show the actual ping data for archived pings,
739 // so skip this there.
742 PingPicker.viewCurrentPingData &&
743 Preferences.get(PREF_DEBUG_SLOW_SQL, false);
744 let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
746 setHasData("slow-sql-section", false);
750 let { mainThread, otherThreads } = debugSlowSql
751 ? Telemetry.debugSlowSQL
752 : aPing.payload.slowSQL;
754 let mainThreadCount = Object.keys(mainThread).length;
755 let otherThreadCount = Object.keys(otherThreads).length;
756 if (mainThreadCount == 0 && otherThreadCount == 0) {
757 setHasData("slow-sql-section", false);
761 setHasData("slow-sql-section", true);
763 document.getElementById("sql-warning").hidden = false;
766 let slowSqlDiv = document.getElementById("slow-sql-tables");
767 removeAllChildNodes(slowSqlDiv);
770 if (mainThreadCount > 0) {
771 let table = document.createElement("table");
772 this.renderTableHeader(table, "main");
773 this.renderTable(table, mainThread);
774 slowSqlDiv.appendChild(table);
778 if (otherThreadCount > 0) {
779 let table = document.createElement("table");
780 this.renderTableHeader(table, "other");
781 this.renderTable(table, otherThreads);
782 slowSqlDiv.appendChild(table);
787 * Creates a header row for a Slow SQL table
788 * Tabs & newlines added to cells to make it easier to copy-paste.
790 * @param aTable Parent table element
791 * @param aTitle Table's title
793 renderTableHeader: function SlowSQL_renderTableHeader(aTable, threadType) {
794 let caption = document.createElement("caption");
795 if (threadType == "main") {
796 document.l10n.setAttributes(caption, "about-telemetry-slow-sql-main");
799 if (threadType == "other") {
800 document.l10n.setAttributes(caption, "about-telemetry-slow-sql-other");
802 aTable.appendChild(caption);
804 let headings = document.createElement("tr");
805 document.l10n.setAttributes(
806 this.appendColumn(headings, "th"),
807 "about-telemetry-slow-sql-hits"
809 document.l10n.setAttributes(
810 this.appendColumn(headings, "th"),
811 "about-telemetry-slow-sql-average"
813 document.l10n.setAttributes(
814 this.appendColumn(headings, "th"),
815 "about-telemetry-slow-sql-statement"
817 aTable.appendChild(headings);
821 * Fills out the table body
822 * Tabs & newlines added to cells to make it easier to copy-paste.
824 * @param aTable Parent table element
825 * @param aSql SQL stats object
827 renderTable: function SlowSQL_renderTable(aTable, aSql) {
828 for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) {
829 let averageTime = totalTime / hitCount;
831 let sqlRow = document.createElement("tr");
833 this.appendColumn(sqlRow, "td", hitCount + "\t");
834 this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
835 this.appendColumn(sqlRow, "td", sql + "\n");
837 aTable.appendChild(sqlRow);
842 * Helper function for appending a column to a Slow SQL table.
844 * @param aRowElement Parent row element
845 * @param aColType Column's tag name
846 * @param aColText Column contents
848 appendColumn: function SlowSQL_appendColumn(
853 let colElement = document.createElement(aColType);
855 let colTextElement = document.createTextNode(aColText);
856 colElement.appendChild(colTextElement);
858 aRowElement.appendChild(colElement);
863 var StackRenderer = {
865 * Outputs the memory map associated with this hang report
867 * @param aDiv Output div
869 renderMemoryMap: async function StackRenderer_renderMemoryMap(
873 let memoryMapTitleElement = document.createElement("span");
874 document.l10n.setAttributes(
875 memoryMapTitleElement,
876 "about-telemetry-memory-map-title"
878 aDiv.appendChild(memoryMapTitleElement);
879 aDiv.appendChild(document.createElement("br"));
881 for (let currentModule of memoryMap) {
882 aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
883 aDiv.appendChild(document.createElement("br"));
886 aDiv.appendChild(document.createElement("br"));
890 * Outputs the raw PCs from the hang's stack
892 * @param aDiv Output div
893 * @param aStack Array of PCs from the hang stack
895 renderStack: function StackRenderer_renderStack(aDiv, aStack) {
896 let stackTitleElement = document.createElement("span");
897 document.l10n.setAttributes(
899 "about-telemetry-stack-title"
901 aDiv.appendChild(stackTitleElement);
902 let stackText = " " + aStack.join(" ");
903 aDiv.appendChild(document.createTextNode(stackText));
905 aDiv.appendChild(document.createElement("br"));
906 aDiv.appendChild(document.createElement("br"));
908 renderStacks: function StackRenderer_renderStacks(
914 let div = document.getElementById(aPrefix);
915 removeAllChildNodes(div);
917 let fetchE = document.getElementById(aPrefix + "-fetch-symbols");
919 fetchE.hidden = false;
921 let hideE = document.getElementById(aPrefix + "-hide-symbols");
926 if (!aStacks.length) {
930 setHasData(aPrefix + "-section", true);
932 this.renderMemoryMap(div, aMemoryMap);
934 for (let i = 0; i < aStacks.length; ++i) {
935 let stack = aStacks[i];
937 this.renderStack(div, stack);
942 * Renders the title of the stack: e.g. "Late Write #1" or
943 * "Hang Report #1 (6 seconds)".
945 * @param aDivId The id of the div to append the header to.
946 * @param aL10nId The l10n id of the message to use for the title.
947 * @param aL10nArgs The l10n args for the provided message id.
949 renderHeader: function StackRenderer_renderHeader(
954 let div = document.getElementById(aDivId);
956 let titleElement = document.createElement("span");
957 titleElement.className = "stack-title";
959 document.l10n.setAttributes(titleElement, aL10nId, aL10nArgs);
961 div.appendChild(titleElement);
962 div.appendChild(document.createElement("br"));
966 var RawPayloadData = {
968 * Renders the raw pyaload.
971 setHasData("raw-payload-section", true);
972 let pre = document.getElementById("raw-payload-data");
973 pre.textContent = JSON.stringify(aPing.payload, null, 2);
978 .getElementById("payload-json-viewer")
979 .addEventListener("click", e => {
980 openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2));
985 function SymbolicationRequest(
992 this.prefix = aPrefix;
993 this.renderHeader = aRenderHeader;
994 this.memoryMap = aMemoryMap;
995 this.stacks = aStacks;
996 this.durations = aDurations;
999 * A callback for onreadystatechange. It replaces the numeric stack with
1000 * the symbolicated one returned by the symbolication server.
1002 SymbolicationRequest.prototype.handleSymbolResponse = async function SymbolicationRequest_handleSymbolResponse() {
1003 if (this.symbolRequest.readyState != 4) {
1007 let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
1008 fetchElement.hidden = true;
1009 let hideElement = document.getElementById(this.prefix + "-hide-symbols");
1010 hideElement.hidden = false;
1011 let div = document.getElementById(this.prefix);
1012 removeAllChildNodes(div);
1013 let errorMessage = await document.l10n.formatValue(
1014 "about-telemetry-error-fetching-symbols"
1017 if (this.symbolRequest.status != 200) {
1018 div.appendChild(document.createTextNode(errorMessage));
1022 let jsonResponse = {};
1024 jsonResponse = JSON.parse(this.symbolRequest.responseText);
1026 div.appendChild(document.createTextNode(errorMessage));
1030 for (let i = 0; i < jsonResponse.length; ++i) {
1031 let stack = jsonResponse[i];
1032 this.renderHeader(i, this.durations);
1034 for (let symbol of stack) {
1035 div.appendChild(document.createTextNode(symbol));
1036 div.appendChild(document.createElement("br"));
1038 div.appendChild(document.createElement("br"));
1042 * Send a request to the symbolication server to symbolicate this stack.
1044 SymbolicationRequest.prototype.fetchSymbols = function SymbolicationRequest_fetchSymbols() {
1045 let symbolServerURI = Preferences.get(
1046 PREF_SYMBOL_SERVER_URI,
1047 DEFAULT_SYMBOL_SERVER_URI
1049 let request = { memoryMap: this.memoryMap, stacks: this.stacks, version: 3 };
1050 let requestJSON = JSON.stringify(request);
1052 this.symbolRequest = new XMLHttpRequest();
1053 this.symbolRequest.open("POST", symbolServerURI, true);
1054 this.symbolRequest.setRequestHeader("Content-type", "application/json");
1055 this.symbolRequest.setRequestHeader("Content-length", requestJSON.length);
1056 this.symbolRequest.setRequestHeader("Connection", "close");
1057 this.symbolRequest.onreadystatechange = this.handleSymbolResponse.bind(this);
1058 this.symbolRequest.send(requestJSON);
1063 * Renders a single Telemetry histogram
1065 * @param aParent Parent element
1066 * @param aName Histogram name
1067 * @param aHgram Histogram information
1068 * @param aOptions Object with render options
1069 * * exponential: bars follow logarithmic scale
1071 render: function Histogram_render(aParent, aName, aHgram, aOptions) {
1072 let options = aOptions || {};
1073 let hgram = this.processHistogram(aHgram, aName);
1075 let outerDiv = document.createElement("div");
1076 outerDiv.className = "histogram";
1077 outerDiv.id = aName;
1079 let divTitle = document.createElement("div");
1080 divTitle.classList.add("histogram-title");
1081 divTitle.appendChild(document.createTextNode(aName));
1082 outerDiv.appendChild(divTitle);
1084 let divStats = document.createElement("div");
1085 divStats.classList.add("histogram-stats");
1087 let histogramStatsArgs = {
1088 sampleCount: hgram.sample_count,
1089 prettyAverage: hgram.pretty_average,
1093 document.l10n.setAttributes(
1095 "about-telemetry-histogram-stats",
1100 hgram.values.reverse();
1103 let textData = this.renderValues(outerDiv, hgram, options);
1105 // The 'Copy' button contains the textual data, copied to clipboard on click
1106 let copyButton = document.createElement("button");
1107 copyButton.className = "copy-node";
1108 document.l10n.setAttributes(copyButton, "about-telemetry-histogram-copy");
1110 copyButton.addEventListener("click", async function() {
1111 let divStatsString = await document.l10n.formatValue(
1112 "about-telemetry-histogram-stats",
1115 copyButton.histogramText =
1116 aName + EOL + divStatsString + EOL + EOL + textData;
1117 Cc["@mozilla.org/widget/clipboardhelper;1"]
1118 .getService(Ci.nsIClipboardHelper)
1119 .copyString(this.histogramText);
1121 outerDiv.appendChild(copyButton);
1123 aParent.appendChild(outerDiv);
1127 processHistogram(aHgram, aName) {
1128 const values = Object.keys(aHgram.values).map(k => aHgram.values[k]);
1129 if (!values.length) {
1130 // If we have no values collected for this histogram, just return
1131 // zero values so we still render it.
1141 const sample_count = values.reduceRight((a, b) => a + b);
1142 const average = Math.round((aHgram.sum * 10) / sample_count) / 10;
1143 const max_value = Math.max(...values);
1145 const labelledValues = Object.keys(aHgram.values).map(k => [
1151 values: labelledValues,
1152 pretty_average: average,
1162 * Return a non-negative, logarithmic representation of a non-negative number.
1163 * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
1165 * @param aNumber Non-negative number
1167 getLogValue(aNumber) {
1168 return Math.max(0, Math.log10(aNumber) + 1);
1172 * Create histogram HTML bars, also returns a textual representation
1173 * Both aMaxValue and aSumValues must be positive.
1174 * Values are assumed to use 0 as baseline.
1176 * @param aDiv Outer parent div
1177 * @param aHgram The histogram data
1178 * @param aOptions Object with render options (@see #render)
1180 renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
1182 // If the last label is not the longest string, alignment will break a little
1184 if (aHgram.values.length) {
1185 labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
1187 let maxBarValue = aOptions.exponential
1188 ? this.getLogValue(aHgram.max)
1191 for (let [label, value] of aHgram.values) {
1192 label = String(label);
1193 let barValue = aOptions.exponential ? this.getLogValue(value) : value;
1195 // Create a text representation: <right-aligned-label> |<bar-of-#><value> <percentage>
1198 " ".repeat(Math.max(0, labelPadTo - label.length)) +
1199 label + // Right-aligned label
1201 "#".repeat(Math.round((MAX_BAR_CHARS * barValue) / maxBarValue)) + // Bar
1205 Math.round((100 * value) / aHgram.sample_count) +
1208 // Construct the HTML labels + bars
1210 Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
1211 let aboveEm = MAX_BAR_HEIGHT - belowEm;
1213 let barDiv = document.createElement("div");
1214 barDiv.className = "bar";
1215 barDiv.style.paddingTop = aboveEm + "em";
1217 // Add value label or an nbsp if no value
1218 barDiv.appendChild(document.createTextNode(value ? value : "\u00A0"));
1220 // Create the blue bar
1221 let bar = document.createElement("div");
1222 bar.className = "bar-inner";
1223 bar.style.height = belowEm + "em";
1224 barDiv.appendChild(bar);
1226 // Add a special class to move the text down to prevent text overlap
1227 if (label.length > 3) {
1228 bar.classList.add("long-label");
1231 barDiv.appendChild(document.createTextNode(label));
1233 aDiv.appendChild(barDiv);
1236 return text.substr(EOL.length); // Trim the EOL before the first line
1241 HASH_SEARCH: "search=",
1243 // A list of ids of sections that do not support search.
1244 blacklist: ["late-writes-section", "raw-payload-section"],
1246 // Pass if: all non-empty array items match (case-sensitive)
1247 isPassText(subject, filter) {
1248 for (let item of filter) {
1249 if (item.length && !subject.includes(item)) {
1250 return false; // mismatch and not a spurious space
1256 isPassRegex(subject, filter) {
1257 return filter.test(subject);
1260 chooseFilter(filterText) {
1261 let filter = filterText.toString();
1262 // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
1263 let isPassFunc; // filter function, set once, then applied to all elements
1264 filter = filter.trim();
1265 if (filter[0] != "/") {
1266 // Plain text: case insensitive, AND if multi-string
1267 isPassFunc = this.isPassText;
1268 filter = filter.toLowerCase().split(" ");
1270 isPassFunc = this.isPassRegex;
1271 var r = filter.match(/^\/(.*)\/(i?)$/);
1273 filter = RegExp(r[1], r[2]);
1275 // Incomplete or bad RegExp - always no match
1276 isPassFunc = function() {
1281 return [isPassFunc, filter];
1284 filterTextRows(table, filterText) {
1285 let [isPassFunc, filter] = this.chooseFilter(filterText);
1286 let allElementHidden = true;
1288 let needLowerCase = isPassFunc === this.isPassText;
1289 let elements = table.rows;
1290 for (let element of elements) {
1291 if (element.firstChild.nodeName == "th") {
1294 for (let cell of element.children) {
1295 let subject = needLowerCase
1296 ? cell.textContent.toLowerCase()
1298 element.hidden = !isPassFunc(subject, filter);
1299 if (!element.hidden) {
1300 if (allElementHidden) {
1301 allElementHidden = false;
1303 // Don't need to check the rest of this row.
1308 // Unhide the first row:
1309 if (!allElementHidden) {
1310 table.rows[0].hidden = false;
1312 return allElementHidden;
1315 filterElements(elements, filterText) {
1316 let [isPassFunc, filter] = this.chooseFilter(filterText);
1317 let allElementHidden = true;
1319 let needLowerCase = isPassFunc === this.isPassText;
1320 for (let element of elements) {
1321 let subject = needLowerCase ? element.id.toLowerCase() : element.id;
1322 element.hidden = !isPassFunc(subject, filter);
1323 if (allElementHidden && !element.hidden) {
1324 allElementHidden = false;
1327 return allElementHidden;
1330 filterKeyedElements(keyedElements, filterText) {
1331 let [isPassFunc, filter] = this.chooseFilter(filterText);
1332 let allElementsHidden = true;
1334 let needLowerCase = isPassFunc === this.isPassText;
1335 keyedElements.forEach(keyedElement => {
1336 let subject = needLowerCase
1337 ? keyedElement.key.id.toLowerCase()
1338 : keyedElement.key.id;
1339 if (!isPassFunc(subject, filter)) {
1340 // If the keyedHistogram's name is not matched
1341 let allKeyedElementsHidden = true;
1342 for (let element of keyedElement.datas) {
1343 let subject = needLowerCase ? element.id.toLowerCase() : element.id;
1344 let match = isPassFunc(subject, filter);
1345 element.hidden = !match;
1347 allKeyedElementsHidden = false;
1350 if (allElementsHidden && !allKeyedElementsHidden) {
1351 allElementsHidden = false;
1353 keyedElement.key.hidden = allKeyedElementsHidden;
1355 // If the keyedHistogram's name is matched
1356 allElementsHidden = false;
1357 keyedElement.key.hidden = false;
1358 for (let element of keyedElement.datas) {
1359 element.hidden = false;
1363 return allElementsHidden;
1367 if (this.idleTimeout) {
1368 clearTimeout(this.idleTimeout);
1370 this.idleTimeout = setTimeout(
1371 () => Search.search(e.target.value),
1376 search(text, sectionParam = null) {
1377 let section = sectionParam;
1379 let sectionId = document
1380 .querySelector(".category.selected")
1381 .getAttribute("value");
1382 section = document.getElementById(sectionId);
1384 if (Search.blacklist.includes(section.id)) {
1387 let noSearchResults = true;
1388 // In the home section, we search all other sections:
1389 if (section.id === "home-section") {
1390 return this.homeSearch(text);
1393 if (section.id === "histograms-section") {
1394 let histograms = section.getElementsByClassName("histogram");
1395 noSearchResults = this.filterElements(histograms, text);
1396 } else if (section.id === "keyed-histograms-section") {
1397 let keyedElements = [];
1398 let keyedHistograms = section.getElementsByClassName("keyed-histogram");
1399 for (let key of keyedHistograms) {
1400 let datas = key.getElementsByClassName("histogram");
1401 keyedElements.push({ key, datas });
1403 noSearchResults = this.filterKeyedElements(keyedElements, text);
1404 } else if (section.id === "keyed-scalars-section") {
1405 let keyedElements = [];
1406 let keyedScalars = section.getElementsByClassName("keyed-scalar");
1407 for (let key of keyedScalars) {
1408 let datas = key.querySelector("table").rows;
1409 keyedElements.push({ key, datas });
1411 noSearchResults = this.filterKeyedElements(keyedElements, text);
1412 } else if (section.matches(".text-search")) {
1413 let tables = section.querySelectorAll("table");
1414 for (let table of tables) {
1415 // If we unhide anything, flip noSearchResults to
1416 // false so we don't show the "no results" bits.
1417 if (!this.filterTextRows(table, text)) {
1418 noSearchResults = false;
1421 } else if (section.querySelector(".sub-section")) {
1422 let keyedSubSections = [];
1423 let subsections = section.querySelectorAll(".sub-section");
1424 for (let section of subsections) {
1425 let datas = section.querySelector("table").rows;
1426 keyedSubSections.push({ key: section, datas });
1428 noSearchResults = this.filterKeyedElements(keyedSubSections, text);
1430 let tables = section.querySelectorAll("table");
1431 for (let table of tables) {
1432 noSearchResults = this.filterElements(table.rows, text);
1433 if (table.caption) {
1434 table.caption.hidden = noSearchResults;
1439 changeUrlSearch(text);
1441 if (!sectionParam) {
1442 // If we are not searching in all section.
1443 this.updateNoResults(text, noSearchResults);
1445 return noSearchResults;
1448 updateNoResults(text, noSearchResults) {
1450 .getElementById("no-search-results")
1451 .classList.toggle("hidden", !noSearchResults);
1452 if (noSearchResults) {
1453 let section = document.querySelector(".category.selected > span");
1454 let searchResultsText = document.getElementById("no-search-results-text");
1455 if (section.parentElement.id === "category-home") {
1456 document.l10n.setAttributes(
1458 "about-telemetry-no-search-results-all",
1459 { searchTerms: text }
1462 let sectionName = section.textContent.trim();
1464 ? document.l10n.setAttributes(
1466 "about-telemetry-no-data-to-display",
1469 : document.l10n.setAttributes(
1471 "about-telemetry-no-search-results",
1472 { sectionName, currentSearchText: text }
1479 document.getElementById("main").classList.remove("search");
1480 document.getElementById("no-search-results").classList.add("hidden");
1481 adjustHeaderState();
1482 Array.from(document.querySelectorAll("section")).forEach(section => {
1483 section.classList.toggle("active", section.id == "home-section");
1488 changeUrlSearch(text);
1489 removeSearchSectionTitles();
1494 document.getElementById("main").classList.add("search");
1495 adjustHeaderState(text);
1496 let noSearchResults = true;
1497 Array.from(document.querySelectorAll("section")).forEach(section => {
1498 if (section.id == "home-section" || section.id == "raw-payload-section") {
1499 section.classList.remove("active");
1502 section.classList.add("active");
1503 let sectionHidden = this.search(text, section);
1504 if (!sectionHidden) {
1505 let sectionTitle = document.querySelector(
1506 `.category[value="${section.id}"] .category-name`
1508 let sectionDataDiv = document.querySelector(
1509 `#${section.id}.has-data.active .data`
1511 let titleDiv = document.createElement("h1");
1512 titleDiv.classList.add("data", "search-section-title");
1513 titleDiv.textContent = sectionTitle;
1514 section.insertBefore(titleDiv, sectionDataDiv);
1515 noSearchResults = false;
1517 // Hide all subsections if the section is hidden
1518 let subsections = section.querySelectorAll(".sub-section");
1519 for (let subsection of subsections) {
1520 subsection.hidden = true;
1524 this.updateNoResults(text, noSearchResults);
1529 * Helper function to render JS objects with white space between top level elements
1530 * so that they look better in the browser
1531 * @param aObject JavaScript object or array to render
1534 function RenderObject(aObject) {
1536 if (Array.isArray(aObject)) {
1537 if (!aObject.length) {
1540 output = "[" + JSON.stringify(aObject[0]);
1541 for (let i = 1; i < aObject.length; i++) {
1542 output += ", " + JSON.stringify(aObject[i]);
1544 return output + "]";
1546 let keys = Object.keys(aObject);
1550 output = '{"' + keys[0] + '":\u00A0' + JSON.stringify(aObject[keys[0]]);
1551 for (let i = 1; i < keys.length; i++) {
1552 output += ', "' + keys[i] + '":\u00A0' + JSON.stringify(aObject[keys[i]]);
1554 return output + "}";
1557 var GenericSubsection = {
1558 addSubSectionToSidebar(id, title) {
1559 let category = document.querySelector("#categories > [value=" + id + "]");
1560 category.classList.add("has-subsection");
1561 let subCategory = document.createElement("div");
1562 subCategory.classList.add("category-subsection");
1563 subCategory.setAttribute("value", id + "-" + title);
1564 subCategory.addEventListener("click", ev => {
1565 let section = ev.target;
1566 showSubSection(section);
1568 subCategory.appendChild(document.createTextNode(title));
1569 category.appendChild(subCategory);
1572 render(data, dataDiv, sectionID) {
1573 for (let [title, sectionData] of data) {
1574 let hasData = sectionData.size > 0;
1575 let s = this.renderSubsectionHeader(title, hasData, sectionID);
1576 s.appendChild(this.renderSubsectionData(title, sectionData));
1577 dataDiv.appendChild(s);
1581 renderSubsectionHeader(title, hasData, sectionID) {
1582 this.addSubSectionToSidebar(sectionID, title);
1583 let section = document.createElement("div");
1584 section.setAttribute("id", sectionID + "-" + title);
1585 section.classList.add("sub-section");
1587 section.classList.add("has-subdata");
1592 renderSubsectionData(title, data) {
1593 // Create data container
1594 let dataDiv = document.createElement("div");
1595 dataDiv.setAttribute("class", "subsection-data subdata");
1596 // Instanciate the data
1597 let table = GenericTable.render(data);
1598 let caption = document.createElement("caption");
1599 caption.textContent = title;
1600 table.appendChild(caption);
1601 dataDiv.appendChild(table);
1606 deleteAllSubSections() {
1607 let subsections = document.querySelectorAll(".category-subsection");
1608 subsections.forEach(el => {
1609 el.parentElement.removeChild(el);
1614 var GenericTable = {
1615 // Returns a table with key and value headers
1617 return ["about-telemetry-keys-header", "about-telemetry-values-header"];
1621 * Returns a n-column table.
1622 * @param rows An array of arrays, each containing data to render
1624 * @param headings The column header strings.
1626 render(rows, headings = this.defaultHeadings()) {
1627 let table = document.createElement("table");
1628 this.renderHeader(table, headings);
1629 this.renderBody(table, rows);
1634 * Create the table header.
1635 * Tabs & newlines added to cells to make it easier to copy-paste.
1637 * @param table Table element
1638 * @param headings Array of column header strings.
1640 renderHeader(table, headings) {
1641 let headerRow = document.createElement("tr");
1642 table.appendChild(headerRow);
1644 for (let i = 0; i < headings.length; ++i) {
1645 let column = document.createElement("th");
1646 document.l10n.setAttributes(column, headings[i]);
1647 headerRow.appendChild(column);
1652 * Create the table body
1653 * Tabs & newlines added to cells to make it easier to copy-paste.
1655 * @param table Table element
1656 * @param rows An array of arrays, each containing data to render
1659 renderBody(table, rows) {
1660 for (let row of rows) {
1661 row = row.map(value => {
1662 // use .valueOf() to unbox Number, String, etc. objects
1665 typeof value == "object" &&
1666 typeof value.valueOf() == "object"
1668 return RenderObject(value);
1673 let newRow = document.createElement("tr");
1675 table.appendChild(newRow);
1677 for (let i = 0; i < row.length; ++i) {
1678 let suffix = i == row.length - 1 ? "\n" : "\t";
1679 let field = document.createElement("td");
1680 field.appendChild(document.createTextNode(row[i] + suffix));
1681 newRow.appendChild(field);
1687 var KeyedHistogram = {
1688 render(parent, id, keyedHistogram) {
1689 let outerDiv = document.createElement("div");
1690 outerDiv.className = "keyed-histogram";
1693 let divTitle = document.createElement("div");
1694 divTitle.classList.add("keyed-title");
1695 divTitle.appendChild(document.createTextNode(id));
1696 outerDiv.appendChild(divTitle);
1698 for (let [name, hgram] of Object.entries(keyedHistogram)) {
1699 Histogram.render(outerDiv, name, hgram);
1702 parent.appendChild(outerDiv);
1707 var AddonDetails = {
1709 * Render the addon details section as a series of headers followed by key/value tables
1710 * @param aPing A ping object to render the data from.
1713 let addonSection = document.getElementById("addon-details");
1714 removeAllChildNodes(addonSection);
1715 let addonDetails = aPing.payload.addonDetails;
1716 const hasData = addonDetails && !!Object.keys(addonDetails).length;
1717 setHasData("addon-details-section", hasData);
1722 for (let provider in addonDetails) {
1723 let providerSection = document.createElement("caption");
1724 document.l10n.setAttributes(
1726 "about-telemetry-addon-provider",
1727 { addonProvider: provider }
1729 let headingStrings = [
1730 "about-telemetry-addon-table-id",
1731 "about-telemetry-addon-table-details",
1733 let table = GenericTable.render(
1734 explodeObject(addonDetails[provider]),
1737 table.appendChild(providerSection);
1738 addonSection.appendChild(table);
1744 static renderContent(data, process, div, section) {
1745 if (data && Object.keys(data).length) {
1746 let s = GenericSubsection.renderSubsectionHeader(process, true, section);
1747 let heading = document.createElement("h2");
1748 document.l10n.setAttributes(heading, "about-telemetry-process", {
1751 s.appendChild(heading);
1753 this.renderData(data, s);
1756 let separator = document.createElement("div");
1757 separator.classList.add("clearfix");
1758 div.appendChild(separator);
1763 * Make parent process the first one, content process the second
1764 * then sort processes alphabetically
1766 static processesComparator(a, b) {
1767 if (a === "parent" || (a === "content" && b !== "parent")) {
1769 } else if (b === "parent" || b === "content") {
1782 static renderSection(divName, section, aPayload) {
1783 let div = document.getElementById(divName);
1784 removeAllChildNodes(div);
1787 let hasData = false;
1788 let selectedStore = getSelectedStore();
1790 let payload = aPayload.stores;
1792 let isCurrentPayload = !!payload;
1795 let sortedProcesses = isCurrentPayload
1796 ? Object.keys(payload[selectedStore]).sort(this.processesComparator)
1797 : Object.keys(aPayload.processes).sort(this.processesComparator);
1799 // Render content by process
1800 for (const process of sortedProcesses) {
1801 data = isCurrentPayload
1802 ? this.dataFiltering(payload, selectedStore, process)
1803 : this.archivePingDataFiltering(aPayload, process);
1804 hasData = hasData || !ObjectUtils.isEmpty(data);
1805 this.renderContent(data, process, div, section, this.renderData);
1807 setHasData(section, hasData);
1811 class Scalars extends Section {
1813 * Return data from the current ping
1815 static dataFiltering(payload, selectedStore, process) {
1816 return payload[selectedStore][process].scalars;
1820 * Return data from an archived ping
1822 static archivePingDataFiltering(payload, process) {
1823 return payload.processes[process].scalars;
1826 static renderData(data, div) {
1827 const scalarsHeadings = [
1828 "about-telemetry-names-header",
1829 "about-telemetry-values-header",
1831 let scalarsTable = GenericTable.render(
1832 explodeObject(data),
1835 div.appendChild(scalarsTable);
1839 * Render the scalar data - if present - from the payload in a simple key-value table.
1840 * @param aPayload A payload object to render the data from.
1842 static render(aPayload) {
1843 const divName = "scalars";
1844 const section = "scalars-section";
1845 this.renderSection(divName, section, aPayload);
1849 class KeyedScalars extends Section {
1851 * Return data from the current ping
1853 static dataFiltering(payload, selectedStore, process) {
1854 return payload[selectedStore][process].keyedScalars;
1858 * Return data from an archived ping
1860 static archivePingDataFiltering(payload, process) {
1861 return payload.processes[process].keyedScalars;
1864 static renderData(data, div) {
1865 const scalarsHeadings = [
1866 "about-telemetry-names-header",
1867 "about-telemetry-values-header",
1869 for (let scalarId in data) {
1870 // Add the name of the scalar.
1871 let container = document.createElement("div");
1872 container.classList.add("keyed-scalar");
1873 container.id = scalarId;
1874 let scalarNameSection = document.createElement("p");
1875 scalarNameSection.classList.add("keyed-title");
1876 scalarNameSection.appendChild(document.createTextNode(scalarId));
1877 container.appendChild(scalarNameSection);
1878 // Populate the section with the key-value pairs from the scalar.
1879 const table = GenericTable.render(
1880 explodeObject(data[scalarId]),
1883 container.appendChild(table);
1884 div.appendChild(container);
1889 * Render the keyed scalar data - if present - from the payload in a simple key-value table.
1890 * @param aPayload A payload object to render the data from.
1892 static render(aPayload) {
1893 const divName = "keyed-scalars";
1894 const section = "keyed-scalars-section";
1895 this.renderSection(divName, section, aPayload);
1901 * Render the event data - if present - from the payload in a simple table.
1902 * @param aPayload A payload object to render the data from.
1905 let eventsDiv = document.getElementById("events");
1906 removeAllChildNodes(eventsDiv);
1908 "about-telemetry-time-stamp-header",
1909 "about-telemetry-category-header",
1910 "about-telemetry-method-header",
1911 "about-telemetry-object-header",
1912 "about-telemetry-values-header",
1913 "about-telemetry-extra-header",
1915 let payload = aPayload.processes;
1916 let hasData = false;
1918 for (const process of Object.keys(aPayload.processes)) {
1919 let data = aPayload.processes[process].events;
1920 if (data && Object.keys(data).length) {
1922 let s = GenericSubsection.renderSubsectionHeader(
1927 let heading = document.createElement("h2");
1928 heading.textContent = process;
1929 s.appendChild(heading);
1930 const table = GenericTable.render(data, headings);
1931 s.appendChild(table);
1932 eventsDiv.appendChild(s);
1933 let separator = document.createElement("div");
1934 separator.classList.add("clearfix");
1935 eventsDiv.appendChild(separator);
1939 // handle archived ping
1940 for (const process of Object.keys(aPayload.events)) {
1942 if (data && Object.keys(data).length) {
1944 let s = GenericSubsection.renderSubsectionHeader(
1949 let heading = document.createElement("h2");
1950 heading.textContent = process;
1951 s.appendChild(heading);
1952 const table = GenericTable.render(data, headings);
1953 eventsDiv.appendChild(table);
1954 let separator = document.createElement("div");
1955 separator.classList.add("clearfix");
1956 eventsDiv.appendChild(separator);
1960 setHasData("events-section", hasData);
1966 let originSection = document.getElementById("origins");
1967 removeAllChildNodes(originSection);
1970 "about-telemetry-origin-origin",
1971 "about-telemetry-origin-count",
1974 let hasData = false;
1975 for (let [metric, origins] of Object.entries(aOrigins || {})) {
1976 if (!Object.entries(origins).length) {
1980 const metricHeader = document.createElement("caption");
1981 metricHeader.appendChild(document.createTextNode(metric));
1983 const table = GenericTable.render(Object.entries(origins), headings);
1984 table.appendChild(metricHeader);
1985 originSection.appendChild(table);
1988 setHasData("origin-telemetry-section", hasData);
1993 * Helper function for showing either the toggle element or "No data collected" message for a section
1995 * @param aSectionID ID of the section element that needs to be changed
1996 * @param aHasData true (default) indicates that toggle should be displayed
1998 function setHasData(aSectionID, aHasData) {
1999 let sectionElement = document.getElementById(aSectionID);
2000 sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
2002 // Display or Hide the section in the sidebar
2003 let sectionCategory = document.querySelector(
2004 ".category[value=" + aSectionID + "]"
2006 sectionCategory.classList[aHasData ? "add" : "remove"]("has-data");
2010 * Sets l10n attributes based on the Telemetry Server Owner pref.
2012 function setupServerOwnerBranding() {
2013 let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
2015 [document.getElementById("page-subtitle"), "about-telemetry-page-subtitle"],
2017 document.getElementById("origins-explanation"),
2018 "about-telemetry-origins-explanation",
2021 for (const [elt, l10nName] of elements) {
2022 document.l10n.setAttributes(elt, l10nName, {
2023 telemetryServerOwner: serverOwner,
2029 * Display the store selector if we are on one
2030 * of the whitelisted sections
2032 function displayStoresSelector(selectedSection) {
2035 "keyed-scalars-section",
2036 "histograms-section",
2037 "keyed-histograms-section",
2039 let stores = document.getElementById("stores");
2040 stores.hidden = !whitelist.includes(selectedSection);
2041 let storesLabel = document.getElementById("storesLabel");
2042 storesLabel.hidden = !whitelist.includes(selectedSection);
2045 function refreshSearch() {
2046 removeSearchSectionTitles();
2047 let selectedSection = document
2048 .querySelector(".category.selected")
2049 .getAttribute("value");
2050 let search = document.getElementById("search");
2051 if (!Search.blacklist.includes(selectedSection)) {
2052 Search.search(search.value);
2056 function adjustSearchState() {
2057 removeSearchSectionTitles();
2058 let selectedSection = document
2059 .querySelector(".category.selected")
2060 .getAttribute("value");
2061 let search = document.getElementById("search");
2063 search.hidden = Search.blacklist.includes(selectedSection);
2064 document.getElementById("no-search-results").classList.add("hidden");
2065 Search.search(""); // reinitialize search state.
2068 function removeSearchSectionTitles() {
2069 for (let sectionTitleDiv of Array.from(
2070 document.getElementsByClassName("search-section-title")
2072 sectionTitleDiv.remove();
2076 function adjustSection() {
2077 let selectedCategory = document.querySelector(".category.selected");
2078 if (!selectedCategory.classList.contains("has-data")) {
2079 PingPicker._showStructuredPingData();
2083 function adjustHeaderState(title = null) {
2084 let selected = document.querySelector(".category.selected .category-name");
2085 let selectedTitle = selected.textContent.trim();
2086 let sectionTitle = document.getElementById("sectionTitle");
2087 if (title !== null) {
2088 document.l10n.setAttributes(
2090 "about-telemetry-results-for-search",
2091 { searchTerms: title }
2094 sectionTitle.textContent = selectedTitle;
2096 let search = document.getElementById("search");
2097 if (selected.parentElement.id === "category-home") {
2098 document.l10n.setAttributes(
2100 "about-telemetry-filter-all-placeholder"
2103 document.l10n.setAttributes(search, "about-telemetry-filter-placeholder", {
2110 * Change the url according to the current section displayed
2111 * e.g about:telemetry#general-data
2113 function changeUrlPath(selectedSection, subSection) {
2115 let hash = window.location.hash.split("_")[0] + "_" + selectedSection;
2116 window.location.hash = hash;
2118 window.location.hash = selectedSection.replace("-section", "-tab");
2123 * Change the url according to the current search text
2125 function changeUrlSearch(searchText) {
2126 let currentHash = window.location.hash;
2127 let hashWithoutSearch = currentHash.split(Search.HASH_SEARCH)[0];
2130 if (!currentHash && !searchText) {
2133 if (!currentHash.includes(Search.HASH_SEARCH) && hashWithoutSearch) {
2134 hashWithoutSearch += "_";
2138 hashWithoutSearch + Search.HASH_SEARCH + searchText.replace(/ /g, "+");
2139 } else if (hashWithoutSearch) {
2140 hash = hashWithoutSearch.slice(0, hashWithoutSearch.length - 1);
2143 window.location.hash = hash;
2147 * Change the section displayed
2149 function show(selected) {
2150 let selectedValue = selected.getAttribute("value");
2151 if (selectedValue === "raw-json-viewer") {
2152 openJsonInFirefoxJsonViewer(JSON.stringify(gPingData, null, 2));
2156 let selected_section = document.getElementById(selectedValue);
2157 let subsections = selected_section.querySelectorAll(".sub-section");
2158 if (selected.classList.contains("has-subsection")) {
2159 for (let subsection of selected.children) {
2160 subsection.classList.remove("selected");
2164 for (let subsection of subsections) {
2165 subsection.hidden = false;
2169 let current_button = document.querySelector(".category.selected");
2170 if (current_button == selected) {
2173 current_button.classList.remove("selected");
2174 selected.classList.add("selected");
2176 document.querySelectorAll("section").forEach(section => {
2177 section.classList.remove("active");
2179 selected_section.classList.add("active");
2181 adjustHeaderState();
2182 displayStoresSelector(selectedValue);
2183 adjustSearchState();
2184 changeUrlPath(selectedValue);
2187 function showSubSection(selected) {
2191 let current_selection = document.querySelector(
2192 ".category-subsection.selected"
2194 if (current_selection) {
2195 current_selection.classList.remove("selected");
2197 selected.classList.add("selected");
2199 let section = document.getElementById(selected.getAttribute("value"));
2200 section.parentElement.childNodes.forEach(element => {
2201 element.hidden = true;
2203 section.hidden = false;
2205 let title = selected.parentElement.querySelector(".category-name")
2207 let subsection = selected.textContent;
2208 document.getElementById("sectionTitle").textContent =
2209 title + " - " + subsection;
2210 changeUrlPath(subsection, true);
2214 * Initializes load/unload, pref change and mouse-click listeners
2216 function setupListeners() {
2217 Settings.attachObservers();
2218 PingPicker.attachObservers();
2219 RawPayloadData.attachObservers();
2221 let menu = document.getElementById("categories");
2222 menu.addEventListener("click", e => {
2223 if (e.target && e.target.parentNode == menu) {
2228 let search = document.getElementById("search");
2229 search.addEventListener("input", Search.searchHandler);
2232 .getElementById("late-writes-fetch-symbols")
2233 .addEventListener("click", function() {
2238 let lateWrites = gPingData.payload.lateWrites;
2239 let req = new SymbolicationRequest(
2241 LateWritesSingleton.renderHeader,
2242 lateWrites.memoryMap,
2249 .getElementById("late-writes-hide-symbols")
2250 .addEventListener("click", function() {
2255 LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
2259 // Restores the sections states
2260 function urlSectionRestore(hash) {
2262 let section = hash.replace("-tab", "-section");
2263 let subsection = section.split("_")[1];
2264 section = section.split("_")[0];
2265 let category = document.querySelector(".category[value=" + section + "]");
2270 ".category-subsection[value=" + section + "-" + subsection + "]";
2271 let subcategory = document.querySelector(selector);
2272 showSubSection(subcategory);
2278 // Restore sections states and search terms
2279 function urlStateRestore() {
2280 let hash = window.location.hash;
2281 let searchQuery = "";
2283 hash = hash.slice(1);
2284 if (hash.includes(Search.HASH_SEARCH)) {
2285 searchQuery = hash.split(Search.HASH_SEARCH)[1].replace(/[+]/g, " ");
2286 hash = hash.split(Search.HASH_SEARCH)[0];
2288 urlSectionRestore(hash);
2291 let search = document.getElementById("search");
2292 search.value = searchQuery;
2296 function openJsonInFirefoxJsonViewer(json) {
2297 json = unescape(encodeURIComponent(json));
2299 window.open("data:application/json;base64," + btoa(json));
2301 show(document.querySelector(".category[value=raw-payload-section]"));
2306 window.removeEventListener("load", onLoad);
2307 // Set the text in the page header and elsewhere that needs the server owner.
2308 setupServerOwnerBranding();
2310 // Set up event listeners
2316 adjustHeaderState();
2320 // Update ping data when async Telemetry init is finished.
2321 Telemetry.asyncFetchTelemetryData(async () => {
2322 await PingPicker.update();
2326 var LateWritesSingleton = {
2327 renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
2328 StackRenderer.renderHeader(
2330 "about-telemetry-late-writes-title",
2331 { lateWriteCount: aIndex + 1 }
2335 renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
2338 lateWrites.stacks &&
2339 lateWrites.stacks.length
2341 setHasData("late-writes-section", hasData);
2346 let stacks = lateWrites.stacks;
2347 let memoryMap = lateWrites.memoryMap;
2348 StackRenderer.renderStacks(
2352 LateWritesSingleton.renderHeader
2357 class HistogramSection extends Section {
2359 * Return data from the current ping
2361 static dataFiltering(payload, selectedStore, process) {
2362 return payload[selectedStore][process].histograms;
2366 * Return data from an archived ping
2368 static archivePingDataFiltering(payload, process) {
2369 if (process === "parent") {
2370 return payload.histograms;
2372 return payload.processes[process].histograms;
2375 static renderData(data, div) {
2376 for (let [hName, hgram] of Object.entries(data)) {
2377 Histogram.render(div, hName, hgram, { unpacked: true });
2381 static render(aPayload) {
2382 const divName = "histograms";
2383 const section = "histograms-section";
2384 this.renderSection(divName, section, aPayload);
2388 class KeyedHistogramSection extends Section {
2390 * Return data from the current ping
2392 static dataFiltering(payload, selectedStore, process) {
2393 return payload[selectedStore][process].keyedHistograms;
2397 * Return data from an archived ping
2399 static archivePingDataFiltering(payload, process) {
2400 if (process === "parent") {
2401 return payload.keyedHistograms;
2403 return payload.processes[process].keyedHistograms;
2406 static renderData(data, div) {
2407 for (let [id, keyed] of Object.entries(data)) {
2408 KeyedHistogram.render(div, id, keyed, { unpacked: true });
2412 static render(aPayload) {
2413 const divName = "keyed-histograms";
2414 const section = "keyed-histograms-section";
2415 this.renderSection(divName, section, aPayload);
2419 var SessionInformation = {
2421 let infoSection = document.getElementById("session-info");
2422 removeAllChildNodes(infoSection);
2424 let hasData = !!Object.keys(aPayload.info).length;
2425 setHasData("session-info-section", hasData);
2428 const table = GenericTable.render(explodeObject(aPayload.info));
2429 infoSection.appendChild(table);
2434 var SimpleMeasurements = {
2436 let simpleSection = document.getElementById("simple-measurements");
2437 removeAllChildNodes(simpleSection);
2439 let simpleMeasurements = this.sortStartupMilestones(
2440 aPayload.simpleMeasurements
2442 let hasData = !!Object.keys(simpleMeasurements).length;
2443 setHasData("simple-measurements-section", hasData);
2446 const table = GenericTable.render(explodeObject(simpleMeasurements));
2447 simpleSection.appendChild(table);
2452 * Helper function for sorting the startup milestones in the Simple Measurements
2453 * section into temporal order.
2455 * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
2456 * @return Sorted measurements
2458 sortStartupMilestones(aSimpleMeasurements) {
2459 const telemetryTimestamps = TelemetryTimestamps.get();
2460 let startupEvents = Services.startup.getStartupInfo();
2461 delete startupEvents.process;
2463 function keyIsMilestone(k) {
2464 return k in startupEvents || k in telemetryTimestamps;
2467 let sortedKeys = Object.keys(aSimpleMeasurements);
2469 // Sort the measurements, with startup milestones at the front + ordered by time
2470 sortedKeys.sort(function keyCompare(keyA, keyB) {
2471 let isKeyAMilestone = keyIsMilestone(keyA);
2472 let isKeyBMilestone = keyIsMilestone(keyB);
2474 // First order by startup vs non-startup measurement
2475 if (isKeyAMilestone && !isKeyBMilestone) {
2478 if (!isKeyAMilestone && isKeyBMilestone) {
2481 // Don't change order of non-startup measurements
2482 if (!isKeyAMilestone && !isKeyBMilestone) {
2486 // If both keys are startup measurements, order them by value
2487 return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
2490 // Insert measurements into a result object in sort-order
2492 for (let key of sortedKeys) {
2493 result[key] = aSimpleMeasurements[key];
2501 * Render stores options
2503 function renderStoreList(payload) {
2504 let storeSelect = document.getElementById("stores");
2505 let storesLabel = document.getElementById("storesLabel");
2506 removeAllChildNodes(storeSelect);
2508 if (!("stores" in payload)) {
2509 storeSelect.classList.add("hidden");
2510 storesLabel.classList.add("hidden");
2514 storeSelect.classList.remove("hidden");
2515 storesLabel.classList.remove("hidden");
2516 storeSelect.disabled = false;
2518 for (let store of Object.keys(payload.stores)) {
2519 let option = document.createElement("option");
2520 option.appendChild(document.createTextNode(store));
2521 option.setAttribute("value", store);
2522 // Select main store by default
2523 if (store === "main") {
2524 option.selected = true;
2526 storeSelect.appendChild(option);
2531 * Return the selected store
2533 function getSelectedStore() {
2534 let storeSelect = document.getElementById("stores");
2535 let storeSelectedOption = storeSelect.selectedOptions.item(0);
2537 storeSelectedOption !== null
2538 ? storeSelectedOption.getAttribute("value")
2540 return selectedStore;
2543 function togglePingSections(isMainPing) {
2544 // We always show the sections that are "common" to all pings.
2545 let commonSections = new Set([
2548 "general-data-section",
2549 "environment-data-section",
2553 let elements = document.querySelectorAll(".category");
2554 for (let section of elements) {
2555 if (commonSections.has(section.getAttribute("value"))) {
2558 // Only show the raw payload for non main ping.
2559 if (section.getAttribute("value") == "raw-payload-section") {
2560 section.classList.toggle("has-data", !isMainPing);
2562 section.classList.toggle("has-data", isMainPing);
2567 function displayPingData(ping, updatePayloadList = false) {
2570 PingPicker.render();
2571 displayRichPingData(ping, updatePayloadList);
2576 PingPicker._showRawPingData();
2580 function displayRichPingData(ping, updatePayloadList) {
2581 // Update the payload list and store lists
2582 if (updatePayloadList) {
2583 renderStoreList(ping.payload);
2586 // Show general data.
2587 GeneralData.render(ping);
2589 // Show environment data.
2590 EnvironmentData.render(ping);
2592 RawPayloadData.render(ping);
2594 // We have special rendering code for the payloads from "main" and "event" pings.
2595 // For any other pings we just render the raw JSON payload.
2596 let isMainPing = ping.type == "main" || ping.type == "saved-session";
2597 let isEventPing = ping.type == "event";
2598 togglePingSections(isMainPing);
2601 // Copy the payload, so we don't modify the raw representation
2602 // Ensure we always have at least the parent process.
2603 let payload = { processes: { parent: {} } };
2604 for (let process of Object.keys(ping.payload.events)) {
2605 payload.processes[process] = {
2606 events: ping.payload.events[process],
2610 // We transformed the actual payload, let's reload the store list if necessary.
2611 if (updatePayloadList) {
2612 renderStoreList(payload);
2616 Events.render(payload);
2624 // Show slow SQL stats
2625 SlowSQL.render(ping);
2627 // Render Addon details.
2628 AddonDetails.render(ping);
2630 let payload = ping.payload;
2631 // Show basic session info gathered
2632 SessionInformation.render(payload);
2634 // Show scalar data.
2635 Scalars.render(payload);
2636 KeyedScalars.render(payload);
2638 // Show histogram data
2639 HistogramSection.render(payload);
2641 // Show keyed histogram data
2642 KeyedHistogramSection.render(payload);
2645 Events.render(payload);
2647 LateWritesSingleton.renderLateWrites(payload.lateWrites);
2649 // Show origin telemetry.
2650 Origins.render(payload.origins);
2652 // Show simple measurements
2653 SimpleMeasurements.render(payload);
2656 window.addEventListener("load", onLoad);