summaryrefslogtreecommitdiffstats
path: root/toolkit/content/aboutTelemetry.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/aboutTelemetry.js')
-rw-r--r--toolkit/content/aboutTelemetry.js2622
1 files changed, 2622 insertions, 0 deletions
diff --git a/toolkit/content/aboutTelemetry.js b/toolkit/content/aboutTelemetry.js
new file mode 100644
index 0000000000..a9a3b01fed
--- /dev/null
+++ b/toolkit/content/aboutTelemetry.js
@@ -0,0 +1,2622 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { BrowserUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/BrowserUtils.sys.mjs"
+);
+const { TelemetryTimestamps } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryTimestamps.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryArchive } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryArchive.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "ObjectUtils",
+ "resource://gre/modules/ObjectUtils.jsm"
+);
+
+const Telemetry = Services.telemetry;
+
+// Maximum height of a histogram bar (in em for html, in chars for text)
+const MAX_BAR_HEIGHT = 8;
+const MAX_BAR_CHARS = 25;
+const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner";
+const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql";
+const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl";
+const DEFAULT_SYMBOL_SERVER_URI =
+ "https://symbolication.services.mozilla.com/symbolicate/v4";
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+
+// ms idle before applying the filter (allow uninterrupted typing)
+const FILTER_IDLE_TIMEOUT = 500;
+
+const isWindows = Services.appinfo.OS == "WINNT";
+const EOL = isWindows ? "\r\n" : "\n";
+
+// This is the ping object currently displayed in the page.
+var gPingData = null;
+
+// Cached value of document's RTL mode
+var documentRTLMode = "";
+
+/**
+ * Helper function for determining whether the document direction is RTL.
+ * Caches result of check on first invocation.
+ */
+function isRTL() {
+ if (!documentRTLMode) {
+ documentRTLMode = window.getComputedStyle(document.body).direction;
+ }
+ return documentRTLMode == "rtl";
+}
+
+function isFlatArray(obj) {
+ if (!Array.isArray(obj)) {
+ return false;
+ }
+ return !obj.some(e => typeof e == "object");
+}
+
+/**
+ * This is a helper function for explodeObject.
+ */
+function flattenObject(obj, map, path, array) {
+ for (let k of Object.keys(obj)) {
+ let newPath = [...path, array ? "[" + k + "]" : k];
+ let v = obj[k];
+ if (!v || typeof v != "object") {
+ map.set(newPath.join("."), v);
+ } else if (isFlatArray(v)) {
+ map.set(newPath.join("."), "[" + v.join(", ") + "]");
+ } else {
+ flattenObject(v, map, newPath, Array.isArray(v));
+ }
+ }
+}
+
+/**
+ * This turns a JSON object into a "flat" stringified form.
+ *
+ * For an object like {a: "1", b: {c: "2", d: "3"}} it returns a Map of the
+ * form Map(["a","1"], ["b.c", "2"], ["b.d", "3"]).
+ */
+function explodeObject(obj) {
+ let map = new Map();
+ flattenObject(obj, map, []);
+ return map;
+}
+
+function filterObject(obj, filterOut) {
+ let ret = {};
+ for (let k of Object.keys(obj)) {
+ if (!filterOut.includes(k)) {
+ ret[k] = obj[k];
+ }
+ }
+ return ret;
+}
+
+/**
+ * This turns a JSON object into a "flat" stringified form, separated into top-level sections.
+ *
+ * For an object like:
+ * {
+ * a: {b: "1"},
+ * c: {d: "2", e: {f: "3"}}
+ * }
+ * it returns a Map of the form:
+ * Map([
+ * ["a", Map(["b","1"])],
+ * ["c", Map([["d", "2"], ["e.f", "3"]])]
+ * ])
+ */
+function sectionalizeObject(obj) {
+ let map = new Map();
+ for (let k of Object.keys(obj)) {
+ map.set(k, explodeObject(obj[k]));
+ }
+ return map;
+}
+
+/**
+ * Obtain the main DOMWindow for the current context.
+ */
+function getMainWindow() {
+ return window.browsingContext.topChromeWindow;
+}
+
+/**
+ * Obtain the DOMWindow that can open a preferences pane.
+ *
+ * This is essentially "get the browser chrome window" with the added check
+ * that the supposed browser chrome window is capable of opening a preferences
+ * pane.
+ *
+ * This may return null if we can't find the browser chrome window.
+ */
+function getMainWindowWithPreferencesPane() {
+ let mainWindow = getMainWindow();
+ if (mainWindow && "openPreferences" in mainWindow) {
+ return mainWindow;
+ }
+ return null;
+}
+
+/**
+ * Remove all child nodes of a document node.
+ */
+function removeAllChildNodes(node) {
+ while (node.hasChildNodes()) {
+ node.removeChild(node.lastChild);
+ }
+}
+
+var Settings = {
+ attachObservers() {
+ let elements = document.getElementsByClassName("change-data-choices-link");
+ for (let el of elements) {
+ el.parentElement.addEventListener("click", function (event) {
+ if (event.target.localName === "a") {
+ if (AppConstants.platform == "android") {
+ var { EventDispatcher } = ChromeUtils.importESModule(
+ "resource://gre/modules/Messaging.sys.mjs"
+ );
+ EventDispatcher.instance.sendRequest({
+ type: "Settings:Show",
+ resource: "preferences_privacy",
+ });
+ } else {
+ // Show the data choices preferences on desktop.
+ let mainWindow = getMainWindowWithPreferencesPane();
+ mainWindow.openPreferences("privacy-reports");
+ }
+ }
+ });
+ }
+ },
+
+ /**
+ * Updates the button & text at the top of the page to reflect Telemetry state.
+ */
+ render() {
+ let settingsExplanation = document.getElementById("settings-explanation");
+ let extendedEnabled = Services.telemetry.canRecordExtended;
+
+ let channel = extendedEnabled ? "prerelease" : "release";
+ let uploadcase = TelemetrySend.sendingEnabled() ? "enabled" : "disabled";
+
+ document.l10n.setAttributes(
+ settingsExplanation,
+ "about-telemetry-settings-explanation",
+ { channel, uploadcase }
+ );
+
+ this.attachObservers();
+ },
+};
+
+var PingPicker = {
+ viewCurrentPingData: null,
+ _archivedPings: null,
+ TYPE_ALL: "all",
+
+ attachObservers() {
+ let pingSourceElements = document.getElementsByName("choose-ping-source");
+ for (let el of pingSourceElements) {
+ el.addEventListener("change", () => this.onPingSourceChanged());
+ }
+
+ let displays = document.getElementsByName("choose-ping-display");
+ for (let el of displays) {
+ el.addEventListener("change", () => this.onPingDisplayChanged());
+ }
+
+ document
+ .getElementById("show-subsession-data")
+ .addEventListener("change", () => {
+ this._updateCurrentPingData();
+ });
+
+ document.getElementById("choose-ping-id").addEventListener("change", () => {
+ this._updateArchivedPingData();
+ });
+ document
+ .getElementById("choose-ping-type")
+ .addEventListener("change", () => {
+ this.filterDisplayedPings();
+ });
+
+ document
+ .getElementById("newer-ping")
+ .addEventListener("click", () => this._movePingIndex(-1));
+ document
+ .getElementById("older-ping")
+ .addEventListener("click", () => this._movePingIndex(1));
+
+ let pingPickerNeedHide = false;
+ let pingPicker = document.getElementById("ping-picker");
+ pingPicker.addEventListener(
+ "mouseenter",
+ () => (pingPickerNeedHide = false)
+ );
+ pingPicker.addEventListener(
+ "mouseleave",
+ () => (pingPickerNeedHide = true)
+ );
+ document.addEventListener("click", ev => {
+ if (pingPickerNeedHide) {
+ pingPicker.classList.add("hidden");
+ }
+ });
+ document
+ .getElementById("stores")
+ .addEventListener("change", () => displayPingData(gPingData));
+ Array.from(document.querySelectorAll(".change-ping")).forEach(el => {
+ el.addEventListener("click", event => {
+ if (!pingPicker.classList.contains("hidden")) {
+ pingPicker.classList.add("hidden");
+ } else {
+ pingPicker.classList.remove("hidden");
+ event.stopPropagation();
+ }
+ });
+ });
+ },
+
+ onPingSourceChanged() {
+ this.update();
+ },
+
+ onPingDisplayChanged() {
+ this.update();
+ },
+
+ render() {
+ // Display the type and controls if the ping is not current
+ let pingDate = document.getElementById("ping-date");
+ let pingType = document.getElementById("ping-type");
+ let controls = document.getElementById("controls");
+ let pingExplanation = document.getElementById("ping-explanation");
+
+ if (!this.viewCurrentPingData) {
+ let pingName = this._getSelectedPingName();
+ // Change sidebar heading text.
+ pingDate.textContent = pingName;
+ pingDate.setAttribute("title", pingName);
+ let pingTypeText = this._getSelectedPingType();
+ controls.classList.remove("hidden");
+ pingType.textContent = pingTypeText;
+ document.l10n.setAttributes(
+ pingExplanation,
+ "about-telemetry-ping-details",
+ { timestamp: pingTypeText, name: pingName }
+ );
+ } else {
+ // Change sidebar heading text.
+ controls.classList.add("hidden");
+ document.l10n.setAttributes(
+ pingType,
+ "about-telemetry-current-data-sidebar"
+ );
+ // Change home page text.
+ document.l10n.setAttributes(
+ pingExplanation,
+ "about-telemetry-data-details-current"
+ );
+ }
+
+ GenericSubsection.deleteAllSubSections();
+ },
+
+ async update() {
+ let viewCurrent = document.getElementById("ping-source-current").checked;
+ let currentChanged = viewCurrent !== this.viewCurrentPingData;
+ this.viewCurrentPingData = viewCurrent;
+
+ // If we have no archived pings, disable the ping archive selection.
+ // This can happen on new profiles or if the ping archive is disabled.
+ let archivedPingList = await TelemetryArchive.promiseArchivedPingList();
+ let sourceArchived = document.getElementById("ping-source-archive");
+ let sourceArchivedContainer = document.getElementById(
+ "ping-source-archive-container"
+ );
+ let archivedDisabled = !archivedPingList.length;
+ sourceArchived.disabled = archivedDisabled;
+ sourceArchivedContainer.classList.toggle("disabled", archivedDisabled);
+
+ if (currentChanged) {
+ if (this.viewCurrentPingData) {
+ document.getElementById("current-ping-picker").hidden = false;
+ document.getElementById("archived-ping-picker").hidden = true;
+ this._updateCurrentPingData();
+ } else {
+ document.getElementById("current-ping-picker").hidden = true;
+ await this._updateArchivedPingList(archivedPingList);
+ document.getElementById("archived-ping-picker").hidden = false;
+ }
+ }
+ },
+
+ _updateCurrentPingData() {
+ const subsession = document.getElementById("show-subsession-data").checked;
+ let ping = TelemetryController.getCurrentPingData(subsession);
+ if (!ping) {
+ return;
+ }
+
+ let stores = Telemetry.getAllStores();
+ let getData = {
+ histograms: Telemetry.getSnapshotForHistograms,
+ keyedHistograms: Telemetry.getSnapshotForKeyedHistograms,
+ scalars: Telemetry.getSnapshotForScalars,
+ keyedScalars: Telemetry.getSnapshotForKeyedScalars,
+ };
+
+ let data = {};
+ for (const [name, fn] of Object.entries(getData)) {
+ for (const store of stores) {
+ if (!data[store]) {
+ data[store] = {};
+ }
+ let measurement = fn(store, /* clear */ false, /* filterTest */ true);
+ let processes = Object.keys(measurement);
+
+ for (const process of processes) {
+ if (!data[store][process]) {
+ data[store][process] = {};
+ }
+
+ data[store][process][name] = measurement[process];
+ }
+ }
+ }
+ ping.payload.stores = data;
+
+ // Delete the unused data from the payload of the current ping.
+ // It's included in the above `stores` attribute.
+ for (const data of Object.values(ping.payload.processes)) {
+ delete data.scalars;
+ delete data.keyedScalars;
+ delete data.histograms;
+ delete data.keyedHistograms;
+ }
+ delete ping.payload.histograms;
+ delete ping.payload.keyedHistograms;
+
+ // augment ping payload with event telemetry
+ let eventSnapshot = Telemetry.snapshotEvents(
+ Telemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ for (let process of Object.keys(eventSnapshot)) {
+ if (process in ping.payload.processes) {
+ ping.payload.processes[process].events = eventSnapshot[process].filter(
+ e => !e[1].startsWith("telemetry.test")
+ );
+ }
+ }
+
+ displayPingData(ping, true);
+ },
+
+ _updateArchivedPingData() {
+ let id = this._getSelectedPingId();
+ let res = Promise.resolve();
+ if (id) {
+ res = TelemetryArchive.promiseArchivedPingById(id).then(ping =>
+ displayPingData(ping, true)
+ );
+ }
+ return res;
+ },
+
+ async _updateArchivedPingList(pingList) {
+ // The archived ping list is sorted in ascending timestamp order,
+ // but descending is more practical for the operations we do here.
+ pingList.reverse();
+ this._archivedPings = pingList;
+ // Render the archive data.
+ this._renderPingList();
+ // Update the displayed ping.
+ await this._updateArchivedPingData();
+ },
+
+ _renderPingList() {
+ let pingSelector = document.getElementById("choose-ping-id");
+ Array.from(pingSelector.children).forEach(child =>
+ removeAllChildNodes(child)
+ );
+
+ let pingTypes = new Set();
+ pingTypes.add(this.TYPE_ALL);
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const yesterday = new Date(today);
+ yesterday.setDate(today.getDate() - 1);
+
+ for (let p of this._archivedPings) {
+ pingTypes.add(p.type);
+ const pingDate = new Date(p.timestampCreated);
+ const datetimeText = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ timeStyle: "medium",
+ }).format(pingDate);
+ const pingName = `${datetimeText}, ${p.type}`;
+
+ let option = document.createElement("option");
+ let content = document.createTextNode(pingName);
+ option.appendChild(content);
+ option.setAttribute("value", p.id);
+ option.dataset.type = p.type;
+ option.dataset.date = datetimeText;
+
+ pingDate.setHours(0, 0, 0, 0);
+ if (pingDate.getTime() === today.getTime()) {
+ pingSelector.children[0].appendChild(option);
+ } else if (pingDate.getTime() === yesterday.getTime()) {
+ pingSelector.children[1].appendChild(option);
+ } else {
+ pingSelector.children[2].appendChild(option);
+ }
+ }
+ this._renderPingTypes(pingTypes);
+ },
+
+ _renderPingTypes(pingTypes) {
+ let pingTypeSelector = document.getElementById("choose-ping-type");
+ removeAllChildNodes(pingTypeSelector);
+ pingTypes.forEach(type => {
+ let option = document.createElement("option");
+ option.appendChild(document.createTextNode(type));
+ option.setAttribute("value", type);
+ pingTypeSelector.appendChild(option);
+ });
+ },
+
+ _movePingIndex(offset) {
+ if (this.viewCurrentPingData) {
+ return;
+ }
+ let typeSelector = document.getElementById("choose-ping-type");
+ let type = typeSelector.selectedOptions.item(0).value;
+
+ let id = this._getSelectedPingId();
+ let index = this._archivedPings.findIndex(p => p.id == id);
+ let newIndex = Math.min(
+ Math.max(0, index + offset),
+ this._archivedPings.length - 1
+ );
+
+ let pingList;
+ if (offset > 0) {
+ pingList = this._archivedPings.slice(newIndex);
+ } else {
+ pingList = this._archivedPings.slice(0, newIndex);
+ pingList.reverse();
+ }
+
+ let ping = pingList.find(p => {
+ return type == this.TYPE_ALL || p.type == type;
+ });
+
+ if (ping) {
+ this.selectPing(ping);
+ this._updateArchivedPingData();
+ }
+ },
+
+ selectPing(ping) {
+ let pingSelector = document.getElementById("choose-ping-id");
+ // Use some() to break if we find the ping.
+ Array.from(pingSelector.children).some(group => {
+ return Array.from(group.children).some(option => {
+ if (option.value == ping.id) {
+ option.selected = true;
+ return true;
+ }
+ return false;
+ });
+ });
+ },
+
+ filterDisplayedPings() {
+ let pingSelector = document.getElementById("choose-ping-id");
+ let typeSelector = document.getElementById("choose-ping-type");
+ let type = typeSelector.selectedOptions.item(0).value;
+ let first = true;
+ Array.from(pingSelector.children).forEach(group => {
+ Array.from(group.children).forEach(option => {
+ if (first && option.dataset.type == type) {
+ option.selected = true;
+ first = false;
+ }
+ option.hidden = type != this.TYPE_ALL && option.dataset.type != type;
+ // Arrow keys should only iterate over visible options
+ option.disabled = option.hidden;
+ });
+ });
+ this._updateArchivedPingData();
+ },
+
+ _getSelectedPingName() {
+ let pingSelector = document.getElementById("choose-ping-id");
+ let selected = pingSelector.selectedOptions.item(0);
+ return selected.dataset.date;
+ },
+
+ _getSelectedPingType() {
+ let pingSelector = document.getElementById("choose-ping-id");
+ let selected = pingSelector.selectedOptions.item(0);
+ return selected.dataset.type;
+ },
+
+ _getSelectedPingId() {
+ let pingSelector = document.getElementById("choose-ping-id");
+ let selected = pingSelector.selectedOptions.item(0);
+ return selected.getAttribute("value");
+ },
+
+ _showRawPingData() {
+ show(document.getElementById("category-raw"));
+ },
+
+ _showStructuredPingData() {
+ show(document.getElementById("category-home"));
+ },
+};
+
+var GeneralData = {
+ /**
+ * Renders the general data
+ */
+ render(aPing) {
+ setHasData("general-data-section", true);
+ let generalDataSection = document.getElementById("general-data");
+ removeAllChildNodes(generalDataSection);
+
+ const headings = [
+ "about-telemetry-names-header",
+ "about-telemetry-values-header",
+ ];
+
+ // The payload & environment parts are handled by other renderers.
+ let ignoreSections = ["payload", "environment"];
+ let data = explodeObject(filterObject(aPing, ignoreSections));
+
+ const table = GenericTable.render(data, headings);
+ generalDataSection.appendChild(table);
+ },
+};
+
+var EnvironmentData = {
+ /**
+ * Renders the environment data
+ */
+ render(ping) {
+ let dataDiv = document.getElementById("environment-data");
+ removeAllChildNodes(dataDiv);
+ const hasData = !!ping.environment;
+ setHasData("environment-data-section", hasData);
+ if (!hasData) {
+ return;
+ }
+
+ let ignore = ["addons"];
+ let env = filterObject(ping.environment, ignore);
+ let sections = sectionalizeObject(env);
+ GenericSubsection.render(sections, dataDiv, "environment-data-section");
+
+ // We use specialized rendering here to make the addon and plugin listings
+ // more readable.
+ this.createAddonSection(dataDiv, ping);
+ },
+
+ renderAddonsObject(addonObj, addonSection, sectionTitle) {
+ let table = document.createElement("table");
+ table.setAttribute("id", sectionTitle);
+ this.appendAddonSubsectionTitle(sectionTitle, table);
+
+ for (let id of Object.keys(addonObj)) {
+ let addon = addonObj[id];
+ this.appendHeadingName(table, addon.name || id);
+ this.appendAddonID(table, id);
+ let data = explodeObject(addon);
+
+ for (let [key, value] of data) {
+ this.appendRow(table, key, value);
+ }
+ }
+
+ addonSection.appendChild(table);
+ },
+
+ renderKeyValueObject(addonObj, addonSection, sectionTitle) {
+ let data = explodeObject(addonObj);
+ let table = GenericTable.render(data);
+ table.setAttribute("class", sectionTitle);
+ this.appendAddonSubsectionTitle(sectionTitle, table);
+ addonSection.appendChild(table);
+ },
+
+ appendAddonID(table, addonID) {
+ this.appendRow(table, "id", addonID);
+ },
+
+ appendHeadingName(table, name) {
+ let headings = document.createElement("tr");
+ this.appendColumn(headings, "th", name);
+ headings.cells[0].colSpan = 2;
+ table.appendChild(headings);
+ },
+
+ appendAddonSubsectionTitle(section, table) {
+ let caption = document.createElement("caption");
+ caption.appendChild(document.createTextNode(section));
+ table.appendChild(caption);
+ },
+
+ createAddonSection(dataDiv, ping) {
+ if (!ping || !("environment" in ping) || !("addons" in ping.environment)) {
+ return;
+ }
+ let addonSection = document.createElement("div");
+ addonSection.setAttribute("class", "subsection-data subdata");
+ let addons = ping.environment.addons;
+ this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
+ this.renderKeyValueObject(addons.theme, addonSection, "theme");
+ this.renderAddonsObject(
+ addons.activeGMPlugins,
+ addonSection,
+ "activeGMPlugins"
+ );
+
+ let hasAddonData = !!Object.keys(ping.environment.addons).length;
+ let s = GenericSubsection.renderSubsectionHeader(
+ "addons",
+ hasAddonData,
+ "environment-data-section"
+ );
+ s.appendChild(addonSection);
+ dataDiv.appendChild(s);
+ },
+
+ appendRow(table, id, value) {
+ let row = document.createElement("tr");
+ row.id = id;
+ this.appendColumn(row, "td", id);
+ this.appendColumn(row, "td", value);
+ table.appendChild(row);
+ },
+ /**
+ * Helper function for appending a column to the data table.
+ *
+ * @param aRowElement Parent row element
+ * @param aColType Column's tag name
+ * @param aColText Column contents
+ */
+ appendColumn(aRowElement, aColType, aColText) {
+ let colElement = document.createElement(aColType);
+ let colTextElement = document.createTextNode(aColText);
+ colElement.appendChild(colTextElement);
+ aRowElement.appendChild(colElement);
+ },
+};
+
+var SlowSQL = {
+ /**
+ * Render slow SQL statistics
+ */
+ render: function SlowSQL_render(aPing) {
+ // We can add the debug SQL data to the current ping later.
+ // However, we need to be careful to never send that debug data
+ // out due to privacy concerns.
+ // We want to show the actual ping data for archived pings,
+ // so skip this there.
+
+ let debugSlowSql =
+ PingPicker.viewCurrentPingData &&
+ Preferences.get(PREF_DEBUG_SLOW_SQL, false);
+ let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
+ if (!slowSql) {
+ setHasData("slow-sql-section", false);
+ return;
+ }
+
+ let { mainThread, otherThreads } = debugSlowSql
+ ? Telemetry.debugSlowSQL
+ : aPing.payload.slowSQL;
+
+ let mainThreadCount = Object.keys(mainThread).length;
+ let otherThreadCount = Object.keys(otherThreads).length;
+ if (mainThreadCount == 0 && otherThreadCount == 0) {
+ setHasData("slow-sql-section", false);
+ return;
+ }
+
+ setHasData("slow-sql-section", true);
+ if (debugSlowSql) {
+ document.getElementById("sql-warning").hidden = false;
+ }
+
+ let slowSqlDiv = document.getElementById("slow-sql-tables");
+ removeAllChildNodes(slowSqlDiv);
+
+ // Main thread
+ if (mainThreadCount > 0) {
+ let table = document.createElement("table");
+ this.renderTableHeader(table, "main");
+ this.renderTable(table, mainThread);
+ slowSqlDiv.appendChild(table);
+ }
+
+ // Other threads
+ if (otherThreadCount > 0) {
+ let table = document.createElement("table");
+ this.renderTableHeader(table, "other");
+ this.renderTable(table, otherThreads);
+ slowSqlDiv.appendChild(table);
+ }
+ },
+
+ /**
+ * Creates a header row for a Slow SQL table
+ * Tabs & newlines added to cells to make it easier to copy-paste.
+ *
+ * @param aTable Parent table element
+ * @param aTitle Table's title
+ */
+ renderTableHeader: function SlowSQL_renderTableHeader(aTable, threadType) {
+ let caption = document.createElement("caption");
+ if (threadType == "main") {
+ document.l10n.setAttributes(caption, "about-telemetry-slow-sql-main");
+ }
+
+ if (threadType == "other") {
+ document.l10n.setAttributes(caption, "about-telemetry-slow-sql-other");
+ }
+ aTable.appendChild(caption);
+
+ let headings = document.createElement("tr");
+ document.l10n.setAttributes(
+ this.appendColumn(headings, "th"),
+ "about-telemetry-slow-sql-hits"
+ );
+ document.l10n.setAttributes(
+ this.appendColumn(headings, "th"),
+ "about-telemetry-slow-sql-average"
+ );
+ document.l10n.setAttributes(
+ this.appendColumn(headings, "th"),
+ "about-telemetry-slow-sql-statement"
+ );
+ aTable.appendChild(headings);
+ },
+
+ /**
+ * Fills out the table body
+ * Tabs & newlines added to cells to make it easier to copy-paste.
+ *
+ * @param aTable Parent table element
+ * @param aSql SQL stats object
+ */
+ renderTable: function SlowSQL_renderTable(aTable, aSql) {
+ for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) {
+ let averageTime = totalTime / hitCount;
+
+ let sqlRow = document.createElement("tr");
+
+ this.appendColumn(sqlRow, "td", hitCount + "\t");
+ this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
+ this.appendColumn(sqlRow, "td", sql + "\n");
+
+ aTable.appendChild(sqlRow);
+ }
+ },
+
+ /**
+ * Helper function for appending a column to a Slow SQL table.
+ *
+ * @param aRowElement Parent row element
+ * @param aColType Column's tag name
+ * @param aColText Column contents
+ */
+ appendColumn: function SlowSQL_appendColumn(
+ aRowElement,
+ aColType,
+ aColText = ""
+ ) {
+ let colElement = document.createElement(aColType);
+ if (aColText) {
+ let colTextElement = document.createTextNode(aColText);
+ colElement.appendChild(colTextElement);
+ }
+ aRowElement.appendChild(colElement);
+ return colElement;
+ },
+};
+
+var StackRenderer = {
+ /**
+ * Outputs the memory map associated with this hang report
+ *
+ * @param aDiv Output div
+ */
+ renderMemoryMap: async function StackRenderer_renderMemoryMap(
+ aDiv,
+ memoryMap
+ ) {
+ let memoryMapTitleElement = document.createElement("span");
+ document.l10n.setAttributes(
+ memoryMapTitleElement,
+ "about-telemetry-memory-map-title"
+ );
+ aDiv.appendChild(memoryMapTitleElement);
+ aDiv.appendChild(document.createElement("br"));
+
+ for (let currentModule of memoryMap) {
+ aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
+ aDiv.appendChild(document.createElement("br"));
+ }
+
+ aDiv.appendChild(document.createElement("br"));
+ },
+
+ /**
+ * Outputs the raw PCs from the hang's stack
+ *
+ * @param aDiv Output div
+ * @param aStack Array of PCs from the hang stack
+ */
+ renderStack: function StackRenderer_renderStack(aDiv, aStack) {
+ let stackTitleElement = document.createElement("span");
+ document.l10n.setAttributes(
+ stackTitleElement,
+ "about-telemetry-stack-title"
+ );
+ aDiv.appendChild(stackTitleElement);
+ let stackText = " " + aStack.join(" ");
+ aDiv.appendChild(document.createTextNode(stackText));
+
+ aDiv.appendChild(document.createElement("br"));
+ aDiv.appendChild(document.createElement("br"));
+ },
+ renderStacks: function StackRenderer_renderStacks(
+ aPrefix,
+ aStacks,
+ aMemoryMap,
+ aRenderHeader
+ ) {
+ let div = document.getElementById(aPrefix);
+ removeAllChildNodes(div);
+
+ let fetchE = document.getElementById(aPrefix + "-fetch-symbols");
+ if (fetchE) {
+ fetchE.hidden = false;
+ }
+ let hideE = document.getElementById(aPrefix + "-hide-symbols");
+ if (hideE) {
+ hideE.hidden = true;
+ }
+
+ if (!aStacks.length) {
+ return;
+ }
+
+ setHasData(aPrefix + "-section", true);
+
+ this.renderMemoryMap(div, aMemoryMap);
+
+ for (let i = 0; i < aStacks.length; ++i) {
+ let stack = aStacks[i];
+ aRenderHeader(i);
+ this.renderStack(div, stack);
+ }
+ },
+
+ /**
+ * Renders the title of the stack: e.g. "Late Write #1" or
+ * "Hang Report #1 (6 seconds)".
+ *
+ * @param aDivId The id of the div to append the header to.
+ * @param aL10nId The l10n id of the message to use for the title.
+ * @param aL10nArgs The l10n args for the provided message id.
+ */
+ renderHeader: function StackRenderer_renderHeader(
+ aDivId,
+ aL10nId,
+ aL10nArgs
+ ) {
+ let div = document.getElementById(aDivId);
+
+ let titleElement = document.createElement("span");
+ titleElement.className = "stack-title";
+
+ document.l10n.setAttributes(titleElement, aL10nId, aL10nArgs);
+
+ div.appendChild(titleElement);
+ div.appendChild(document.createElement("br"));
+ },
+};
+
+var RawPayloadData = {
+ /**
+ * Renders the raw pyaload.
+ */
+ render(aPing) {
+ setHasData("raw-payload-section", true);
+ let pre = document.getElementById("raw-payload-data");
+ pre.textContent = JSON.stringify(aPing.payload, null, 2);
+ },
+
+ attachObservers() {
+ document
+ .getElementById("payload-json-viewer")
+ .addEventListener("click", e => {
+ openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2));
+ });
+ },
+};
+
+function SymbolicationRequest(
+ aPrefix,
+ aRenderHeader,
+ aMemoryMap,
+ aStacks,
+ aDurations = null
+) {
+ this.prefix = aPrefix;
+ this.renderHeader = aRenderHeader;
+ this.memoryMap = aMemoryMap;
+ this.stacks = aStacks;
+ this.durations = aDurations;
+}
+/**
+ * A callback for onreadystatechange. It replaces the numeric stack with
+ * the symbolicated one returned by the symbolication server.
+ */
+SymbolicationRequest.prototype.handleSymbolResponse =
+ async function SymbolicationRequest_handleSymbolResponse() {
+ if (this.symbolRequest.readyState != 4) {
+ return;
+ }
+
+ let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
+ fetchElement.hidden = true;
+ let hideElement = document.getElementById(this.prefix + "-hide-symbols");
+ hideElement.hidden = false;
+ let div = document.getElementById(this.prefix);
+ removeAllChildNodes(div);
+ let errorMessage = await document.l10n.formatValue(
+ "about-telemetry-error-fetching-symbols"
+ );
+
+ if (this.symbolRequest.status != 200) {
+ div.appendChild(document.createTextNode(errorMessage));
+ return;
+ }
+
+ let jsonResponse = {};
+ try {
+ jsonResponse = JSON.parse(this.symbolRequest.responseText);
+ } catch (e) {
+ div.appendChild(document.createTextNode(errorMessage));
+ return;
+ }
+
+ for (let i = 0; i < jsonResponse.length; ++i) {
+ let stack = jsonResponse[i];
+ this.renderHeader(i, this.durations);
+
+ for (let symbol of stack) {
+ div.appendChild(document.createTextNode(symbol));
+ div.appendChild(document.createElement("br"));
+ }
+ div.appendChild(document.createElement("br"));
+ }
+ };
+/**
+ * Send a request to the symbolication server to symbolicate this stack.
+ */
+SymbolicationRequest.prototype.fetchSymbols =
+ function SymbolicationRequest_fetchSymbols() {
+ let symbolServerURI = Preferences.get(
+ PREF_SYMBOL_SERVER_URI,
+ DEFAULT_SYMBOL_SERVER_URI
+ );
+ let request = {
+ memoryMap: this.memoryMap,
+ stacks: this.stacks,
+ version: 3,
+ };
+ let requestJSON = JSON.stringify(request);
+
+ this.symbolRequest = new XMLHttpRequest();
+ this.symbolRequest.open("POST", symbolServerURI, true);
+ this.symbolRequest.setRequestHeader("Content-type", "application/json");
+ this.symbolRequest.setRequestHeader("Content-length", requestJSON.length);
+ this.symbolRequest.setRequestHeader("Connection", "close");
+ this.symbolRequest.onreadystatechange =
+ this.handleSymbolResponse.bind(this);
+ this.symbolRequest.send(requestJSON);
+ };
+
+var Histogram = {
+ /**
+ * Renders a single Telemetry histogram
+ *
+ * @param aParent Parent element
+ * @param aName Histogram name
+ * @param aHgram Histogram information
+ * @param aOptions Object with render options
+ * * exponential: bars follow logarithmic scale
+ */
+ render: function Histogram_render(aParent, aName, aHgram, aOptions) {
+ let options = aOptions || {};
+ let hgram = this.processHistogram(aHgram, aName);
+
+ let outerDiv = document.createElement("div");
+ outerDiv.className = "histogram";
+ outerDiv.id = aName;
+
+ let divTitle = document.createElement("div");
+ divTitle.classList.add("histogram-title");
+ divTitle.appendChild(document.createTextNode(aName));
+ outerDiv.appendChild(divTitle);
+
+ let divStats = document.createElement("div");
+ divStats.classList.add("histogram-stats");
+
+ let histogramStatsArgs = {
+ sampleCount: hgram.sample_count,
+ prettyAverage: hgram.pretty_average,
+ sum: hgram.sum,
+ };
+
+ document.l10n.setAttributes(
+ divStats,
+ "about-telemetry-histogram-stats",
+ histogramStatsArgs
+ );
+
+ if (isRTL()) {
+ hgram.values.reverse();
+ }
+
+ let textData = this.renderValues(outerDiv, hgram, options);
+
+ // The 'Copy' button contains the textual data, copied to clipboard on click
+ let copyButton = document.createElement("button");
+ copyButton.className = "copy-node";
+ document.l10n.setAttributes(copyButton, "about-telemetry-histogram-copy");
+
+ copyButton.addEventListener("click", async function () {
+ let divStatsString = await document.l10n.formatValue(
+ "about-telemetry-histogram-stats",
+ histogramStatsArgs
+ );
+ copyButton.histogramText =
+ aName + EOL + divStatsString + EOL + EOL + textData;
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(this.histogramText);
+ });
+ outerDiv.appendChild(copyButton);
+
+ aParent.appendChild(outerDiv);
+ return outerDiv;
+ },
+
+ processHistogram(aHgram, aName) {
+ const values = Object.keys(aHgram.values).map(k => aHgram.values[k]);
+ if (!values.length) {
+ // If we have no values collected for this histogram, just return
+ // zero values so we still render it.
+ return {
+ values: [],
+ pretty_average: 0,
+ max: 0,
+ sample_count: 0,
+ sum: 0,
+ };
+ }
+
+ const sample_count = values.reduceRight((a, b) => a + b);
+ const average = Math.round((aHgram.sum * 10) / sample_count) / 10;
+ const max_value = Math.max(...values);
+
+ const labelledValues = Object.keys(aHgram.values).map(k => [
+ Number(k),
+ aHgram.values[k],
+ ]);
+
+ let result = {
+ values: labelledValues,
+ pretty_average: average,
+ max: max_value,
+ sample_count,
+ sum: aHgram.sum,
+ };
+
+ return result;
+ },
+
+ /**
+ * Return a non-negative, logarithmic representation of a non-negative number.
+ * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
+ *
+ * @param aNumber Non-negative number
+ */
+ getLogValue(aNumber) {
+ return Math.max(0, Math.log10(aNumber) + 1);
+ },
+
+ /**
+ * Create histogram HTML bars, also returns a textual representation
+ * Both aMaxValue and aSumValues must be positive.
+ * Values are assumed to use 0 as baseline.
+ *
+ * @param aDiv Outer parent div
+ * @param aHgram The histogram data
+ * @param aOptions Object with render options (@see #render)
+ */
+ renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
+ let text = "";
+ // If the last label is not the longest string, alignment will break a little
+ let labelPadTo = 0;
+ if (aHgram.values.length) {
+ labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
+ }
+ let maxBarValue = aOptions.exponential
+ ? this.getLogValue(aHgram.max)
+ : aHgram.max;
+
+ for (let [label, value] of aHgram.values) {
+ label = String(label);
+ let barValue = aOptions.exponential ? this.getLogValue(value) : value;
+
+ // Create a text representation: <right-aligned-label> |<bar-of-#><value> <percentage>
+ text +=
+ EOL +
+ " ".repeat(Math.max(0, labelPadTo - label.length)) +
+ label + // Right-aligned label
+ " |" +
+ "#".repeat(Math.round((MAX_BAR_CHARS * barValue) / maxBarValue)) + // Bar
+ " " +
+ value + // Value
+ " " +
+ Math.round((100 * value) / aHgram.sample_count) +
+ "%"; // Percentage
+
+ // Construct the HTML labels + bars
+ let belowEm =
+ Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
+ let aboveEm = MAX_BAR_HEIGHT - belowEm;
+
+ let barDiv = document.createElement("div");
+ barDiv.className = "bar";
+ barDiv.style.paddingTop = aboveEm + "em";
+
+ // Add value label or an nbsp if no value
+ barDiv.appendChild(document.createTextNode(value ? value : "\u00A0"));
+
+ // Create the blue bar
+ let bar = document.createElement("div");
+ bar.className = "bar-inner";
+ bar.style.height = belowEm + "em";
+ barDiv.appendChild(bar);
+
+ // Add a special class to move the text down to prevent text overlap
+ if (label.length > 3) {
+ bar.classList.add("long-label");
+ }
+ // Add bucket label
+ barDiv.appendChild(document.createTextNode(label));
+
+ aDiv.appendChild(barDiv);
+ }
+
+ return text.substr(EOL.length); // Trim the EOL before the first line
+ },
+};
+
+var Search = {
+ HASH_SEARCH: "search=",
+
+ // A list of ids of sections that do not support search.
+ blacklist: ["late-writes-section", "raw-payload-section"],
+
+ // Pass if: all non-empty array items match (case-sensitive)
+ isPassText(subject, filter) {
+ for (let item of filter) {
+ if (item.length && !subject.includes(item)) {
+ return false; // mismatch and not a spurious space
+ }
+ }
+ return true;
+ },
+
+ isPassRegex(subject, filter) {
+ return filter.test(subject);
+ },
+
+ chooseFilter(filterText) {
+ let filter = filterText.toString();
+ // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
+ let isPassFunc; // filter function, set once, then applied to all elements
+ filter = filter.trim();
+ if (filter[0] != "/") {
+ // Plain text: case insensitive, AND if multi-string
+ isPassFunc = this.isPassText;
+ filter = filter.toLowerCase().split(" ");
+ } else {
+ isPassFunc = this.isPassRegex;
+ var r = filter.match(/^\/(.*)\/(i?)$/);
+ try {
+ filter = RegExp(r[1], r[2]);
+ } catch (e) {
+ // Incomplete or bad RegExp - always no match
+ isPassFunc = function () {
+ return false;
+ };
+ }
+ }
+ return [isPassFunc, filter];
+ },
+
+ filterTextRows(table, filterText) {
+ let [isPassFunc, filter] = this.chooseFilter(filterText);
+ let allElementHidden = true;
+
+ let needLowerCase = isPassFunc === this.isPassText;
+ let elements = table.rows;
+ for (let element of elements) {
+ if (element.firstChild.nodeName == "th") {
+ continue;
+ }
+ for (let cell of element.children) {
+ let subject = needLowerCase
+ ? cell.textContent.toLowerCase()
+ : cell.textContent;
+ element.hidden = !isPassFunc(subject, filter);
+ if (!element.hidden) {
+ if (allElementHidden) {
+ allElementHidden = false;
+ }
+ // Don't need to check the rest of this row.
+ break;
+ }
+ }
+ }
+ // Unhide the first row:
+ if (!allElementHidden) {
+ table.rows[0].hidden = false;
+ }
+ return allElementHidden;
+ },
+
+ filterElements(elements, filterText) {
+ let [isPassFunc, filter] = this.chooseFilter(filterText);
+ let allElementHidden = true;
+
+ let needLowerCase = isPassFunc === this.isPassText;
+ for (let element of elements) {
+ let subject = needLowerCase ? element.id.toLowerCase() : element.id;
+ element.hidden = !isPassFunc(subject, filter);
+ if (allElementHidden && !element.hidden) {
+ allElementHidden = false;
+ }
+ }
+ return allElementHidden;
+ },
+
+ filterKeyedElements(keyedElements, filterText) {
+ let [isPassFunc, filter] = this.chooseFilter(filterText);
+ let allElementsHidden = true;
+
+ let needLowerCase = isPassFunc === this.isPassText;
+ keyedElements.forEach(keyedElement => {
+ let subject = needLowerCase
+ ? keyedElement.key.id.toLowerCase()
+ : keyedElement.key.id;
+ if (!isPassFunc(subject, filter)) {
+ // If the keyedHistogram's name is not matched
+ let allKeyedElementsHidden = true;
+ for (let element of keyedElement.datas) {
+ let subject = needLowerCase ? element.id.toLowerCase() : element.id;
+ let match = isPassFunc(subject, filter);
+ element.hidden = !match;
+ if (match) {
+ allKeyedElementsHidden = false;
+ }
+ }
+ if (allElementsHidden && !allKeyedElementsHidden) {
+ allElementsHidden = false;
+ }
+ keyedElement.key.hidden = allKeyedElementsHidden;
+ } else {
+ // If the keyedHistogram's name is matched
+ allElementsHidden = false;
+ keyedElement.key.hidden = false;
+ for (let element of keyedElement.datas) {
+ element.hidden = false;
+ }
+ }
+ });
+ return allElementsHidden;
+ },
+
+ searchHandler(e) {
+ if (this.idleTimeout) {
+ clearTimeout(this.idleTimeout);
+ }
+ this.idleTimeout = setTimeout(
+ () => Search.search(e.target.value),
+ FILTER_IDLE_TIMEOUT
+ );
+ },
+
+ search(text, sectionParam = null) {
+ let section = sectionParam;
+ if (!section) {
+ let sectionId = document
+ .querySelector(".category.selected")
+ .getAttribute("value");
+ section = document.getElementById(sectionId);
+ }
+ if (Search.blacklist.includes(section.id)) {
+ return false;
+ }
+ let noSearchResults = true;
+ // In the home section, we search all other sections:
+ if (section.id === "home-section") {
+ return this.homeSearch(text);
+ }
+
+ if (section.id === "histograms-section") {
+ let histograms = section.getElementsByClassName("histogram");
+ noSearchResults = this.filterElements(histograms, text);
+ } else if (section.id === "keyed-histograms-section") {
+ let keyedElements = [];
+ let keyedHistograms = section.getElementsByClassName("keyed-histogram");
+ for (let key of keyedHistograms) {
+ let datas = key.getElementsByClassName("histogram");
+ keyedElements.push({ key, datas });
+ }
+ noSearchResults = this.filterKeyedElements(keyedElements, text);
+ } else if (section.id === "keyed-scalars-section") {
+ let keyedElements = [];
+ let keyedScalars = section.getElementsByClassName("keyed-scalar");
+ for (let key of keyedScalars) {
+ let datas = key.querySelector("table").rows;
+ keyedElements.push({ key, datas });
+ }
+ noSearchResults = this.filterKeyedElements(keyedElements, text);
+ } else if (section.matches(".text-search")) {
+ let tables = section.querySelectorAll("table");
+ for (let table of tables) {
+ // If we unhide anything, flip noSearchResults to
+ // false so we don't show the "no results" bits.
+ if (!this.filterTextRows(table, text)) {
+ noSearchResults = false;
+ }
+ }
+ } else if (section.querySelector(".sub-section")) {
+ let keyedSubSections = [];
+ let subsections = section.querySelectorAll(".sub-section");
+ for (let section of subsections) {
+ let datas = section.querySelector("table").rows;
+ keyedSubSections.push({ key: section, datas });
+ }
+ noSearchResults = this.filterKeyedElements(keyedSubSections, text);
+ } else {
+ let tables = section.querySelectorAll("table");
+ for (let table of tables) {
+ noSearchResults = this.filterElements(table.rows, text);
+ if (table.caption) {
+ table.caption.hidden = noSearchResults;
+ }
+ }
+ }
+
+ changeUrlSearch(text);
+
+ if (!sectionParam) {
+ // If we are not searching in all section.
+ this.updateNoResults(text, noSearchResults);
+ }
+ return noSearchResults;
+ },
+
+ updateNoResults(text, noSearchResults) {
+ document
+ .getElementById("no-search-results")
+ .classList.toggle("hidden", !noSearchResults);
+ if (noSearchResults) {
+ let section = document.querySelector(".category.selected > span");
+ let searchResultsText = document.getElementById("no-search-results-text");
+ if (section.parentElement.id === "category-home") {
+ document.l10n.setAttributes(
+ searchResultsText,
+ "about-telemetry-no-search-results-all",
+ { searchTerms: text }
+ );
+ } else {
+ let sectionName = section.textContent.trim();
+ text === ""
+ ? document.l10n.setAttributes(
+ searchResultsText,
+ "about-telemetry-no-data-to-display",
+ { sectionName }
+ )
+ : document.l10n.setAttributes(
+ searchResultsText,
+ "about-telemetry-no-search-results",
+ { sectionName, currentSearchText: text }
+ );
+ }
+ }
+ },
+
+ resetHome() {
+ document.getElementById("main").classList.remove("search");
+ document.getElementById("no-search-results").classList.add("hidden");
+ adjustHeaderState();
+ Array.from(document.querySelectorAll("section")).forEach(section => {
+ section.classList.toggle("active", section.id == "home-section");
+ });
+ },
+
+ homeSearch(text) {
+ changeUrlSearch(text);
+ removeSearchSectionTitles();
+ if (text === "") {
+ this.resetHome();
+ return;
+ }
+ document.getElementById("main").classList.add("search");
+ adjustHeaderState(text);
+ let noSearchResults = true;
+ Array.from(document.querySelectorAll("section")).forEach(section => {
+ if (section.id == "home-section" || section.id == "raw-payload-section") {
+ section.classList.remove("active");
+ return;
+ }
+ section.classList.add("active");
+ let sectionHidden = this.search(text, section);
+ if (!sectionHidden) {
+ let sectionTitle = document.querySelector(
+ `.category[value="${section.id}"] .category-name`
+ ).textContent;
+ let sectionDataDiv = document.querySelector(
+ `#${section.id}.has-data.active .data`
+ );
+ let titleDiv = document.createElement("h1");
+ titleDiv.classList.add("data", "search-section-title");
+ titleDiv.textContent = sectionTitle;
+ section.insertBefore(titleDiv, sectionDataDiv);
+ noSearchResults = false;
+ } else {
+ // Hide all subsections if the section is hidden
+ let subsections = section.querySelectorAll(".sub-section");
+ for (let subsection of subsections) {
+ subsection.hidden = true;
+ }
+ }
+ });
+ this.updateNoResults(text, noSearchResults);
+ },
+};
+
+/*
+ * Helper function to render JS objects with white space between top level elements
+ * so that they look better in the browser
+ * @param aObject JavaScript object or array to render
+ * @return String
+ */
+function RenderObject(aObject) {
+ let output = "";
+ if (Array.isArray(aObject)) {
+ if (!aObject.length) {
+ return "[]";
+ }
+ output = "[" + JSON.stringify(aObject[0]);
+ for (let i = 1; i < aObject.length; i++) {
+ output += ", " + JSON.stringify(aObject[i]);
+ }
+ return output + "]";
+ }
+ let keys = Object.keys(aObject);
+ if (!keys.length) {
+ return "{}";
+ }
+ output = '{"' + keys[0] + '":\u00A0' + JSON.stringify(aObject[keys[0]]);
+ for (let i = 1; i < keys.length; i++) {
+ output += ', "' + keys[i] + '":\u00A0' + JSON.stringify(aObject[keys[i]]);
+ }
+ return output + "}";
+}
+
+var GenericSubsection = {
+ addSubSectionToSidebar(id, title) {
+ let category = document.querySelector("#categories > [value=" + id + "]");
+ category.classList.add("has-subsection");
+ let subCategory = document.createElement("div");
+ subCategory.classList.add("category-subsection");
+ subCategory.setAttribute("value", id + "-" + title);
+ subCategory.addEventListener("click", ev => {
+ let section = ev.target;
+ showSubSection(section);
+ });
+ subCategory.appendChild(document.createTextNode(title));
+ category.appendChild(subCategory);
+ },
+
+ render(data, dataDiv, sectionID) {
+ for (let [title, sectionData] of data) {
+ let hasData = sectionData.size > 0;
+ let s = this.renderSubsectionHeader(title, hasData, sectionID);
+ s.appendChild(this.renderSubsectionData(title, sectionData));
+ dataDiv.appendChild(s);
+ }
+ },
+
+ renderSubsectionHeader(title, hasData, sectionID) {
+ this.addSubSectionToSidebar(sectionID, title);
+ let section = document.createElement("div");
+ section.setAttribute("id", sectionID + "-" + title);
+ section.classList.add("sub-section");
+ if (hasData) {
+ section.classList.add("has-subdata");
+ }
+ return section;
+ },
+
+ renderSubsectionData(title, data) {
+ // Create data container
+ let dataDiv = document.createElement("div");
+ dataDiv.setAttribute("class", "subsection-data subdata");
+ // Instanciate the data
+ let table = GenericTable.render(data);
+ let caption = document.createElement("caption");
+ caption.textContent = title;
+ table.appendChild(caption);
+ dataDiv.appendChild(table);
+
+ return dataDiv;
+ },
+
+ deleteAllSubSections() {
+ let subsections = document.querySelectorAll(".category-subsection");
+ subsections.forEach(el => {
+ el.parentElement.removeChild(el);
+ });
+ },
+};
+
+var GenericTable = {
+ // Returns a table with key and value headers
+ defaultHeadings() {
+ return ["about-telemetry-keys-header", "about-telemetry-values-header"];
+ },
+
+ /**
+ * Returns a n-column table.
+ * @param rows An array of arrays, each containing data to render
+ * for one row.
+ * @param headings The column header strings.
+ */
+ render(rows, headings = this.defaultHeadings()) {
+ let table = document.createElement("table");
+ this.renderHeader(table, headings);
+ this.renderBody(table, rows);
+ return table;
+ },
+
+ /**
+ * Create the table header.
+ * Tabs & newlines added to cells to make it easier to copy-paste.
+ *
+ * @param table Table element
+ * @param headings Array of column header strings.
+ */
+ renderHeader(table, headings) {
+ let headerRow = document.createElement("tr");
+ table.appendChild(headerRow);
+
+ for (let i = 0; i < headings.length; ++i) {
+ let column = document.createElement("th");
+ document.l10n.setAttributes(column, headings[i]);
+ headerRow.appendChild(column);
+ }
+ },
+
+ /**
+ * Create the table body
+ * Tabs & newlines added to cells to make it easier to copy-paste.
+ *
+ * @param table Table element
+ * @param rows An array of arrays, each containing data to render
+ * for one row.
+ */
+ renderBody(table, rows) {
+ for (let row of rows) {
+ row = row.map(value => {
+ // use .valueOf() to unbox Number, String, etc. objects
+ if (
+ value &&
+ typeof value == "object" &&
+ typeof value.valueOf() == "object"
+ ) {
+ return RenderObject(value);
+ }
+ return value;
+ });
+
+ let newRow = document.createElement("tr");
+ newRow.id = row[0];
+ table.appendChild(newRow);
+
+ for (let i = 0; i < row.length; ++i) {
+ let suffix = i == row.length - 1 ? "\n" : "\t";
+ let field = document.createElement("td");
+ field.appendChild(document.createTextNode(row[i] + suffix));
+ newRow.appendChild(field);
+ }
+ }
+ },
+};
+
+var KeyedHistogram = {
+ render(parent, id, keyedHistogram) {
+ let outerDiv = document.createElement("div");
+ outerDiv.className = "keyed-histogram";
+ outerDiv.id = id;
+
+ let divTitle = document.createElement("div");
+ divTitle.classList.add("keyed-title");
+ divTitle.appendChild(document.createTextNode(id));
+ outerDiv.appendChild(divTitle);
+
+ for (let [name, hgram] of Object.entries(keyedHistogram)) {
+ Histogram.render(outerDiv, name, hgram);
+ }
+
+ parent.appendChild(outerDiv);
+ return outerDiv;
+ },
+};
+
+var AddonDetails = {
+ /**
+ * Render the addon details section as a series of headers followed by key/value tables
+ * @param aPing A ping object to render the data from.
+ */
+ render(aPing) {
+ let addonSection = document.getElementById("addon-details");
+ removeAllChildNodes(addonSection);
+ let addonDetails = aPing.payload.addonDetails;
+ const hasData = addonDetails && !!Object.keys(addonDetails).length;
+ setHasData("addon-details-section", hasData);
+ if (!hasData) {
+ return;
+ }
+
+ for (let provider in addonDetails) {
+ let providerSection = document.createElement("caption");
+ document.l10n.setAttributes(
+ providerSection,
+ "about-telemetry-addon-provider",
+ { addonProvider: provider }
+ );
+ let headingStrings = [
+ "about-telemetry-addon-table-id",
+ "about-telemetry-addon-table-details",
+ ];
+ let table = GenericTable.render(
+ explodeObject(addonDetails[provider]),
+ headingStrings
+ );
+ table.appendChild(providerSection);
+ addonSection.appendChild(table);
+ }
+ },
+};
+
+class Section {
+ static renderContent(data, process, div, section) {
+ if (data && Object.keys(data).length) {
+ let s = GenericSubsection.renderSubsectionHeader(process, true, section);
+ let heading = document.createElement("h2");
+ document.l10n.setAttributes(heading, "about-telemetry-process", {
+ process,
+ });
+ s.appendChild(heading);
+
+ this.renderData(data, s);
+
+ div.appendChild(s);
+ let separator = document.createElement("div");
+ separator.classList.add("clearfix");
+ div.appendChild(separator);
+ }
+ }
+
+ /**
+ * Make parent process the first one, content process the second
+ * then sort processes alphabetically
+ */
+ static processesComparator(a, b) {
+ if (a === "parent" || (a === "content" && b !== "parent")) {
+ return -1;
+ } else if (b === "parent" || b === "content") {
+ return 1;
+ } else if (a < b) {
+ return -1;
+ } else if (a > b) {
+ return 1;
+ }
+ return 0;
+ }
+
+ /**
+ * Render sections
+ */
+ static renderSection(divName, section, aPayload) {
+ let div = document.getElementById(divName);
+ removeAllChildNodes(div);
+
+ let data = {};
+ let hasData = false;
+ let selectedStore = getSelectedStore();
+
+ let payload = aPayload.stores;
+
+ let isCurrentPayload = !!payload;
+
+ // Sort processes
+ let sortedProcesses = isCurrentPayload
+ ? Object.keys(payload[selectedStore]).sort(this.processesComparator)
+ : Object.keys(aPayload.processes).sort(this.processesComparator);
+
+ // Render content by process
+ for (const process of sortedProcesses) {
+ data = isCurrentPayload
+ ? this.dataFiltering(payload, selectedStore, process)
+ : this.archivePingDataFiltering(aPayload, process);
+ hasData = hasData || !ObjectUtils.isEmpty(data);
+ this.renderContent(data, process, div, section, this.renderData);
+ }
+ setHasData(section, hasData);
+ }
+}
+
+class Scalars extends Section {
+ /**
+ * Return data from the current ping
+ */
+ static dataFiltering(payload, selectedStore, process) {
+ return payload[selectedStore][process].scalars;
+ }
+
+ /**
+ * Return data from an archived ping
+ */
+ static archivePingDataFiltering(payload, process) {
+ return payload.processes[process].scalars;
+ }
+
+ static renderData(data, div) {
+ const scalarsHeadings = [
+ "about-telemetry-names-header",
+ "about-telemetry-values-header",
+ ];
+ let scalarsTable = GenericTable.render(
+ explodeObject(data),
+ scalarsHeadings
+ );
+ div.appendChild(scalarsTable);
+ }
+
+ /**
+ * Render the scalar data - if present - from the payload in a simple key-value table.
+ * @param aPayload A payload object to render the data from.
+ */
+ static render(aPayload) {
+ const divName = "scalars";
+ const section = "scalars-section";
+ this.renderSection(divName, section, aPayload);
+ }
+}
+
+class KeyedScalars extends Section {
+ /**
+ * Return data from the current ping
+ */
+ static dataFiltering(payload, selectedStore, process) {
+ return payload[selectedStore][process].keyedScalars;
+ }
+
+ /**
+ * Return data from an archived ping
+ */
+ static archivePingDataFiltering(payload, process) {
+ return payload.processes[process].keyedScalars;
+ }
+
+ static renderData(data, div) {
+ const scalarsHeadings = [
+ "about-telemetry-names-header",
+ "about-telemetry-values-header",
+ ];
+ for (let scalarId in data) {
+ // Add the name of the scalar.
+ let container = document.createElement("div");
+ container.classList.add("keyed-scalar");
+ container.id = scalarId;
+ let scalarNameSection = document.createElement("p");
+ scalarNameSection.classList.add("keyed-title");
+ scalarNameSection.appendChild(document.createTextNode(scalarId));
+ container.appendChild(scalarNameSection);
+ // Populate the section with the key-value pairs from the scalar.
+ const table = GenericTable.render(
+ explodeObject(data[scalarId]),
+ scalarsHeadings
+ );
+ container.appendChild(table);
+ div.appendChild(container);
+ }
+ }
+
+ /**
+ * Render the keyed scalar data - if present - from the payload in a simple key-value table.
+ * @param aPayload A payload object to render the data from.
+ */
+ static render(aPayload) {
+ const divName = "keyed-scalars";
+ const section = "keyed-scalars-section";
+ this.renderSection(divName, section, aPayload);
+ }
+}
+
+var Events = {
+ /**
+ * Render the event data - if present - from the payload in a simple table.
+ * @param aPayload A payload object to render the data from.
+ */
+ render(aPayload) {
+ let eventsDiv = document.getElementById("events");
+ removeAllChildNodes(eventsDiv);
+ const headings = [
+ "about-telemetry-time-stamp-header",
+ "about-telemetry-category-header",
+ "about-telemetry-method-header",
+ "about-telemetry-object-header",
+ "about-telemetry-values-header",
+ "about-telemetry-extra-header",
+ ];
+ let payload = aPayload.processes;
+ let hasData = false;
+ if (payload) {
+ for (const process of Object.keys(aPayload.processes)) {
+ let data = aPayload.processes[process].events;
+ if (data && Object.keys(data).length) {
+ hasData = true;
+ let s = GenericSubsection.renderSubsectionHeader(
+ process,
+ true,
+ "events-section"
+ );
+ let heading = document.createElement("h2");
+ heading.textContent = process;
+ s.appendChild(heading);
+ const table = GenericTable.render(data, headings);
+ s.appendChild(table);
+ eventsDiv.appendChild(s);
+ let separator = document.createElement("div");
+ separator.classList.add("clearfix");
+ eventsDiv.appendChild(separator);
+ }
+ }
+ } else {
+ // handle archived ping
+ for (const process of Object.keys(aPayload.events)) {
+ let data = process;
+ if (data && Object.keys(data).length) {
+ hasData = true;
+ let s = GenericSubsection.renderSubsectionHeader(
+ process,
+ true,
+ "events-section"
+ );
+ let heading = document.createElement("h2");
+ heading.textContent = process;
+ s.appendChild(heading);
+ const table = GenericTable.render(data, headings);
+ eventsDiv.appendChild(table);
+ let separator = document.createElement("div");
+ separator.classList.add("clearfix");
+ eventsDiv.appendChild(separator);
+ }
+ }
+ }
+ setHasData("events-section", hasData);
+ },
+};
+
+/**
+ * Helper function for showing either the toggle element or "No data collected" message for a section
+ *
+ * @param aSectionID ID of the section element that needs to be changed
+ * @param aHasData true (default) indicates that toggle should be displayed
+ */
+function setHasData(aSectionID, aHasData) {
+ let sectionElement = document.getElementById(aSectionID);
+ sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
+
+ // Display or Hide the section in the sidebar
+ let sectionCategory = document.querySelector(
+ ".category[value=" + aSectionID + "]"
+ );
+ sectionCategory.classList[aHasData ? "add" : "remove"]("has-data");
+}
+
+/**
+ * Sets l10n attributes based on the Telemetry Server Owner pref.
+ */
+function setupServerOwnerBranding() {
+ let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
+ const elements = [
+ [document.getElementById("page-subtitle"), "about-telemetry-page-subtitle"],
+ ];
+ for (const [elt, l10nName] of elements) {
+ document.l10n.setAttributes(elt, l10nName, {
+ telemetryServerOwner: serverOwner,
+ });
+ }
+}
+
+/**
+ * Display the store selector if we are on one
+ * of the whitelisted sections
+ */
+function displayStoresSelector(selectedSection) {
+ let whitelist = [
+ "scalars-section",
+ "keyed-scalars-section",
+ "histograms-section",
+ "keyed-histograms-section",
+ ];
+ let stores = document.getElementById("stores");
+ stores.hidden = !whitelist.includes(selectedSection);
+ let storesLabel = document.getElementById("storesLabel");
+ storesLabel.hidden = !whitelist.includes(selectedSection);
+}
+
+function refreshSearch() {
+ removeSearchSectionTitles();
+ let selectedSection = document
+ .querySelector(".category.selected")
+ .getAttribute("value");
+ let search = document.getElementById("search");
+ if (!Search.blacklist.includes(selectedSection)) {
+ Search.search(search.value);
+ }
+}
+
+function adjustSearchState() {
+ removeSearchSectionTitles();
+ let selectedSection = document
+ .querySelector(".category.selected")
+ .getAttribute("value");
+ let search = document.getElementById("search");
+ search.value = "";
+ search.hidden = Search.blacklist.includes(selectedSection);
+ document.getElementById("no-search-results").classList.add("hidden");
+ Search.search(""); // reinitialize search state.
+}
+
+function removeSearchSectionTitles() {
+ for (let sectionTitleDiv of Array.from(
+ document.getElementsByClassName("search-section-title")
+ )) {
+ sectionTitleDiv.remove();
+ }
+}
+
+function adjustSection() {
+ let selectedCategory = document.querySelector(".category.selected");
+ if (!selectedCategory.classList.contains("has-data")) {
+ PingPicker._showStructuredPingData();
+ }
+}
+
+function adjustHeaderState(title = null) {
+ let selected = document.querySelector(".category.selected .category-name");
+ let selectedTitle = selected.textContent.trim();
+ let sectionTitle = document.getElementById("sectionTitle");
+ if (title !== null) {
+ document.l10n.setAttributes(
+ sectionTitle,
+ "about-telemetry-results-for-search",
+ { searchTerms: title }
+ );
+ } else {
+ sectionTitle.textContent = selectedTitle;
+ }
+ let search = document.getElementById("search");
+ if (selected.parentElement.id === "category-home") {
+ document.l10n.setAttributes(
+ search,
+ "about-telemetry-filter-all-placeholder"
+ );
+ } else {
+ document.l10n.setAttributes(search, "about-telemetry-filter-placeholder", {
+ selectedTitle,
+ });
+ }
+}
+
+/**
+ * Change the url according to the current section displayed
+ * e.g about:telemetry#general-data
+ */
+function changeUrlPath(selectedSection, subSection) {
+ if (subSection) {
+ let hash = window.location.hash.split("_")[0] + "_" + selectedSection;
+ window.location.hash = hash;
+ } else {
+ window.location.hash = selectedSection.replace("-section", "-tab");
+ }
+}
+
+/**
+ * Change the url according to the current search text
+ */
+function changeUrlSearch(searchText) {
+ let currentHash = window.location.hash;
+ let hashWithoutSearch = currentHash.split(Search.HASH_SEARCH)[0];
+ let hash = "";
+
+ if (!currentHash && !searchText) {
+ return;
+ }
+ if (!currentHash.includes(Search.HASH_SEARCH) && hashWithoutSearch) {
+ hashWithoutSearch += "_";
+ }
+ if (searchText) {
+ hash =
+ hashWithoutSearch + Search.HASH_SEARCH + searchText.replace(/ /g, "+");
+ } else if (hashWithoutSearch) {
+ hash = hashWithoutSearch.slice(0, hashWithoutSearch.length - 1);
+ }
+
+ window.location.hash = hash;
+}
+
+/**
+ * Change the section displayed
+ */
+function show(selected) {
+ let selectedValue = selected.getAttribute("value");
+ if (selectedValue === "raw-json-viewer") {
+ openJsonInFirefoxJsonViewer(JSON.stringify(gPingData, null, 2));
+ return;
+ }
+
+ let selected_section = document.getElementById(selectedValue);
+ let subsections = selected_section.querySelectorAll(".sub-section");
+ if (selected.classList.contains("has-subsection")) {
+ for (let subsection of selected.children) {
+ subsection.classList.remove("selected");
+ }
+ }
+ if (subsections) {
+ for (let subsection of subsections) {
+ subsection.hidden = false;
+ }
+ }
+
+ let current_button = document.querySelector(".category.selected");
+ if (current_button == selected) {
+ return;
+ }
+ current_button.classList.remove("selected");
+ selected.classList.add("selected");
+
+ document.querySelectorAll("section").forEach(section => {
+ section.classList.remove("active");
+ });
+ selected_section.classList.add("active");
+
+ adjustHeaderState();
+ displayStoresSelector(selectedValue);
+ adjustSearchState();
+ changeUrlPath(selectedValue);
+}
+
+function showSubSection(selected) {
+ if (!selected) {
+ return;
+ }
+ let current_selection = document.querySelector(
+ ".category-subsection.selected"
+ );
+ if (current_selection) {
+ current_selection.classList.remove("selected");
+ }
+ selected.classList.add("selected");
+
+ let section = document.getElementById(selected.getAttribute("value"));
+ section.parentElement.childNodes.forEach(element => {
+ element.hidden = true;
+ });
+ section.hidden = false;
+
+ let title =
+ selected.parentElement.querySelector(".category-name").textContent;
+ let subsection = selected.textContent;
+ document.getElementById("sectionTitle").textContent =
+ title + " - " + subsection;
+ changeUrlPath(subsection, true);
+}
+
+/**
+ * Initializes load/unload, pref change and mouse-click listeners
+ */
+function setupListeners() {
+ Settings.attachObservers();
+ PingPicker.attachObservers();
+ RawPayloadData.attachObservers();
+
+ let menu = document.getElementById("categories");
+ menu.addEventListener("click", e => {
+ if (e.target && e.target.parentNode == menu) {
+ show(e.target);
+ }
+ });
+
+ let search = document.getElementById("search");
+ search.addEventListener("input", Search.searchHandler);
+
+ document
+ .getElementById("late-writes-fetch-symbols")
+ .addEventListener("click", function () {
+ if (!gPingData) {
+ return;
+ }
+
+ let lateWrites = gPingData.payload.lateWrites;
+ let req = new SymbolicationRequest(
+ "late-writes",
+ LateWritesSingleton.renderHeader,
+ lateWrites.memoryMap,
+ lateWrites.stacks
+ );
+ req.fetchSymbols();
+ });
+
+ document
+ .getElementById("late-writes-hide-symbols")
+ .addEventListener("click", function () {
+ if (!gPingData) {
+ return;
+ }
+
+ LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
+ });
+}
+
+// Restores the sections states
+function urlSectionRestore(hash) {
+ if (hash) {
+ let section = hash.replace("-tab", "-section");
+ let subsection = section.split("_")[1];
+ section = section.split("_")[0];
+ let category = document.querySelector(".category[value=" + section + "]");
+ if (category) {
+ show(category);
+ if (subsection) {
+ let selector =
+ ".category-subsection[value=" + section + "-" + subsection + "]";
+ let subcategory = document.querySelector(selector);
+ showSubSection(subcategory);
+ }
+ }
+ }
+}
+
+// Restore sections states and search terms
+function urlStateRestore() {
+ let hash = window.location.hash;
+ let searchQuery = "";
+ if (hash) {
+ hash = hash.slice(1);
+ if (hash.includes(Search.HASH_SEARCH)) {
+ searchQuery = hash.split(Search.HASH_SEARCH)[1].replace(/[+]/g, " ");
+ hash = hash.split(Search.HASH_SEARCH)[0];
+ }
+ urlSectionRestore(hash);
+ }
+ if (searchQuery) {
+ let search = document.getElementById("search");
+ search.value = searchQuery;
+ }
+}
+
+function openJsonInFirefoxJsonViewer(json) {
+ json = unescape(encodeURIComponent(json));
+ try {
+ window.open("data:application/json;base64," + btoa(json));
+ } catch (e) {
+ show(document.querySelector(".category[value=raw-payload-section]"));
+ }
+}
+
+function onLoad() {
+ window.removeEventListener("load", onLoad);
+ // Set the text in the page header and elsewhere that needs the server owner.
+ setupServerOwnerBranding();
+
+ // Set up event listeners
+ setupListeners();
+
+ // Render settings.
+ Settings.render();
+
+ adjustHeaderState();
+
+ urlStateRestore();
+
+ // Update ping data when async Telemetry init is finished.
+ Telemetry.asyncFetchTelemetryData(async () => {
+ await PingPicker.update();
+ });
+}
+
+var LateWritesSingleton = {
+ renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
+ StackRenderer.renderHeader(
+ "late-writes",
+ "about-telemetry-late-writes-title",
+ { lateWriteCount: aIndex + 1 }
+ );
+ },
+
+ renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
+ let hasData = !!(
+ lateWrites &&
+ lateWrites.stacks &&
+ lateWrites.stacks.length
+ );
+ setHasData("late-writes-section", hasData);
+ if (!hasData) {
+ return;
+ }
+
+ let stacks = lateWrites.stacks;
+ let memoryMap = lateWrites.memoryMap;
+ StackRenderer.renderStacks(
+ "late-writes",
+ stacks,
+ memoryMap,
+ LateWritesSingleton.renderHeader
+ );
+ },
+};
+
+class HistogramSection extends Section {
+ /**
+ * Return data from the current ping
+ */
+ static dataFiltering(payload, selectedStore, process) {
+ return payload[selectedStore][process].histograms;
+ }
+
+ /**
+ * Return data from an archived ping
+ */
+ static archivePingDataFiltering(payload, process) {
+ if (process === "parent") {
+ return payload.histograms;
+ }
+ return payload.processes[process].histograms;
+ }
+
+ static renderData(data, div) {
+ for (let [hName, hgram] of Object.entries(data)) {
+ Histogram.render(div, hName, hgram, { unpacked: true });
+ }
+ }
+
+ static render(aPayload) {
+ const divName = "histograms";
+ const section = "histograms-section";
+ this.renderSection(divName, section, aPayload);
+ }
+}
+
+class KeyedHistogramSection extends Section {
+ /**
+ * Return data from the current ping
+ */
+ static dataFiltering(payload, selectedStore, process) {
+ return payload[selectedStore][process].keyedHistograms;
+ }
+
+ /**
+ * Return data from an archived ping
+ */
+ static archivePingDataFiltering(payload, process) {
+ if (process === "parent") {
+ return payload.keyedHistograms;
+ }
+ return payload.processes[process].keyedHistograms;
+ }
+
+ static renderData(data, div) {
+ for (let [id, keyed] of Object.entries(data)) {
+ KeyedHistogram.render(div, id, keyed, { unpacked: true });
+ }
+ }
+
+ static render(aPayload) {
+ const divName = "keyed-histograms";
+ const section = "keyed-histograms-section";
+ this.renderSection(divName, section, aPayload);
+ }
+}
+
+var SessionInformation = {
+ render(aPayload) {
+ let infoSection = document.getElementById("session-info");
+ removeAllChildNodes(infoSection);
+
+ let hasData = !!Object.keys(aPayload.info).length;
+ setHasData("session-info-section", hasData);
+
+ if (hasData) {
+ const table = GenericTable.render(explodeObject(aPayload.info));
+ infoSection.appendChild(table);
+ }
+ },
+};
+
+var SimpleMeasurements = {
+ render(aPayload) {
+ let simpleSection = document.getElementById("simple-measurements");
+ removeAllChildNodes(simpleSection);
+
+ let simpleMeasurements = this.sortStartupMilestones(
+ aPayload.simpleMeasurements
+ );
+ let hasData = !!Object.keys(simpleMeasurements).length;
+ setHasData("simple-measurements-section", hasData);
+
+ if (hasData) {
+ const table = GenericTable.render(explodeObject(simpleMeasurements));
+ simpleSection.appendChild(table);
+ }
+ },
+
+ /**
+ * Helper function for sorting the startup milestones in the Simple Measurements
+ * section into temporal order.
+ *
+ * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
+ * @return Sorted measurements
+ */
+ sortStartupMilestones(aSimpleMeasurements) {
+ const telemetryTimestamps = TelemetryTimestamps.get();
+ let startupEvents = Services.startup.getStartupInfo();
+ delete startupEvents.process;
+
+ function keyIsMilestone(k) {
+ return k in startupEvents || k in telemetryTimestamps;
+ }
+
+ let sortedKeys = Object.keys(aSimpleMeasurements);
+
+ // Sort the measurements, with startup milestones at the front + ordered by time
+ sortedKeys.sort(function keyCompare(keyA, keyB) {
+ let isKeyAMilestone = keyIsMilestone(keyA);
+ let isKeyBMilestone = keyIsMilestone(keyB);
+
+ // First order by startup vs non-startup measurement
+ if (isKeyAMilestone && !isKeyBMilestone) {
+ return -1;
+ }
+ if (!isKeyAMilestone && isKeyBMilestone) {
+ return 1;
+ }
+ // Don't change order of non-startup measurements
+ if (!isKeyAMilestone && !isKeyBMilestone) {
+ return 0;
+ }
+
+ // If both keys are startup measurements, order them by value
+ return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
+ });
+
+ // Insert measurements into a result object in sort-order
+ let result = {};
+ for (let key of sortedKeys) {
+ result[key] = aSimpleMeasurements[key];
+ }
+
+ return result;
+ },
+};
+
+/**
+ * Render stores options
+ */
+function renderStoreList(payload) {
+ let storeSelect = document.getElementById("stores");
+ let storesLabel = document.getElementById("storesLabel");
+ removeAllChildNodes(storeSelect);
+
+ if (!("stores" in payload)) {
+ storeSelect.classList.add("hidden");
+ storesLabel.classList.add("hidden");
+ return;
+ }
+
+ storeSelect.classList.remove("hidden");
+ storesLabel.classList.remove("hidden");
+ storeSelect.disabled = false;
+
+ for (let store of Object.keys(payload.stores)) {
+ let option = document.createElement("option");
+ option.appendChild(document.createTextNode(store));
+ option.setAttribute("value", store);
+ // Select main store by default
+ if (store === "main") {
+ option.selected = true;
+ }
+ storeSelect.appendChild(option);
+ }
+}
+
+/**
+ * Return the selected store
+ */
+function getSelectedStore() {
+ let storeSelect = document.getElementById("stores");
+ let storeSelectedOption = storeSelect.selectedOptions.item(0);
+ let selectedStore =
+ storeSelectedOption !== null
+ ? storeSelectedOption.getAttribute("value")
+ : undefined;
+ return selectedStore;
+}
+
+function togglePingSections(isMainPing) {
+ // We always show the sections that are "common" to all pings.
+ let commonSections = new Set([
+ "heading",
+ "home-section",
+ "general-data-section",
+ "environment-data-section",
+ "raw-json-viewer",
+ ]);
+
+ let elements = document.querySelectorAll(".category");
+ for (let section of elements) {
+ if (commonSections.has(section.getAttribute("value"))) {
+ continue;
+ }
+ // Only show the raw payload for non main ping.
+ if (section.getAttribute("value") == "raw-payload-section") {
+ section.classList.toggle("has-data", !isMainPing);
+ } else {
+ section.classList.toggle("has-data", isMainPing);
+ }
+ }
+}
+
+function displayPingData(ping, updatePayloadList = false) {
+ gPingData = ping;
+ try {
+ PingPicker.render();
+ displayRichPingData(ping, updatePayloadList);
+ adjustSection();
+ refreshSearch();
+ } catch (err) {
+ console.log(err);
+ PingPicker._showRawPingData();
+ }
+}
+
+function displayRichPingData(ping, updatePayloadList) {
+ // Update the payload list and store lists
+ if (updatePayloadList) {
+ renderStoreList(ping.payload);
+ }
+
+ // Show general data.
+ GeneralData.render(ping);
+
+ // Show environment data.
+ EnvironmentData.render(ping);
+
+ RawPayloadData.render(ping);
+
+ // We have special rendering code for the payloads from "main" and "event" pings.
+ // For any other pings we just render the raw JSON payload.
+ let isMainPing = ping.type == "main" || ping.type == "saved-session";
+ let isEventPing = ping.type == "event";
+ togglePingSections(isMainPing);
+
+ if (isEventPing) {
+ // Copy the payload, so we don't modify the raw representation
+ // Ensure we always have at least the parent process.
+ let payload = { processes: { parent: {} } };
+ for (let process of Object.keys(ping.payload.events)) {
+ payload.processes[process] = {
+ events: ping.payload.events[process],
+ };
+ }
+
+ // We transformed the actual payload, let's reload the store list if necessary.
+ if (updatePayloadList) {
+ renderStoreList(payload);
+ }
+
+ // Show event data.
+ Events.render(payload);
+ return;
+ }
+
+ if (!isMainPing) {
+ return;
+ }
+
+ // Show slow SQL stats
+ SlowSQL.render(ping);
+
+ // Render Addon details.
+ AddonDetails.render(ping);
+
+ let payload = ping.payload;
+ // Show basic session info gathered
+ SessionInformation.render(payload);
+
+ // Show scalar data.
+ Scalars.render(payload);
+ KeyedScalars.render(payload);
+
+ // Show histogram data
+ HistogramSection.render(payload);
+
+ // Show keyed histogram data
+ KeyedHistogramSection.render(payload);
+
+ // Show event data.
+ Events.render(payload);
+
+ LateWritesSingleton.renderLateWrites(payload.lateWrites);
+
+ // Show simple measurements
+ SimpleMeasurements.render(payload);
+}
+
+window.addEventListener("load", onLoad);