forked from mirrors/gecko-dev
		
	 cc6c461108
			
		
	
	
		cc6c461108
		
	
	
	
	
		
			
			This ensures the corresponding section is shown for event pings in about:telemetry, even if there's no actual data for the parent process. This avoids a bigger rewrite of the logic of what sections and processes to display. Differential Revision: https://phabricator.services.mozilla.com/D18805 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			2581 lines
		
	
	
	
		
			84 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2581 lines
		
	
	
	
		
			84 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.import("resource://gre/modules/BrowserUtils.jsm");
 | |
| const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| const {TelemetryTimestamps} = ChromeUtils.import("resource://gre/modules/TelemetryTimestamps.jsm");
 | |
| const {TelemetryController} = ChromeUtils.import("resource://gre/modules/TelemetryController.jsm");
 | |
| const {TelemetryArchive} = ChromeUtils.import("resource://gre/modules/TelemetryArchive.jsm");
 | |
| const {TelemetrySend} = ChromeUtils.import("resource://gre/modules/TelemetrySend.jsm");
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(this, "AppConstants",
 | |
|                                "resource://gre/modules/AppConstants.jsm");
 | |
| ChromeUtils.defineModuleGetter(this, "Preferences",
 | |
|                                "resource://gre/modules/Preferences.jsm");
 | |
| 
 | |
| const Telemetry = Services.telemetry;
 | |
| const bundle = Services.strings.createBundle(
 | |
|   "chrome://global/locale/aboutTelemetry.properties");
 | |
| const brandBundle = Services.strings.createBundle(
 | |
|   "chrome://branding/locale/brand.properties");
 | |
| 
 | |
| // 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://symbols.mozilla.org/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.docShell.rootTreeItem.domWindow;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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 = {
 | |
|   SETTINGS: [
 | |
|     // data upload
 | |
|     {
 | |
|       pref: PREF_FHR_UPLOAD_ENABLED,
 | |
|       defaultPrefValue: false,
 | |
|     },
 | |
|     // extended "Telemetry" recording
 | |
|     {
 | |
|       pref: PREF_TELEMETRY_ENABLED,
 | |
|       defaultPrefValue: false,
 | |
|     },
 | |
|   ],
 | |
| 
 | |
|   attachObservers() {
 | |
|     for (let s of this.SETTINGS) {
 | |
|       let setting = s;
 | |
|       Preferences.observe(setting.pref, this.render, this);
 | |
|     }
 | |
| 
 | |
|     let elements = document.getElementsByClassName("change-data-choices-link");
 | |
|     for (let el of elements) {
 | |
|       el.addEventListener("click", function() {
 | |
|         if (AppConstants.platform == "android") {
 | |
|           var {EventDispatcher} = ChromeUtils.import("resource://gre/modules/Messaging.jsm");
 | |
|           EventDispatcher.instance.sendRequest({
 | |
|             type: "Settings:Show",
 | |
|             resource: "preferences_privacy",
 | |
|           });
 | |
|         } else {
 | |
|           // Show the data choices preferences on desktop.
 | |
|           let mainWindow = getMainWindowWithPreferencesPane();
 | |
|           mainWindow.openPreferences("privacy-reports", { origin: "aboutTelemetry" });
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   detachObservers() {
 | |
|     for (let setting of this.SETTINGS) {
 | |
|       Preferences.ignore(setting.pref, this.render, this);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   getStatusString(enabled) {
 | |
|     let status = bundle.GetStringFromName(enabled ? "telemetryUploadEnabled" : "telemetryUploadDisabled");
 | |
|     return status;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Updates the button & text at the top of the page to reflect Telemetry state.
 | |
|    */
 | |
|   render() {
 | |
|     let settingsExplanation = document.getElementById("settings-explanation");
 | |
|     let uploadEnabled = this.getStatusString(TelemetrySend.sendingEnabled());
 | |
|     let extendedEnabled = Services.telemetry.canRecordExtended;
 | |
|     let collectedData = bundle.GetStringFromName(extendedEnabled ? "prereleaseData" : "releaseData");
 | |
|     let explanation = bundle.GetStringFromName("settingsExplanation");
 | |
| 
 | |
|     let fragment = BrowserUtils.getLocalizedFragment(document, explanation, collectedData, this.convertStringToLink(uploadEnabled));
 | |
|     settingsExplanation.appendChild(fragment);
 | |
| 
 | |
|     this.attachObservers();
 | |
|   },
 | |
| 
 | |
|   convertStringToLink(string) {
 | |
|     let link = document.createElement("a");
 | |
|     link.setAttribute("href", "#");
 | |
|     link.setAttribute("class", "change-data-choices-link");
 | |
|     link.textContent = string;
 | |
|     return link;
 | |
|   },
 | |
| };
 | |
| 
 | |
| var PingPicker = {
 | |
|   viewCurrentPingData: null,
 | |
|   _archivedPings: null,
 | |
|   TYPE_ALL: bundle.GetStringFromName("telemetryPingTypeAll"),
 | |
| 
 | |
|   attachObservers() {
 | |
|     let pingSourceElements = document.getElementsByName("choose-ping-source");
 | |
|     for (let el of pingSourceElements) {
 | |
|       el.addEventListener("change", () => this.onPingSourceChanged());
 | |
|     }
 | |
| 
 | |
|     let displays = document.getElementsByName("choose-ping-display");
 | |
|     for (let el of displays) {
 | |
|       el.addEventListener("change", () => this.onPingDisplayChanged());
 | |
|     }
 | |
| 
 | |
|     document.getElementById("show-subsession-data").addEventListener("change", () => {
 | |
|       this._updateCurrentPingData();
 | |
|     });
 | |
| 
 | |
|     document.getElementById("choose-ping-id").addEventListener("change", () => {
 | |
|       this._updateArchivedPingData();
 | |
|     });
 | |
|     document.getElementById("choose-ping-type").addEventListener("change", () => {
 | |
|       this.filterDisplayedPings();
 | |
|     });
 | |
| 
 | |
| 
 | |
|     document.getElementById("newer-ping")
 | |
|             .addEventListener("click", () => this._movePingIndex(-1));
 | |
|     document.getElementById("older-ping")
 | |
|             .addEventListener("click", () => this._movePingIndex(1));
 | |
| 
 | |
|     let pingPickerNeedHide = false;
 | |
|     let pingPicker = document.getElementById("ping-picker");
 | |
|     pingPicker.addEventListener("mouseenter", () => pingPickerNeedHide = false);
 | |
|     pingPicker.addEventListener("mouseleave", () => pingPickerNeedHide = true);
 | |
|     document.addEventListener("click", (ev) => {
 | |
|       if (pingPickerNeedHide) {
 | |
|         pingPicker.classList.add("hidden");
 | |
|       }
 | |
|     });
 | |
|     document.getElementById("processes")
 | |
|             .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() {
 | |
|     let pings = bundle.GetStringFromName("pingExplanationLink");
 | |
|     let pingLink = document.createElement("a");
 | |
|     pingLink.setAttribute("href", "https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/concepts/pings.html");
 | |
|     pingLink.textContent = pings;
 | |
|     let pingName = this._getSelectedPingName();
 | |
|     let pingNameSpan = document.createElement("span");
 | |
|     pingNameSpan.setAttribute("class", "change-ping");
 | |
| 
 | |
|     // 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 fragment;
 | |
|     if (!this.viewCurrentPingData) {
 | |
|       // Change sidebar heading text.
 | |
|       pingDate.textContent = pingName;
 | |
|       pingDate.setAttribute("title", pingName);
 | |
|       let pingTypeText = this._getSelectedPingType();
 | |
|       controls.classList.remove("hidden");
 | |
|       pingType.textContent = pingTypeText;
 | |
| 
 | |
|       // Change home page text.
 | |
|       pingName = bundle.formatStringFromName("namedPing", [pingName, pingTypeText], 2);
 | |
|       pingNameSpan.textContent = pingName;
 | |
|       let explanation = bundle.GetStringFromName("pingDetails");
 | |
|       fragment = BrowserUtils.getLocalizedFragment(document, explanation, pingLink, pingNameSpan);
 | |
|     } else {
 | |
|       // Change sidebar heading text.
 | |
|       controls.classList.add("hidden");
 | |
|       pingType.textContent = bundle.GetStringFromName("currentPingSidebar");
 | |
| 
 | |
|       // Change home page text.
 | |
|       pingNameSpan.textContent = pingName;
 | |
|       let explanation = bundle.GetStringFromName("pingDetailsCurrent");
 | |
|       fragment = BrowserUtils.getLocalizedFragment(document, explanation, pingLink, pingNameSpan);
 | |
|     }
 | |
| 
 | |
|     let pingExplanation = document.getElementById("ping-explanation");
 | |
|     removeAllChildNodes(pingExplanation);
 | |
|     pingExplanation.appendChild(fragment);
 | |
|     pingExplanation.querySelector(".change-ping").addEventListener("click", (ev) => {
 | |
|       document.getElementById("ping-picker").classList.remove("hidden");
 | |
|       ev.stopPropagation();
 | |
|     });
 | |
| 
 | |
|     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 == 0);
 | |
|     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_RELEASE_CHANNEL_OPTIN, 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() {
 | |
|     if (this.viewCurrentPingData) return bundle.GetStringFromName("currentPing");
 | |
| 
 | |
|     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 = [
 | |
|       "namesHeader",
 | |
|       "valuesHeader",
 | |
|     ].map(h => bundle.GetStringFromName(h));
 | |
| 
 | |
|     // 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);
 | |
|   },
 | |
| 
 | |
|   renderPersona(addonObj, addonSection, sectionTitle) {
 | |
|     let table = document.createElement("table");
 | |
|     table.setAttribute("id", sectionTitle);
 | |
|     this.appendAddonSubsectionTitle(sectionTitle, table);
 | |
|     this.appendRow(table, "persona", addonObj.persona);
 | |
|     addonSection.appendChild(table);
 | |
|   },
 | |
| 
 | |
|   renderActivePlugins(addonObj, addonSection, sectionTitle) {
 | |
|     let table = document.createElement("table");
 | |
|     table.setAttribute("id", sectionTitle);
 | |
|     this.appendAddonSubsectionTitle(sectionTitle, table);
 | |
| 
 | |
|     for (let plugin of addonObj) {
 | |
|       let data = explodeObject(plugin);
 | |
|       this.appendHeadingName(table, data.get("name"));
 | |
| 
 | |
|       for (let [key, value] of data) {
 | |
|         this.appendRow(table, key, value);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     addonSection.appendChild(table);
 | |
|   },
 | |
| 
 | |
|   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.renderActivePlugins(addons.activePlugins, addonSection, "activePlugins");
 | |
|     this.renderKeyValueObject(addons.theme, addonSection, "theme");
 | |
|     this.renderAddonsObject(addons.activeGMPlugins, addonSection, "activeGMPlugins");
 | |
|     this.renderPersona(addons, addonSection, "persona");
 | |
| 
 | |
|     let hasAddonData = Object.keys(ping.environment.addons).length > 0;
 | |
|     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 = {
 | |
| 
 | |
|   slowSqlHits: bundle.GetStringFromName("slowSqlHits"),
 | |
| 
 | |
|   slowSqlAverage: bundle.GetStringFromName("slowSqlAverage"),
 | |
| 
 | |
|   slowSqlStatement: bundle.GetStringFromName("slowSqlStatement"),
 | |
| 
 | |
|   mainThreadTitle: bundle.GetStringFromName("slowSqlMain"),
 | |
| 
 | |
|   otherThreadTitle: bundle.GetStringFromName("slowSqlOther"),
 | |
| 
 | |
|   /**
 | |
|    * 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, this.mainThreadTitle);
 | |
|       this.renderTable(table, mainThread);
 | |
| 
 | |
|       slowSqlDiv.appendChild(table);
 | |
|     }
 | |
| 
 | |
|     // Other threads
 | |
|     if (otherThreadCount > 0) {
 | |
|       let table = document.createElement("table");
 | |
|       this.renderTableHeader(table, this.otherThreadTitle);
 | |
|       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, aTitle) {
 | |
|     let caption = document.createElement("caption");
 | |
|     caption.appendChild(document.createTextNode(aTitle + "\n"));
 | |
|     aTable.appendChild(caption);
 | |
| 
 | |
|     let headings = document.createElement("tr");
 | |
|     this.appendColumn(headings, "th", this.slowSqlHits + "\t");
 | |
|     this.appendColumn(headings, "th", this.slowSqlAverage + "\t");
 | |
|     this.appendColumn(headings, "th", this.slowSqlStatement + "\n");
 | |
|     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);
 | |
|     let colTextElement = document.createTextNode(aColText);
 | |
|     colElement.appendChild(colTextElement);
 | |
|     aRowElement.appendChild(colElement);
 | |
|   },
 | |
| };
 | |
| 
 | |
| var StackRenderer = {
 | |
| 
 | |
|   stackTitle: bundle.GetStringFromName("stackTitle"),
 | |
| 
 | |
|   memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"),
 | |
| 
 | |
|   /**
 | |
|    * Outputs the memory map associated with this hang report
 | |
|    *
 | |
|    * @param aDiv Output div
 | |
|    */
 | |
|   renderMemoryMap: function StackRenderer_renderMemoryMap(aDiv, memoryMap) {
 | |
|     aDiv.appendChild(document.createTextNode(this.memoryMapTitle));
 | |
|     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) {
 | |
|     aDiv.appendChild(document.createTextNode(this.stackTitle));
 | |
|     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 == 0) {
 | |
|       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 aFormatArgs formating args to be passed to formatStringFromName.
 | |
|    */
 | |
|   renderHeader: function StackRenderer_renderHeader(aPrefix, aFormatArgs) {
 | |
|     let div = document.getElementById(aPrefix);
 | |
| 
 | |
|     let titleElement = document.createElement("span");
 | |
|     titleElement.className = "stack-title";
 | |
| 
 | |
|     let titleText = bundle.formatStringFromName(
 | |
|       aPrefix + "-title", aFormatArgs, aFormatArgs.length);
 | |
|     titleElement.appendChild(document.createTextNode(titleText));
 | |
| 
 | |
|     div.appendChild(titleElement);
 | |
|     div.appendChild(document.createElement("br"));
 | |
|   },
 | |
| };
 | |
| 
 | |
| var RawPayloadData = {
 | |
|   /**
 | |
|    * Renders the raw pyaload.
 | |
|    */
 | |
|   render(aPing) {
 | |
|     setHasData("raw-payload-section", true);
 | |
|     let pre = document.getElementById("raw-payload-data");
 | |
|     pre.textContent = JSON.stringify(aPing.payload, null, 2);
 | |
|   },
 | |
| 
 | |
|   attachObservers() {
 | |
|     document.getElementById("payload-json-viewer").addEventListener("click", (e) => {
 | |
|       openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2));
 | |
|     });
 | |
|   },
 | |
| };
 | |
| 
 | |
| function SymbolicationRequest(aPrefix, aRenderHeader,
 | |
|                               aMemoryMap, aStacks, aDurations = null) {
 | |
|   this.prefix = aPrefix;
 | |
|   this.renderHeader = aRenderHeader;
 | |
|   this.memoryMap = aMemoryMap;
 | |
|   this.stacks = aStacks;
 | |
|   this.durations = aDurations;
 | |
| }
 | |
| /**
 | |
|  * A callback for onreadystatechange. It replaces the numeric stack with
 | |
|  * the symbolicated one returned by the symbolication server.
 | |
|  */
 | |
| SymbolicationRequest.prototype.handleSymbolResponse =
 | |
| 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 = bundle.GetStringFromName("errorFetchingSymbols");
 | |
| 
 | |
|   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 CapturedStacks = {
 | |
|   symbolRequest: null,
 | |
| 
 | |
|   render: function CapturedStacks_render(payload) {
 | |
|     // Retrieve captured stacks from telemetry payload.
 | |
|     let capturedStacks = "processes" in payload && "parent" in payload.processes
 | |
|       ? payload.processes.parent.capturedStacks
 | |
|       : false;
 | |
|     let hasData = capturedStacks && capturedStacks.stacks &&
 | |
|                   capturedStacks.stacks.length > 0;
 | |
|     setHasData("captured-stacks-section", hasData);
 | |
|     if (!hasData) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let stacks = capturedStacks.stacks;
 | |
|     let memoryMap = capturedStacks.memoryMap;
 | |
|     let captures = capturedStacks.captures;
 | |
| 
 | |
|     StackRenderer.renderStacks("captured-stacks", stacks, memoryMap,
 | |
|                               (index) => this.renderCaptureHeader(index, captures));
 | |
|   },
 | |
| 
 | |
|   renderCaptureHeader: function CaptureStacks_renderCaptureHeader(index, captures) {
 | |
|     let key = captures[index][0];
 | |
|     let cardinality = captures[index][2];
 | |
|     StackRenderer.renderHeader("captured-stacks", [key, cardinality]);
 | |
|   },
 | |
| };
 | |
| 
 | |
| var Histogram = {
 | |
| 
 | |
|   hgramSamplesCaption: bundle.GetStringFromName("histogramSamples"),
 | |
| 
 | |
|   hgramAverageCaption: bundle.GetStringFromName("histogramAverage"),
 | |
| 
 | |
|   hgramSumCaption: bundle.GetStringFromName("histogramSum"),
 | |
| 
 | |
|   hgramCopyCaption: bundle.GetStringFromName("histogramCopy"),
 | |
| 
 | |
|   /**
 | |
|    * 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 stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " +
 | |
|                 this.hgramAverageCaption + " = " + hgram.pretty_average + ", " +
 | |
|                 this.hgramSumCaption + " = " + hgram.sum;
 | |
| 
 | |
|     let divStats = document.createElement("div");
 | |
|     divStats.classList.add("histogram-stats");
 | |
|     divStats.appendChild(document.createTextNode(stats));
 | |
|     outerDiv.appendChild(divStats);
 | |
| 
 | |
|     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";
 | |
|     copyButton.appendChild(document.createTextNode(this.hgramCopyCaption));
 | |
|     copyButton.histogramText = aName + EOL + stats + EOL + EOL + textData;
 | |
|     copyButton.addEventListener("click", function() {
 | |
|       Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper)
 | |
|                                                  .copyString(this.histogramText);
 | |
|     });
 | |
|     outerDiv.appendChild(copyButton);
 | |
| 
 | |
|     aParent.appendChild(outerDiv);
 | |
|     return outerDiv;
 | |
|   },
 | |
| 
 | |
|   processHistogram(aHgram, aName) {
 | |
|     const values = Object.keys(aHgram.values).map(k => aHgram.values[k]);
 | |
|     if (!values.length) {
 | |
|       // If we have no values collected for this histogram, just return
 | |
|       // zero values so we still render it.
 | |
|       return {
 | |
|         values: [],
 | |
|         pretty_average: 0,
 | |
|         max: 0,
 | |
|         sample_count: 0,
 | |
|         sum: 0,
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     const sample_count = values.reduceRight((a, b) => a + b);
 | |
|     const average = Math.round(aHgram.sum * 10 / sample_count) / 10;
 | |
|     const max_value = Math.max(...values);
 | |
| 
 | |
|     const labelledValues = Object.keys(aHgram.values)
 | |
|                            .map(k => [Number(k), aHgram.values[k]]);
 | |
| 
 | |
|     let result = {
 | |
|       values: labelledValues,
 | |
|       pretty_average: average,
 | |
|       max: max_value,
 | |
|       sample_count,
 | |
|       sum: aHgram.sum,
 | |
|     };
 | |
| 
 | |
|     return result;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Return a non-negative, logarithmic representation of a non-negative number.
 | |
|    * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
 | |
|    *
 | |
|    * @param aNumber Non-negative number
 | |
|    */
 | |
|   getLogValue(aNumber) {
 | |
|     return Math.max(0, Math.log10(aNumber) + 1);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create histogram HTML bars, also returns a textual representation
 | |
|    * Both aMaxValue and aSumValues must be positive.
 | |
|    * Values are assumed to use 0 as baseline.
 | |
|    *
 | |
|    * @param aDiv Outer parent div
 | |
|    * @param aHgram The histogram data
 | |
|    * @param aOptions Object with render options (@see #render)
 | |
|    */
 | |
|   renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
 | |
|     let text = "";
 | |
|     // If the last label is not the longest string, alignment will break a little
 | |
|     let labelPadTo = 0;
 | |
|     if (aHgram.values.length) {
 | |
|       labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
 | |
|     }
 | |
|     let maxBarValue = aOptions.exponential ? this.getLogValue(aHgram.max) : aHgram.max;
 | |
| 
 | |
|     for (let [label, value] of aHgram.values) {
 | |
|       label = String(label);
 | |
|       let barValue = aOptions.exponential ? this.getLogValue(value) : value;
 | |
| 
 | |
|       // Create a text representation: <right-aligned-label> |<bar-of-#><value>  <percentage>
 | |
|       text += EOL
 | |
|               + " ".repeat(Math.max(0, labelPadTo - label.length)) + label // Right-aligned label
 | |
|               + " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar
 | |
|               + "  " + value // Value
 | |
|               + "  " + Math.round(100 * value / aHgram.sample_count) + "%"; // Percentage
 | |
| 
 | |
|       // Construct the HTML labels + bars
 | |
|       let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
 | |
|       let aboveEm = MAX_BAR_HEIGHT - belowEm;
 | |
| 
 | |
|       let barDiv = document.createElement("div");
 | |
|       barDiv.className = "bar";
 | |
|       barDiv.style.paddingTop = aboveEm + "em";
 | |
| 
 | |
|       // Add value label or an nbsp if no value
 | |
|       barDiv.appendChild(document.createTextNode(value ? value : "\u00A0"));
 | |
| 
 | |
|       // Create the blue bar
 | |
|       let bar = document.createElement("div");
 | |
|       bar.className = "bar-inner";
 | |
|       bar.style.height = belowEm + "em";
 | |
|       barDiv.appendChild(bar);
 | |
| 
 | |
|       // Add a special class to move the text down to prevent text overlap
 | |
|       if (label.length > 3) {
 | |
|           bar.classList.add("long-label");
 | |
|       }
 | |
|       // Add bucket label
 | |
|       barDiv.appendChild(document.createTextNode(label));
 | |
| 
 | |
|       aDiv.appendChild(barDiv);
 | |
|     }
 | |
| 
 | |
|     return text.substr(EOL.length); // Trim the EOL before the first line
 | |
|   },
 | |
| };
 | |
| 
 | |
| 
 | |
| var Search = {
 | |
| 
 | |
|   HASH_SEARCH: "search=",
 | |
| 
 | |
|   // A list of ids of sections that do not support search.
 | |
|   blacklist: [
 | |
|     "late-writes-section",
 | |
|     "raw-payload-section",
 | |
|   ],
 | |
| 
 | |
|   // Pass if: all non-empty array items match (case-sensitive)
 | |
|   isPassText(subject, filter) {
 | |
|     for (let item of filter) {
 | |
|       if (item.length && !subject.includes(item)) {
 | |
|         return false; // mismatch and not a spurious space
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   isPassRegex(subject, filter) {
 | |
|     return filter.test(subject);
 | |
|   },
 | |
| 
 | |
|   chooseFilter(filterText) {
 | |
|     let filter = filterText.toString();
 | |
|     // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
 | |
|     let isPassFunc; // filter function, set once, then applied to all elements
 | |
|     filter = filter.trim();
 | |
|     if (filter[0] != "/") { // Plain text: case insensitive, AND if multi-string
 | |
|       isPassFunc = this.isPassText;
 | |
|       filter = filter.toLowerCase().split(" ");
 | |
|     } else {
 | |
|       isPassFunc = this.isPassRegex;
 | |
|       var r = filter.match(/^\/(.*)\/(i?)$/);
 | |
|       try {
 | |
|         filter = RegExp(r[1], r[2]);
 | |
|       } catch (e) { // Incomplete or bad RegExp - always no match
 | |
|         isPassFunc = function() {
 | |
|           return false;
 | |
|         };
 | |
|       }
 | |
|     }
 | |
|     return [isPassFunc, filter];
 | |
|   },
 | |
| 
 | |
|   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;
 | |
|     if (section.id === "home-section") {
 | |
|       return this.homeSearch(text);
 | |
|     } else 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.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 searchStatus;
 | |
|       let section = document.querySelector(".category.selected > span");
 | |
|       if (section.parentElement.id === "category-home") {
 | |
|         searchStatus = bundle.formatStringFromName("noSearchResultsAll", [text], 1);
 | |
|       } else {
 | |
|         let sectionName = section.textContent.trim();
 | |
|         searchStatus = (text === "")
 | |
|           ? bundle.formatStringFromName("noDataToDisplay", [sectionName], 1)
 | |
|           : bundle.formatStringFromName("noSearchResults", [sectionName, text], 2);
 | |
|       }
 | |
|       document.getElementById("no-search-results-text").textContent = searchStatus;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   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");
 | |
|     let title = bundle.formatStringFromName("resultsForSearch", [text], 1);
 | |
|     adjustHeaderState(title);
 | |
|     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;
 | |
|       }
 | |
|     });
 | |
|     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 == 0) {
 | |
|       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 == 0) {
 | |
|     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 = {
 | |
| 
 | |
|   defaultHeadings: [
 | |
|     bundle.GetStringFromName("keysHeader"),
 | |
|     bundle.GetStringFromName("valuesHeader"),
 | |
|   ],
 | |
| 
 | |
|   /**
 | |
|    * 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 suffix = (i == (headings.length - 1)) ? "\n" : "\t";
 | |
|       let column = document.createElement("th");
 | |
|       column.appendChild(document.createTextNode(headings[i] + suffix));
 | |
|       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 = {
 | |
|   tableIDTitle: bundle.GetStringFromName("addonTableID"),
 | |
|   tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"),
 | |
| 
 | |
|   /**
 | |
|    * 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: function AddonDetails_render(aPing) {
 | |
|     let addonSection = document.getElementById("addon-details");
 | |
|     removeAllChildNodes(addonSection);
 | |
|     let addonDetails = aPing.payload.addonDetails;
 | |
|     const hasData = addonDetails && Object.keys(addonDetails).length > 0;
 | |
|     setHasData("addon-details-section", hasData);
 | |
|     if (!hasData) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     for (let provider in addonDetails) {
 | |
|       let providerSection = document.createElement("caption");
 | |
|       let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
 | |
|       providerSection.appendChild(document.createTextNode(titleText));
 | |
| 
 | |
|       let headingStrings = [this.tableIDTitle, this.tableDetailsTitle ];
 | |
|       let table = GenericTable.render(explodeObject(addonDetails[provider]),
 | |
|                                       headingStrings);
 | |
|       table.appendChild(providerSection);
 | |
|       addonSection.appendChild(table);
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| var Scalars = {
 | |
|   /**
 | |
|    * 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.
 | |
|    */
 | |
|   render(aPayload) {
 | |
|     let scalarsSection = document.getElementById("scalars");
 | |
|     removeAllChildNodes(scalarsSection);
 | |
| 
 | |
|     let processesSelect = document.getElementById("processes");
 | |
|     let selectedProcess = processesSelect.selectedOptions.item(0).getAttribute("value");
 | |
| 
 | |
|     if (!selectedProcess) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let payload = aPayload.stores;
 | |
|     if (payload) { // Check for stores in the current ping data first
 | |
|       let hasData = false;
 | |
|       for (const store of Object.keys(payload)) {
 | |
|         if (!(selectedProcess in payload[store])) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         let scalars = payload[store][selectedProcess].scalars || {};
 | |
|         hasData = hasData || Array.from(processesSelect.options).some((option) => {
 | |
|           let value = option.getAttribute("value");
 | |
|           let sclrs = payload[store][value] && payload[store][value].scalars;
 | |
|           return sclrs && Object.keys(sclrs).length > 0;
 | |
|         });
 | |
|         if (Object.keys(scalars).length > 0) {
 | |
|           const headings = [
 | |
|             "namesHeader",
 | |
|             "valuesHeader",
 | |
|           ].map(h => bundle.GetStringFromName(h));
 | |
| 
 | |
|           let s = GenericSubsection.renderSubsectionHeader(store, true, "scalars-section");
 | |
|           let table = GenericTable.render(explodeObject(scalars), headings);
 | |
|           let caption = document.createElement("caption");
 | |
|           caption.textContent = store;
 | |
|           table.appendChild(caption);
 | |
|           s.appendChild(table);
 | |
|           scalarsSection.appendChild(s);
 | |
|         }
 | |
|       }
 | |
|       setHasData("scalars-section", hasData);
 | |
|     } else { // Handle archived pings
 | |
|       if (!aPayload.processes ||
 | |
|         !(selectedProcess in aPayload.processes)) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let scalars = aPayload.processes[selectedProcess].scalars || {};
 | |
|       let hasData = Array.from(processesSelect.options).some((option) => {
 | |
|         let value = option.getAttribute("value");
 | |
|         let sclrs = aPayload.processes[value].scalars;
 | |
|         return sclrs && Object.keys(sclrs).length > 0;
 | |
|       });
 | |
| 
 | |
|       setHasData("scalars-section", hasData);
 | |
|       if (Object.keys(scalars).length > 0) {
 | |
|         const headings = [
 | |
|           "namesHeader",
 | |
|           "valuesHeader",
 | |
|         ].map(h => bundle.GetStringFromName(h));
 | |
|         const table = GenericTable.render(explodeObject(scalars), headings);
 | |
|         scalarsSection.appendChild(table);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| var KeyedScalars = {
 | |
|   /**
 | |
|    * 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.
 | |
|    */
 | |
|   render(aPayload) {
 | |
|     let scalarsSection = document.getElementById("keyed-scalars");
 | |
|     removeAllChildNodes(scalarsSection);
 | |
| 
 | |
|     let processesSelect = document.getElementById("processes");
 | |
|     let selectedProcess = processesSelect.selectedOptions.item(0).getAttribute("value");
 | |
| 
 | |
|     if (!selectedProcess) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let payload = aPayload.stores;
 | |
|     if (payload) { // Check for stores in the current ping data first
 | |
|       let hasData = false;
 | |
|       for (const store of Object.keys(payload)) {
 | |
|         if (!(selectedProcess in payload[store])) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         let keyedScalars = payload[store][selectedProcess].keyedScalars || {};
 | |
|         hasData = hasData || Array.from(processesSelect.options).some((option) => {
 | |
|           let value = option.getAttribute("value");
 | |
|           let keyedS = payload[store][value] && payload[store][value].keyedScalars;
 | |
|           return keyedS && Object.keys(keyedS).length > 0;
 | |
|         });
 | |
|         if (!Object.keys(keyedScalars).length > 0) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         let s = GenericSubsection.renderSubsectionHeader(store, true, "keyed-scalars-section");
 | |
|         let heading = document.createElement("h2");
 | |
|         heading.textContent = store;
 | |
|         s.appendChild(heading);
 | |
| 
 | |
|         const headings = [
 | |
|           "namesHeader",
 | |
|           "valuesHeader",
 | |
|         ].map(h => bundle.GetStringFromName(h));
 | |
|         for (let scalar in keyedScalars) {
 | |
|           // Add the name of the scalar.
 | |
|           let container = document.createElement("div");
 | |
|           container.classList.add("keyed-scalar");
 | |
|           container.id = scalar;
 | |
|           let scalarNameSection = document.createElement("p");
 | |
|           scalarNameSection.classList.add("keyed-title");
 | |
|           scalarNameSection.appendChild(document.createTextNode(scalar));
 | |
|           container.appendChild(scalarNameSection);
 | |
|           // Populate the section with the key-value pairs from the scalar.
 | |
|           const table = GenericTable.render(explodeObject(keyedScalars[scalar]), headings);
 | |
|           container.appendChild(table);
 | |
|           s.appendChild(container);
 | |
|         }
 | |
| 
 | |
|         scalarsSection.appendChild(s);
 | |
|       }
 | |
|       setHasData("keyed-scalars-section", hasData);
 | |
|     } else { // Handle archived pings
 | |
|       if (!aPayload.processes ||
 | |
|         !(selectedProcess in aPayload.processes)) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let keyedScalars = aPayload.processes[selectedProcess].keyedScalars || {};
 | |
|       let hasData = Array.from(processesSelect.options).some((option) => {
 | |
|         let value = option.getAttribute("value");
 | |
|         let keyedS = aPayload.processes[value].keyedScalars;
 | |
|         return keyedS && Object.keys(keyedS).length > 0;
 | |
|       });
 | |
| 
 | |
|       setHasData("keyed-scalars-section", hasData);
 | |
|       if (!Object.keys(keyedScalars).length > 0) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       const headings = [
 | |
|         "namesHeader",
 | |
|         "valuesHeader",
 | |
|       ].map(h => bundle.GetStringFromName(h));
 | |
|       for (let scalar in keyedScalars) {
 | |
|         // Add the name of the scalar.
 | |
|         let container = document.createElement("div");
 | |
|         container.classList.add("keyed-scalar");
 | |
|         container.id = scalar;
 | |
|         let scalarNameSection = document.createElement("p");
 | |
|         scalarNameSection.classList.add("keyed-title");
 | |
|         scalarNameSection.appendChild(document.createTextNode(scalar));
 | |
|         container.appendChild(scalarNameSection);
 | |
|         // Populate the section with the key-value pairs from the scalar.
 | |
|         const table = GenericTable.render(explodeObject(keyedScalars[scalar]), headings);
 | |
|         container.appendChild(table);
 | |
|         scalarsSection.appendChild(container);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| 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 eventsSection = document.getElementById("events");
 | |
|     removeAllChildNodes(eventsSection);
 | |
| 
 | |
|     let processesSelect = document.getElementById("processes");
 | |
|     let selectedProcess = processesSelect.selectedOptions.item(0).getAttribute("value");
 | |
| 
 | |
|     if (!aPayload.processes ||
 | |
|         !selectedProcess ||
 | |
|         !(selectedProcess in aPayload.processes)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let events = aPayload.processes[selectedProcess].events || {};
 | |
|     let hasData = Array.from(processesSelect.options).some((option) => {
 | |
|       let value = option.getAttribute("value");
 | |
|       let evts = aPayload.processes[value].events;
 | |
|       return evts && Object.keys(evts).length > 0;
 | |
|     });
 | |
|     setHasData("events-section", hasData);
 | |
|     if (Object.keys(events).length > 0) {
 | |
|       const headings = [
 | |
|         "timestampHeader",
 | |
|         "categoryHeader",
 | |
|         "methodHeader",
 | |
|         "objectHeader",
 | |
|         "valuesHeader",
 | |
|         "extraHeader",
 | |
|       ].map(h => bundle.GetStringFromName(h));
 | |
| 
 | |
|       const table = GenericTable.render(events, headings);
 | |
|       eventsSection.appendChild(table);
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * 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 the text of the page header based on a config pref + bundle strings
 | |
|  */
 | |
| function setupPageHeader() {
 | |
|   let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
 | |
|   let brandName = brandBundle.GetStringFromName("brandFullName");
 | |
|   let subtitleText = bundle.formatStringFromName(
 | |
|     "pageSubtitle", [serverOwner, brandName], 2);
 | |
| 
 | |
|   let subtitleElement = document.getElementById("page-subtitle");
 | |
|   subtitleElement.appendChild(document.createTextNode(subtitleText));
 | |
| 
 | |
|   let links = [
 | |
|     "https://docs.telemetry.mozilla.org/",
 | |
|     "https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/index.html",
 | |
|     "https://telemetry.mozilla.org/",
 | |
|     "https://telemetry.mozilla.org/probe-dictionary/",
 | |
|   ];
 | |
|   let htmlLink = document.querySelectorAll("#home-section > ul > li > a");
 | |
|   htmlLink.forEach((a, index) => {
 | |
|     a.href = links[index];
 | |
|   });
 | |
| }
 | |
| 
 | |
| function displayProcessesSelector(selectedSection) {
 | |
|   let whitelist = [
 | |
|     "scalars-section",
 | |
|     "keyed-scalars-section",
 | |
|     "histograms-section",
 | |
|     "keyed-histograms-section",
 | |
|     "events-section",
 | |
|   ];
 | |
|   let processes = document.getElementById("processes");
 | |
|   processes.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();
 | |
|   document.getElementById("sectionTitle").textContent = title ? title : selectedTitle;
 | |
| 
 | |
|   let placeholder;
 | |
|   if (selected.parentElement.id === "category-home") {
 | |
|     placeholder = bundle.GetStringFromName("filterAllPlaceholder");
 | |
|   } else {
 | |
|     placeholder = bundle.formatStringFromName("filterPlaceholder", [ selectedTitle ], 1);
 | |
|   }
 | |
|   let search = document.getElementById("search");
 | |
|   search.setAttribute("placeholder", placeholder);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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();
 | |
|   displayProcessesSelector(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);
 | |
| 
 | |
|   // Clean up observers when page is closed
 | |
|   window.addEventListener("unload",
 | |
|     function(aEvent) {
 | |
|       Settings.detachObservers();
 | |
|   }, {once: true});
 | |
| 
 | |
|   document.getElementById("captured-stacks-fetch-symbols").addEventListener("click",
 | |
|     function() {
 | |
|       if (!gPingData) {
 | |
|         return;
 | |
|       }
 | |
|       let capturedStacks = gPingData.payload.processes.parent.capturedStacks;
 | |
|       let req = new SymbolicationRequest("captured-stacks",
 | |
|                                          CapturedStacks.renderCaptureHeader,
 | |
|                                          capturedStacks.memoryMap,
 | |
|                                          capturedStacks.stacks,
 | |
|                                          capturedStacks.captures);
 | |
|       req.fetchSymbols();
 | |
|   });
 | |
| 
 | |
|   document.getElementById("captured-stacks-hide-symbols").addEventListener("click",
 | |
|     function() {
 | |
|       if (gPingData) {
 | |
|         CapturedStacks.render(gPingData.payload);
 | |
|       }
 | |
|   });
 | |
| 
 | |
|   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);
 | |
|   Telemetry.scalarAdd("telemetry.about_telemetry_pageload", 1);
 | |
| 
 | |
|   // Set the text in the page header
 | |
|   setupPageHeader();
 | |
| 
 | |
|   // 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", [aIndex + 1]);
 | |
|   },
 | |
| 
 | |
|   renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
 | |
|     setHasData("late-writes-section", !!lateWrites);
 | |
|     if (!lateWrites) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let stacks = lateWrites.stacks;
 | |
|     let memoryMap = lateWrites.memoryMap;
 | |
|     StackRenderer.renderStacks("late-writes", stacks, memoryMap,
 | |
|                                LateWritesSingleton.renderHeader);
 | |
|   },
 | |
| };
 | |
| 
 | |
| var HistogramSection = {
 | |
|   render(aPayload) {
 | |
|     let hgramDiv = document.getElementById("histograms");
 | |
|     removeAllChildNodes(hgramDiv);
 | |
| 
 | |
|     let histograms = {};
 | |
|     let hgramsSelect = document.getElementById("processes");
 | |
|     let hgramsOption = hgramsSelect.selectedOptions.item(0);
 | |
|     let hgramsProcess = hgramsOption.getAttribute("value");
 | |
| 
 | |
|     let payload = aPayload.stores;
 | |
|     if (payload) { // Check for stores in the current ping data first
 | |
|       let hasData = false;
 | |
|       for (const store of Object.keys(payload)) {
 | |
|         if (store in payload && hgramsProcess in payload[store] &&
 | |
|           "histograms" in payload[store][hgramsProcess]) {
 | |
|           histograms = payload[store][hgramsProcess].histograms;
 | |
|         }
 | |
| 
 | |
|         hasData = hasData || Array.from(hgramsSelect.options).some((option) => {
 | |
|           let value = option.getAttribute("value");
 | |
|           let histos = payload[store][value].histograms;
 | |
|           return histos && Object.keys(histos).length > 0;
 | |
|         });
 | |
| 
 | |
|         if (Object.keys(histograms).length > 0) {
 | |
|           let s = GenericSubsection.renderSubsectionHeader(store, true, "histograms-section");
 | |
|           let heading = document.createElement("h2");
 | |
|           heading.textContent = store;
 | |
|           s.appendChild(heading);
 | |
|           for (let [name, hgram] of Object.entries(histograms)) {
 | |
|             Histogram.render(s, name, hgram, {unpacked: true});
 | |
|           }
 | |
|           hgramDiv.appendChild(s);
 | |
|           let separator = document.createElement("div");
 | |
|           separator.classList.add("clearfix");
 | |
|           hgramDiv.appendChild(separator);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       setHasData("histograms-section", hasData);
 | |
|     } else { // Handle archived pings
 | |
|       if (hgramsProcess === "parent") {
 | |
|         histograms = aPayload.histograms;
 | |
|       } else if ("processes" in aPayload && hgramsProcess in aPayload.processes &&
 | |
|         "histograms" in aPayload.processes[hgramsProcess]) {
 | |
|         histograms = aPayload.processes[hgramsProcess].histograms;
 | |
|       }
 | |
| 
 | |
|       let hasData = Array.from(hgramsSelect.options).some((option) => {
 | |
|         let value = option.getAttribute("value");
 | |
|         if (value == "parent") {
 | |
|           return Object.keys(aPayload.histograms).length > 0;
 | |
|         }
 | |
|         let histos = aPayload.processes[value].histograms;
 | |
|         return histos && Object.keys(histos).length > 0;
 | |
|       });
 | |
|       setHasData("histograms-section", hasData);
 | |
| 
 | |
|       if (Object.keys(histograms).length > 0) {
 | |
|         for (let [name, hgram] of Object.entries(histograms)) {
 | |
|           Histogram.render(hgramDiv, name, hgram, {unpacked: true});
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| var KeyedHistogramSection = {
 | |
|   render(aPayload) {
 | |
|     let keyedDiv = document.getElementById("keyed-histograms");
 | |
|     removeAllChildNodes(keyedDiv);
 | |
| 
 | |
|     let keyedHistograms = {};
 | |
|     let keyedHgramsSelect = document.getElementById("processes");
 | |
|     let keyedHgramsOption = keyedHgramsSelect.selectedOptions.item(0);
 | |
|     let keyedHgramsProcess = keyedHgramsOption.getAttribute("value");
 | |
| 
 | |
|     let payload = aPayload.stores;
 | |
|     if (payload) { // Check for stores in the current ping data first
 | |
|       let hasData = false;
 | |
|       for (const store of Object.keys(payload)) {
 | |
|         if (store in payload && keyedHgramsProcess in payload[store] &&
 | |
|           "keyedHistograms" in payload[store][keyedHgramsProcess]) {
 | |
|           keyedHistograms = payload[store][keyedHgramsProcess].keyedHistograms;
 | |
|         }
 | |
| 
 | |
|         hasData = hasData || Array.from(keyedHgramsSelect.options).some((option) => {
 | |
|           let value = option.getAttribute("value");
 | |
|           let keyedHistos = payload[store][value].keyedHistograms;
 | |
|           return keyedHistos && Object.keys(keyedHistos).length > 0;
 | |
|         });
 | |
| 
 | |
|         if (Object.keys(keyedHistograms).length > 0) {
 | |
|           let s = GenericSubsection.renderSubsectionHeader(store, true, "keyed-histograms-section");
 | |
|           let heading = document.createElement("h2");
 | |
|           heading.textContent = store;
 | |
|           s.appendChild(heading);
 | |
|           for (let [id, keyed] of Object.entries(keyedHistograms)) {
 | |
|             KeyedHistogram.render(s, id, keyed, {unpacked: true});
 | |
|           }
 | |
|           keyedDiv.appendChild(s);
 | |
|           let separator = document.createElement("div");
 | |
|           separator.classList.add("clearfix");
 | |
|           keyedDiv.appendChild(separator);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       setHasData("keyed-histograms-section", hasData);
 | |
|     } else { // Handle archived pings
 | |
|       if (keyedHgramsProcess === "parent") {
 | |
|         keyedHistograms = aPayload.keyedHistograms;
 | |
|       } else if ("processes" in aPayload && keyedHgramsProcess in aPayload.processes &&
 | |
|         "keyedHistograms" in aPayload.processes[keyedHgramsProcess]) {
 | |
|         keyedHistograms = aPayload.processes[keyedHgramsProcess].keyedHistograms;
 | |
|       }
 | |
| 
 | |
|       let hasData = Array.from(keyedHgramsSelect.options).some((option) => {
 | |
|         let value = option.getAttribute("value");
 | |
|         if (value == "parent") {
 | |
|           return Object.keys(aPayload.keyedHistograms).length > 0;
 | |
|         }
 | |
|         let keyedHistos = aPayload.processes[value].keyedHistograms;
 | |
|         return keyedHistos && Object.keys(keyedHistos).length > 0;
 | |
|       });
 | |
|       setHasData("keyed-histograms-section", hasData);
 | |
|       if (Object.keys(keyedHistograms).length > 0) {
 | |
|         for (let [id, keyed] of Object.entries(keyedHistograms)) {
 | |
|           if (Object.keys(keyed).length > 0) {
 | |
|             KeyedHistogram.render(keyedDiv, id, keyed, {unpacked: true});
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| var SessionInformation = {
 | |
|   render(aPayload) {
 | |
|     let infoSection = document.getElementById("session-info");
 | |
|     removeAllChildNodes(infoSection);
 | |
| 
 | |
|     let hasData = Object.keys(aPayload.info).length > 0;
 | |
|     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 > 0;
 | |
|     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;
 | |
|   },
 | |
| };
 | |
| 
 | |
| function renderProcessList(payload, selectEl) {
 | |
|   removeAllChildNodes(selectEl);
 | |
|   let option = document.createElement("option");
 | |
|   option.appendChild(document.createTextNode("parent"));
 | |
|   option.setAttribute("value", "parent");
 | |
|   option.selected = true;
 | |
|   selectEl.appendChild(option);
 | |
| 
 | |
|   if (!("processes" in payload)) {
 | |
|     selectEl.disabled = true;
 | |
|     return;
 | |
|   }
 | |
|   selectEl.disabled = false;
 | |
| 
 | |
|   for (let process of Object.keys(payload.processes)) {
 | |
|     // TODO: parent hgrams are on root payload, not in payload.processes.parent
 | |
|     // When/If that gets moved, you'll need to remove this
 | |
|     if (process === "parent") {
 | |
|       continue;
 | |
|     }
 | |
|     option = document.createElement("option");
 | |
|     option.appendChild(document.createTextNode(process));
 | |
|     option.setAttribute("value", process);
 | |
|     selectEl.appendChild(option);
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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 process lists
 | |
|   if (updatePayloadList) {
 | |
|     renderProcessList(ping.payload, document.getElementById("processes"));
 | |
|   }
 | |
| 
 | |
|   // 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 process list if necessary.
 | |
|     if (updatePayloadList) {
 | |
|       renderProcessList(payload, document.getElementById("processes"));
 | |
|     }
 | |
| 
 | |
|     // 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);
 | |
| 
 | |
|   // Show captured stacks.
 | |
|   CapturedStacks.render(payload);
 | |
| 
 | |
|   LateWritesSingleton.renderLateWrites(payload.lateWrites);
 | |
| 
 | |
|   // Show simple measurements
 | |
|   SimpleMeasurements.render(payload);
 | |
| }
 | |
| 
 | |
| window.addEventListener("load", onLoad);
 |