forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			2618 lines
		
	
	
	
		
			76 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2618 lines
		
	
	
	
		
			76 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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, {
 | |
|   ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
 | |
|   Preferences: "resource://gre/modules/Preferences.sys.mjs",
 | |
| });
 | |
| 
 | |
| 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", () => {
 | |
|       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", () => {
 | |
|         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) {
 | |
|     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);
 | 
