forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1984 lines
		
	
	
	
		
			64 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1984 lines
		
	
	
	
		
			64 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';
 | |
| 
 | |
| var Ci = Components.interfaces;
 | |
| var Cc = Components.classes;
 | |
| var Cu = Components.utils;
 | |
| 
 | |
| Cu.import("resource://gre/modules/Services.jsm");
 | |
| Cu.import("resource://gre/modules/TelemetryTimestamps.jsm");
 | |
| Cu.import("resource://gre/modules/TelemetryController.jsm");
 | |
| Cu.import("resource://gre/modules/TelemetrySession.jsm");
 | |
| Cu.import("resource://gre/modules/TelemetryArchive.jsm");
 | |
| Cu.import("resource://gre/modules/TelemetryUtils.jsm");
 | |
| Cu.import("resource://gre/modules/TelemetryLog.jsm");
 | |
| Cu.import("resource://gre/modules/Preferences.jsm");
 | |
| Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 | |
| Cu.import("resource://gre/modules/Task.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
 | |
|                                   "resource://gre/modules/AppConstants.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 = 18;
 | |
| 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 = "http://symbolapi.mozilla.org";
 | |
| 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 isArray(arg) {
 | |
|   return Object.prototype.toString.call(arg) === '[object Array]';
 | |
| }
 | |
| 
 | |
| function isFlatArray(obj) {
 | |
|   if (!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, 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.indexOf(k) == -1) {
 | |
|       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.QueryInterface(Ci.nsIInterfaceRequestor)
 | |
|                .getInterface(Ci.nsIWebNavigation)
 | |
|                .QueryInterface(Ci.nsIDocShellTreeItem)
 | |
|                .rootTreeItem
 | |
|                .QueryInterface(Ci.nsIInterfaceRequestor)
 | |
|                .getInterface(Ci.nsIDOMWindow);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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 && "openAdvancedPreferences" in mainWindow) {
 | |
|     return mainWindow;
 | |
|   }
 | |
|   return null;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Remove all child nodes of a document node.
 | |
|  */
 | |
| function removeAllChildNodes(node) {
 | |
|   while (node.hasChildNodes()) {
 | |
|     node.removeChild(node.lastChild);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Pad a number to two digits with leading "0".
 | |
|  */
 | |
| function padToTwoDigits(n) {
 | |
|   return (n > 9) ? n: "0" + n;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Return yesterdays date with the same time.
 | |
|  */
 | |
| function yesterday(date) {
 | |
|   let d = new Date(date);
 | |
|   d.setDate(d.getDate() - 1);
 | |
|   return d;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This returns a short date string of the form YYYY/MM/DD.
 | |
|  */
 | |
| function shortDateString(date) {
 | |
|   return date.getFullYear()
 | |
|          + "/" + padToTwoDigits(date.getMonth() + 1)
 | |
|          + "/" + padToTwoDigits(date.getDate());
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This returns a short time string of the form hh:mm:ss.
 | |
|  */
 | |
| function shortTimeString(date) {
 | |
|   return padToTwoDigits(date.getHours())
 | |
|          + ":" + padToTwoDigits(date.getMinutes())
 | |
|          + ":" + padToTwoDigits(date.getSeconds());
 | |
| }
 | |
| 
 | |
| var Settings = {
 | |
|   SETTINGS: [
 | |
|     // data upload
 | |
|     {
 | |
|       pref: PREF_FHR_UPLOAD_ENABLED,
 | |
|       defaultPrefValue: false,
 | |
|       descriptionEnabledId: "description-upload-enabled",
 | |
|       descriptionDisabledId: "description-upload-disabled",
 | |
|     },
 | |
|     // extended "Telemetry" recording
 | |
|     {
 | |
|       pref: PREF_TELEMETRY_ENABLED,
 | |
|       defaultPrefValue: false,
 | |
|       descriptionEnabledId: "description-extended-recording-enabled",
 | |
|       descriptionDisabledId: "description-extended-recording-disabled",
 | |
|     },
 | |
|   ],
 | |
| 
 | |
|   attachObservers: function() {
 | |
|     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") {
 | |
|           Cu.import("resource://gre/modules/Messaging.jsm");
 | |
|           Messaging.sendRequest({
 | |
|             type: "Settings:Show",
 | |
|             resource: "preferences_privacy",
 | |
|           });
 | |
|         } else {
 | |
|           // Show the data choices preferences on desktop.
 | |
|           let mainWindow = getMainWindowWithPreferencesPane();
 | |
|           mainWindow.openAdvancedPreferences("dataChoicesTab");
 | |
|         }
 | |
|       }, false);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   detachObservers: function() {
 | |
|     for (let setting of this.SETTINGS) {
 | |
|       Preferences.ignore(setting.pref, this.render, this);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Updates the button & text at the top of the page to reflect Telemetry state.
 | |
|    */
 | |
|   render: function() {
 | |
|     for (let setting of this.SETTINGS) {
 | |
|       let enabledElement = document.getElementById(setting.descriptionEnabledId);
 | |
|       let disabledElement = document.getElementById(setting.descriptionDisabledId);
 | |
| 
 | |
|       if (Preferences.get(setting.pref, setting.defaultPrefValue)) {
 | |
|         enabledElement.classList.remove("hidden");
 | |
|         disabledElement.classList.add("hidden");
 | |
|       } else {
 | |
|         enabledElement.classList.add("hidden");
 | |
|         disabledElement.classList.remove("hidden");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| };
 | |
| 
 | |
| var PingPicker = {
 | |
|   viewCurrentPingData: null,
 | |
|   viewStructuredPingData: null,
 | |
|   _archivedPings: null,
 | |
| 
 | |
|   attachObservers: function() {
 | |
|     let elements = document.getElementsByName("choose-ping-source");
 | |
|     for (let el of elements) {
 | |
|       el.addEventListener("change", () => this.onPingSourceChanged(), false);
 | |
|     }
 | |
| 
 | |
|     let displays = document.getElementsByName("choose-ping-display");
 | |
|     for (let el of displays) {
 | |
|       el.addEventListener("change", () => this.onPingDisplayChanged(), false);
 | |
|     }
 | |
| 
 | |
|     document.getElementById("show-subsession-data").addEventListener("change", () => {
 | |
|       this._updateCurrentPingData();
 | |
|     });
 | |
| 
 | |
|     document.getElementById("choose-ping-week").addEventListener("change", () => {
 | |
|       this._renderPingList();
 | |
|       this._updateArchivedPingData();
 | |
|     }, false);
 | |
|     document.getElementById("choose-ping-id").addEventListener("change", () => {
 | |
|       this._updateArchivedPingData()
 | |
|     }, false);
 | |
| 
 | |
|     document.getElementById("newer-ping")
 | |
|             .addEventListener("click", () => this._movePingIndex(-1), false);
 | |
|     document.getElementById("older-ping")
 | |
|             .addEventListener("click", () => this._movePingIndex(1), false);
 | |
|     document.getElementById("choose-payload")
 | |
|             .addEventListener("change", () => displayPingData(gPingData), false);
 | |
|   },
 | |
| 
 | |
|   onPingSourceChanged: function() {
 | |
|     this.update();
 | |
|   },
 | |
| 
 | |
|   onPingDisplayChanged: function() {
 | |
|     this.update();
 | |
|   },
 | |
| 
 | |
|   update: Task.async(function*() {
 | |
|     let viewCurrent = document.getElementById("ping-source-current").checked;
 | |
|     let viewStructured = document.getElementById("ping-source-structured").checked;
 | |
|     let currentChanged = viewCurrent !== this.viewCurrentPingData;
 | |
|     let structuredChanged = viewStructured !== this.viewStructuredPingData;
 | |
|     this.viewCurrentPingData = viewCurrent;
 | |
|     this.viewStructuredPingData = viewStructured;
 | |
| 
 | |
|     // 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 = yield TelemetryArchive.promiseArchivedPingList();
 | |
|     let sourceArchived = document.getElementById("ping-source-archive");
 | |
|     sourceArchived.disabled = (archivedPingList.length == 0);
 | |
| 
 | |
|     if (currentChanged) {
 | |
|       if (this.viewCurrentPingData) {
 | |
|         document.getElementById("current-ping-picker").classList.remove("hidden");
 | |
|         document.getElementById("archived-ping-picker").classList.add("hidden");
 | |
|         this._updateCurrentPingData();
 | |
|       } else {
 | |
|         document.getElementById("current-ping-picker").classList.add("hidden");
 | |
|         yield this._updateArchivedPingList(archivedPingList);
 | |
|         document.getElementById("archived-ping-picker").classList.remove("hidden");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (structuredChanged) {
 | |
|       if (this.viewStructuredPingData) {
 | |
|         this._showStructuredPingData();
 | |
|       } else {
 | |
|         this._showRawPingData();
 | |
|       }
 | |
|     }
 | |
|   }),
 | |
| 
 | |
|   _updateCurrentPingData: function() {
 | |
|     const subsession = document.getElementById("show-subsession-data").checked;
 | |
|     const ping = TelemetryController.getCurrentPingData(subsession);
 | |
|     if (!ping) {
 | |
|       return;
 | |
|     }
 | |
|     displayPingData(ping, true);
 | |
|   },
 | |
| 
 | |
|   _updateArchivedPingData: function() {
 | |
|     let id = this._getSelectedPingId();
 | |
|     return TelemetryArchive.promiseArchivedPingById(id)
 | |
|                            .then((ping) => displayPingData(ping, true));
 | |
|   },
 | |
| 
 | |
|   _updateArchivedPingList: Task.async(function*(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;
 | |
| 
 | |
|     // Collect the start dates for all the weeks we have pings for.
 | |
|     let weekStart = (date) => {
 | |
|       let weekDay = (date.getDay() + 6) % 7;
 | |
|       let monday = new Date(date);
 | |
|       monday.setDate(date.getDate() - weekDay);
 | |
|       return TelemetryUtils.truncateToDays(monday);
 | |
|     };
 | |
| 
 | |
|     let weekStartDates = new Set();
 | |
|     for (let p of pingList) {
 | |
|       weekStartDates.add(weekStart(new Date(p.timestampCreated)).getTime());
 | |
|     }
 | |
| 
 | |
|     // Build a list of the week date ranges we have ping data for.
 | |
|     let plusOneWeek = (date) => {
 | |
|       let d = date;
 | |
|       d.setDate(d.getDate() + 7);
 | |
|       return d;
 | |
|     };
 | |
| 
 | |
|     this._weeks = Array.from(weekStartDates.values(), startTime => ({
 | |
|       startDate: new Date(startTime),
 | |
|       endDate: plusOneWeek(new Date(startTime)),
 | |
|     }));
 | |
| 
 | |
|     // Render the archive data.
 | |
|     this._renderWeeks();
 | |
|     this._renderPingList();
 | |
| 
 | |
|     // Update the displayed ping.
 | |
|     yield this._updateArchivedPingData();
 | |
|   }),
 | |
| 
 | |
|   _renderWeeks: function() {
 | |
|     let weekSelector = document.getElementById("choose-ping-week");
 | |
|     removeAllChildNodes(weekSelector);
 | |
| 
 | |
|     let index = 0;
 | |
|     for (let week of this._weeks) {
 | |
|       let text = shortDateString(week.startDate)
 | |
|                  + " - " + shortDateString(yesterday(week.endDate));
 | |
| 
 | |
|       let option = document.createElement("option");
 | |
|       let content = document.createTextNode(text);
 | |
|       option.appendChild(content);
 | |
|       weekSelector.appendChild(option);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _getSelectedWeek: function() {
 | |
|     let weekSelector = document.getElementById("choose-ping-week");
 | |
|     return this._weeks[weekSelector.selectedIndex];
 | |
|   },
 | |
| 
 | |
|   _renderPingList: function(id = null) {
 | |
|     let pingSelector = document.getElementById("choose-ping-id");
 | |
|     removeAllChildNodes(pingSelector);
 | |
| 
 | |
|     let weekRange = this._getSelectedWeek();
 | |
|     let pings = this._archivedPings.filter(
 | |
|       (p) => p.timestampCreated >= weekRange.startDate.getTime() &&
 | |
|              p.timestampCreated < weekRange.endDate.getTime());
 | |
| 
 | |
|     for (let p of pings) {
 | |
|       let date = new Date(p.timestampCreated);
 | |
|       let text = shortDateString(date)
 | |
|                  + " " + shortTimeString(date)
 | |
|                  + " - " + p.type;
 | |
| 
 | |
|       let option = document.createElement("option");
 | |
|       let content = document.createTextNode(text);
 | |
|       option.appendChild(content);
 | |
|       option.setAttribute("value", p.id);
 | |
|       if (id && p.id == id) {
 | |
|         option.selected = true;
 | |
|       }
 | |
|       pingSelector.appendChild(option);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _getSelectedPingId: function() {
 | |
|     let pingSelector = document.getElementById("choose-ping-id");
 | |
|     let selected = pingSelector.selectedOptions.item(0);
 | |
|     return selected.getAttribute("value");
 | |
|   },
 | |
| 
 | |
|   _movePingIndex: function(offset) {
 | |
|     const id = this._getSelectedPingId();
 | |
|     const index = this._archivedPings.findIndex((p) => p.id == id);
 | |
|     const newIndex = Math.min(Math.max(index + offset, 0), this._archivedPings.length - 1);
 | |
|     const ping = this._archivedPings[newIndex];
 | |
| 
 | |
|     const weekIndex = this._weeks.findIndex(
 | |
|       (week) => ping.timestampCreated >= week.startDate.getTime() &&
 | |
|                 ping.timestampCreated < week.endDate.getTime());
 | |
|     const options = document.getElementById("choose-ping-week").options;
 | |
|     options.item(weekIndex).selected = true;
 | |
| 
 | |
|     this._renderPingList(ping.id);
 | |
|     this._updateArchivedPingData();
 | |
|   },
 | |
| 
 | |
|   _showRawPingData: function() {
 | |
|     document.getElementById("raw-ping-data-section").classList.remove("hidden");
 | |
|     document.getElementById("structured-ping-data-section").classList.add("hidden");
 | |
|   },
 | |
| 
 | |
|   _showStructuredPingData: function() {
 | |
|     document.getElementById("raw-ping-data-section").classList.add("hidden");
 | |
|     document.getElementById("structured-ping-data-section").classList.remove("hidden");
 | |
|   },
 | |
| };
 | |
| 
 | |
| var GeneralData = {
 | |
|   /**
 | |
|    * Renders the general data
 | |
|    */
 | |
|   render: function(aPing) {
 | |
|     setHasData("general-data-section", true);
 | |
|     let table = document.createElement("table");
 | |
| 
 | |
|     let caption = document.createElement("caption");
 | |
|     let captionString = bundle.GetStringFromName("generalDataTitle");
 | |
|     caption.appendChild(document.createTextNode(captionString + "\n"));
 | |
|     table.appendChild(caption);
 | |
| 
 | |
|     let headings = document.createElement("tr");
 | |
|     this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingName") + "\t");
 | |
|     this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingValue") + "\t");
 | |
|     table.appendChild(headings);
 | |
| 
 | |
|     // The payload & environment parts are handled by other renderers.
 | |
|     let ignoreSections = ["payload", "environment"];
 | |
|     let data = explodeObject(filterObject(aPing, ignoreSections));
 | |
| 
 | |
|     for (let [path, value] of data) {
 | |
|         let row = document.createElement("tr");
 | |
|         this.appendColumn(row, "td", path + "\t");
 | |
|         this.appendColumn(row, "td", value + "\t");
 | |
|         table.appendChild(row);
 | |
|     }
 | |
| 
 | |
|     let dataDiv = document.getElementById("general-data");
 | |
|     removeAllChildNodes(dataDiv);
 | |
|     dataDiv.appendChild(table);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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: function(aRowElement, aColType, aColText) {
 | |
|     let colElement = document.createElement(aColType);
 | |
|     let colTextElement = document.createTextNode(aColText);
 | |
|     colElement.appendChild(colTextElement);
 | |
|     aRowElement.appendChild(colElement);
 | |
|   },
 | |
| };
 | |
| 
 | |
| var EnvironmentData = {
 | |
|   /**
 | |
|    * Renders the environment data
 | |
|    */
 | |
|   render: function(ping) {
 | |
|     let dataDiv = document.getElementById("environment-data");
 | |
|     removeAllChildNodes(dataDiv);
 | |
|     const hasData = !!ping.environment;
 | |
|     setHasData("environment-data-section", hasData);
 | |
|     if (!hasData) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let data = sectionalizeObject(ping.environment);
 | |
| 
 | |
|     for (let [section, sectionData] of data) {
 | |
|       if (section == "addons") {
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       let table = document.createElement("table");
 | |
|       this.appendHeading(table);
 | |
| 
 | |
|       for (let [path, value] of sectionData) {
 | |
|         let row = document.createElement("tr");
 | |
|         this.appendColumn(row, "td", path);
 | |
|         this.appendColumn(row, "td", value);
 | |
|         table.appendChild(row);
 | |
|       }
 | |
| 
 | |
|       let hasData = sectionData.size > 0;
 | |
|       this.createSubsection(section, hasData, table, dataDiv);
 | |
|     }
 | |
| 
 | |
|     // We use specialized rendering here to make the addon and plugin listings
 | |
|     // more readable.
 | |
|     this.createAddonSection(dataDiv, ping);
 | |
|   },
 | |
| 
 | |
|   createSubsection: function(title, hasSubdata, subSectionData, dataDiv) {
 | |
|     let dataSection = document.createElement("section");
 | |
|     dataSection.classList.add("data-subsection");
 | |
| 
 | |
|     if (hasSubdata) {
 | |
|       dataSection.classList.add("has-subdata");
 | |
|     }
 | |
| 
 | |
|     // Create section heading
 | |
|     let sectionName = document.createElement("h2");
 | |
|     sectionName.setAttribute("class", "section-name");
 | |
|     sectionName.appendChild(document.createTextNode(title));
 | |
|     sectionName.addEventListener("click", toggleSection, false);
 | |
| 
 | |
|     // Create caption for toggling the subsection visibility.
 | |
|     let toggleCaption = document.createElement("span");
 | |
|     toggleCaption.setAttribute("class", "toggle-caption");
 | |
|     let toggleText = bundle.GetStringFromName("environmentDataSubsectionToggle");
 | |
|     toggleCaption.appendChild(document.createTextNode(" " + toggleText));
 | |
|     toggleCaption.addEventListener("click", toggleSection, false);
 | |
| 
 | |
|     // Create caption for empty subsections.
 | |
|     let emptyCaption = document.createElement("span");
 | |
|     emptyCaption.setAttribute("class", "empty-caption");
 | |
|     let emptyText = bundle.GetStringFromName("environmentDataSubsectionEmpty");
 | |
|     emptyCaption.appendChild(document.createTextNode(" " + emptyText));
 | |
| 
 | |
|     // Create data container
 | |
|     let data = document.createElement("div");
 | |
|     data.setAttribute("class", "subsection-data subdata");
 | |
|     data.appendChild(subSectionData);
 | |
| 
 | |
|     // Append elements
 | |
|     dataSection.appendChild(sectionName);
 | |
|     dataSection.appendChild(toggleCaption);
 | |
|     dataSection.appendChild(emptyCaption);
 | |
|     dataSection.appendChild(data);
 | |
| 
 | |
|     dataDiv.appendChild(dataSection);
 | |
|   },
 | |
| 
 | |
|   renderPersona: function(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: function(addonObj, addonSection, sectionTitle) {
 | |
|     let data = explodeObject(addonObj);
 | |
|     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: function(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: function(addonObj, addonSection, sectionTitle) {
 | |
|     let data = explodeObject(addonObj);
 | |
|     let table = document.createElement("table");
 | |
|     table.setAttribute("class", sectionTitle);
 | |
|     this.appendAddonSubsectionTitle(sectionTitle, table);
 | |
|     this.appendHeading(table);
 | |
| 
 | |
|     for (let [key, value] of data) {
 | |
|       this.appendRow(table, key, value);
 | |
|     }
 | |
| 
 | |
|     addonSection.appendChild(table);
 | |
|   },
 | |
| 
 | |
|   appendAddonID: function(table, addonID) {
 | |
|     this.appendRow(table, "id", addonID);
 | |
|   },
 | |
| 
 | |
|   appendHeading: function(table) {
 | |
|     let headings = document.createElement("tr");
 | |
|     this.appendColumn(headings, "th", bundle.GetStringFromName("environmentDataHeadingName"));
 | |
|     this.appendColumn(headings, "th", bundle.GetStringFromName("environmentDataHeadingValue"));
 | |
|     table.appendChild(headings);
 | |
|   },
 | |
| 
 | |
|   appendHeadingName: function(table, name) {
 | |
|     let headings = document.createElement("tr");
 | |
|     this.appendColumn(headings, "th", name);
 | |
|     headings.cells[0].colSpan = 2;
 | |
|     table.appendChild(headings);
 | |
|   },
 | |
| 
 | |
|   appendAddonSubsectionTitle: function(section, table) {
 | |
|     let caption = document.createElement("caption");
 | |
|     caption.setAttribute("class", "addon-caption");
 | |
|     caption.appendChild(document.createTextNode(section));
 | |
|     table.appendChild(caption);
 | |
|   },
 | |
| 
 | |
|   createAddonSection: function(dataDiv, ping) {
 | |
|     let addonSection = document.createElement("div");
 | |
|     let addons = ping.environment.addons;
 | |
|     this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
 | |
|     this.renderActivePlugins(addons.activePlugins, addonSection, "activePlugins");
 | |
|     this.renderKeyValueObject(addons.theme, addonSection, "theme");
 | |
|     this.renderKeyValueObject(addons.activeExperiment, addonSection, "activeExperiment");
 | |
|     this.renderAddonsObject(addons.activeGMPlugins, addonSection, "activeGMPlugins");
 | |
|     this.renderPersona(addons, addonSection, "persona");
 | |
| 
 | |
|     let hasAddonData = Object.keys(ping.environment.addons).length > 0;
 | |
|     this.createSubsection("addons", hasAddonData, addonSection, dataDiv);
 | |
|   },
 | |
| 
 | |
|   appendRow: function(table, id, value) {
 | |
|     let row = document.createElement("tr");
 | |
|     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: function(aRowElement, aColType, aColText) {
 | |
|     let colElement = document.createElement(aColType);
 | |
|     let colTextElement = document.createTextNode(aColText);
 | |
|     colElement.appendChild(colTextElement);
 | |
|     aRowElement.appendChild(colElement);
 | |
|   },
 | |
| };
 | |
| 
 | |
| var TelLog = {
 | |
|   /**
 | |
|    * Renders the telemetry log
 | |
|    */
 | |
|   render: function(aPing) {
 | |
|     let entries = aPing.payload.log;
 | |
|     const hasData = entries && entries.length > 0;
 | |
|     setHasData("telemetry-log-section", hasData);
 | |
|     if (!hasData) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let table = document.createElement("table");
 | |
| 
 | |
|     let caption = document.createElement("caption");
 | |
|     let captionString = bundle.GetStringFromName("telemetryLogTitle");
 | |
|     caption.appendChild(document.createTextNode(captionString + "\n"));
 | |
|     table.appendChild(caption);
 | |
| 
 | |
|     let headings = document.createElement("tr");
 | |
|     this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingId") + "\t");
 | |
|     this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingTimestamp") + "\t");
 | |
|     this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingData") + "\t");
 | |
|     table.appendChild(headings);
 | |
| 
 | |
|     for (let entry of entries) {
 | |
|         let row = document.createElement("tr");
 | |
|         for (let elem of entry) {
 | |
|             this.appendColumn(row, "td", elem + "\t");
 | |
|         }
 | |
|         table.appendChild(row);
 | |
|     }
 | |
| 
 | |
|     let dataDiv = document.getElementById("telemetry-log");
 | |
|     removeAllChildNodes(dataDiv);
 | |
|     dataDiv.appendChild(table);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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: function(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").classList.remove("hidden");
 | |
|     }
 | |
| 
 | |
|     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);
 | |
|       slowSqlDiv.appendChild(document.createElement("hr"));
 | |
|     }
 | |
| 
 | |
|     // Other threads
 | |
|     if (otherThreadCount > 0) {
 | |
|       let table = document.createElement("table");
 | |
|       this.renderTableHeader(table, this.otherThreadTitle);
 | |
|       this.renderTable(table, otherThreads);
 | |
| 
 | |
|       slowSqlDiv.appendChild(table);
 | |
|       slowSqlDiv.appendChild(document.createElement("hr"));
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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 + '-data');
 | |
|     removeAllChildNodes(div);
 | |
| 
 | |
|     let fetchE = document.getElementById(aPrefix + '-fetch-symbols');
 | |
|     if (fetchE) {
 | |
|       fetchE.classList.remove("hidden");
 | |
|     }
 | |
|     let hideE = document.getElementById(aPrefix + '-hide-symbols');
 | |
|     if (hideE) {
 | |
|       hideE.classList.add("hidden");
 | |
|     }
 | |
| 
 | |
|     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 + "-data");
 | |
| 
 | |
|     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 RawPayload = {
 | |
|   /**
 | |
|    * Renders the raw payload
 | |
|    */
 | |
|   render: function(aPing) {
 | |
|     setHasData("raw-payload-section", true);
 | |
|     let pre = document.getElementById("raw-payload-data-pre");
 | |
|     pre.textContent = JSON.stringify(aPing.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.classList.add("hidden");
 | |
|   let hideElement = document.getElementById(this.prefix + "-hide-symbols");
 | |
|   hideElement.classList.remove("hidden");
 | |
|   let div = document.getElementById(this.prefix + "-data");
 | |
|   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 ChromeHangs = {
 | |
| 
 | |
|   symbolRequest: null,
 | |
| 
 | |
|   /**
 | |
|    * Renders raw chrome hang data
 | |
|    */
 | |
|   render: function ChromeHangs_render(aPing) {
 | |
|     let hangs = aPing.payload.chromeHangs;
 | |
|     setHasData("chrome-hangs-section", !!hangs);
 | |
|     if (!hangs) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let stacks = hangs.stacks;
 | |
|     let memoryMap = hangs.memoryMap;
 | |
|     let durations = hangs.durations;
 | |
| 
 | |
|     StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap,
 | |
|                                (index) => this.renderHangHeader(index, durations));
 | |
|   },
 | |
| 
 | |
|   renderHangHeader: function ChromeHangs_renderHangHeader(aIndex, aDurations) {
 | |
|     StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, aDurations[aIndex]]);
 | |
|   }
 | |
| };
 | |
| 
 | |
| var ThreadHangStats = {
 | |
| 
 | |
|   /**
 | |
|    * Renders raw thread hang stats data
 | |
|    */
 | |
|   render: function(aPayload) {
 | |
|     let div = document.getElementById("thread-hang-stats");
 | |
|     removeAllChildNodes(div);
 | |
| 
 | |
|     let stats = aPayload.threadHangStats;
 | |
|     setHasData("thread-hang-stats-section", stats && (stats.length > 0));
 | |
|     if (!stats) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     stats.forEach((thread) => {
 | |
|       div.appendChild(this.renderThread(thread));
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Creates and fills data corresponding to a thread
 | |
|    */
 | |
|   renderThread: function(aThread) {
 | |
|     let div = document.createElement("div");
 | |
| 
 | |
|     let title = document.createElement("h2");
 | |
|     title.textContent = aThread.name;
 | |
|     div.appendChild(title);
 | |
| 
 | |
|     // Don't localize the histogram name, because the
 | |
|     // name is also used as the div element's ID
 | |
|     Histogram.render(div, aThread.name + "-Activity",
 | |
|                      aThread.activity, {exponential: true}, true);
 | |
|     aThread.hangs.forEach((hang, index) => {
 | |
|       let hangName = aThread.name + "-Hang-" + (index + 1);
 | |
|       let hangDiv = Histogram.render(
 | |
|         div, hangName, hang.histogram, {exponential: true}, true);
 | |
|       let stackDiv = document.createElement("div");
 | |
|       let stack = hang.nativeStack || hang.stack;
 | |
|       stack.forEach((frame) => {
 | |
|         stackDiv.appendChild(document.createTextNode(frame));
 | |
|         // Leave an extra <br> at the end of the stack listing
 | |
|         stackDiv.appendChild(document.createElement("br"));
 | |
|       });
 | |
|       // Insert stack after the histogram title
 | |
|       hangDiv.insertBefore(stackDiv, hangDiv.childNodes[1]);
 | |
|     });
 | |
|     return div;
 | |
|   },
 | |
| };
 | |
| 
 | |
| 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
 | |
|    * @param aIsBHR whether or not requires fixing the labels for TimeHistogram
 | |
|    */
 | |
|   render: function Histogram_render(aParent, aName, aHgram, aOptions, aIsBHR) {
 | |
|     let options = aOptions || {};
 | |
|     let hgram = this.processHistogram(aHgram, aName, aIsBHR);
 | |
| 
 | |
|     let outerDiv = document.createElement("div");
 | |
|     outerDiv.className = "histogram";
 | |
|     outerDiv.id = aName;
 | |
| 
 | |
|     let divTitle = document.createElement("div");
 | |
|     divTitle.className = "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.appendChild(document.createTextNode(stats));
 | |
|     outerDiv.appendChild(divStats);
 | |
| 
 | |
|     if (isRTL()) {
 | |
|       hgram.buckets.reverse();
 | |
|       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: function(aHgram, aName, aIsBHR) {
 | |
|     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);
 | |
| 
 | |
|     function labelFunc(k) {
 | |
|       // - BHR histograms are TimeHistograms: Exactly power-of-two buckets (from 0)
 | |
|       //   (buckets: [0..1], [2..3], [4..7], [8..15], ... note the 0..1 anomaly - same bucket)
 | |
|       // - TimeHistogram's JS representation adds a dummy (empty) "0" bucket, and
 | |
|       //   the rest of the buckets have the label as the upper value of the
 | |
|       //   bucket (non TimeHistograms have the lower value of the bucket as label).
 | |
|       //   So JS TimeHistograms bucket labels are: 0 (dummy), 1, 3, 7, 15, ...
 | |
|       // - see toolkit/components/telemetry/Telemetry.cpp
 | |
|       //   (CreateJSTimeHistogram, CreateJSThreadHangStats, CreateJSHangHistogram)
 | |
|       // - see toolkit/components/telemetry/ThreadHangStats.h
 | |
|       // Fix BHR labels to the "standard" format for about:telemetry as follows:
 | |
|       //   - The dummy 0 label+bucket will be filtered before arriving here
 | |
|       //   - If it's 1 -> manually correct it to 0 (the 0..1 anomaly)
 | |
|       //   - For the rest, set the label as the bottom value instead of the upper.
 | |
|       //   --> so we'll end with the following (non dummy) labels: 0, 2, 4, 8, 16, ...
 | |
|       if (!aIsBHR) {
 | |
|         return k;
 | |
|       }
 | |
|       return k == 1 ? 0 : (k + 1) / 2;
 | |
|     }
 | |
| 
 | |
|     const labelledValues = Object.keys(aHgram.values)
 | |
|                            .filter(label => !aIsBHR || Number(label) != 0) // remove dummy 0 label for BHR
 | |
|                            .map(k => [labelFunc(Number(k)), aHgram.values[k]]);
 | |
| 
 | |
|     let result = {
 | |
|       values: labelledValues,
 | |
|       pretty_average: average,
 | |
|       max: max_value,
 | |
|       sample_count: 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: function(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) {
 | |
|       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 - String(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 bucket label
 | |
|       barDiv.appendChild(document.createTextNode(label));
 | |
| 
 | |
|       aDiv.appendChild(barDiv);
 | |
|     }
 | |
| 
 | |
|     return text.substr(EOL.length); // Trim the EOL before the first line
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Helper function for filtering histogram elements by their id
 | |
|    * Adds the "filter-blocked" class to histogram nodes whose IDs don't match the filter.
 | |
|    *
 | |
|    * @param aContainerNode Container node containing the histogram class nodes to filter
 | |
|    * @param aFilterText either text or /RegEx/. If text, case-insensitive and AND words
 | |
|    */
 | |
|   filterHistograms: function _filterHistograms(aContainerNode, aFilterText) {
 | |
|     let filter = aFilterText.toString();
 | |
| 
 | |
|     // Pass if: all non-empty array items match (case-sensitive)
 | |
|     function isPassText(subject, filter) {
 | |
|       for (let item of filter) {
 | |
|         if (item.length && subject.indexOf(item) < 0) {
 | |
|           return false; // mismatch and not a spurious space
 | |
|         }
 | |
|       }
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     function isPassRegex(subject, filter) {
 | |
|       return filter.test(subject);
 | |
|     }
 | |
| 
 | |
|     // 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 = isPassText;
 | |
|       filter = filter.toLowerCase().split(" ");
 | |
|     } else {
 | |
|       isPassFunc = 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;
 | |
|         };
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let needLower = (isPassFunc === isPassText);
 | |
| 
 | |
|     let histograms = aContainerNode.getElementsByClassName("histogram");
 | |
|     for (let hist of histograms) {
 | |
|       hist.classList[isPassFunc((needLower ? hist.id.toLowerCase() : hist.id), filter) ? "remove" : "add"]("filter-blocked");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Event handler for change at histograms filter input
 | |
|    *
 | |
|    * When invoked, 'this' is expected to be the filter HTML node.
 | |
|    */
 | |
|   histogramFilterChanged: function _histogramFilterChanged() {
 | |
|     if (this.idleTimeout) {
 | |
|       clearTimeout(this.idleTimeout);
 | |
|     }
 | |
| 
 | |
|     this.idleTimeout = setTimeout( () => {
 | |
|       Histogram.filterHistograms(document.getElementById(this.getAttribute("target_id")), this.value);
 | |
|     }, FILTER_IDLE_TIMEOUT);
 | |
|   }
 | |
| };
 | |
| 
 | |
| /*
 | |
|  * 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 KeyValueTable = {
 | |
|   /**
 | |
|    * Returns a 2-column table with keys and values
 | |
|    * @param aMeasurements Each key in this JS object is rendered as a row in
 | |
|    *                      the table with its corresponding value
 | |
|    * @param aKeysLabel    Column header for the keys column
 | |
|    * @param aValuesLabel  Column header for the values column
 | |
|    */
 | |
|   render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) {
 | |
|     let table = document.createElement("table");
 | |
|     this.renderHeader(table, aKeysLabel, aValuesLabel);
 | |
|     this.renderBody(table, aMeasurements);
 | |
|     return table;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create the table header
 | |
|    * Tabs & newlines added to cells to make it easier to copy-paste.
 | |
|    *
 | |
|    * @param aTable Table element
 | |
|    * @param aKeysLabel    Column header for the keys column
 | |
|    * @param aValuesLabel  Column header for the values column
 | |
|    */
 | |
|   renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) {
 | |
|     let headerRow = document.createElement("tr");
 | |
|     aTable.appendChild(headerRow);
 | |
| 
 | |
|     let keysColumn = document.createElement("th");
 | |
|     keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t"));
 | |
|     let valuesColumn = document.createElement("th");
 | |
|     valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n"));
 | |
| 
 | |
|     headerRow.appendChild(keysColumn);
 | |
|     headerRow.appendChild(valuesColumn);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create the table body
 | |
|    * Tabs & newlines added to cells to make it easier to copy-paste.
 | |
|    *
 | |
|    * @param aTable Table element
 | |
|    * @param aMeasurements Key/value map
 | |
|    */
 | |
|   renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) {
 | |
|     for (let [key, value] of Object.entries(aMeasurements)) {
 | |
|       // use .valueOf() to unbox Number, String, etc. objects
 | |
|       if (value &&
 | |
|          (typeof value == "object") &&
 | |
|          (typeof value.valueOf() == "object")) {
 | |
|         value = RenderObject(value);
 | |
|       }
 | |
| 
 | |
|       let newRow = document.createElement("tr");
 | |
|       aTable.appendChild(newRow);
 | |
| 
 | |
|       let keyField = document.createElement("td");
 | |
|       keyField.appendChild(document.createTextNode(key + "\t"));
 | |
|       newRow.appendChild(keyField);
 | |
| 
 | |
|       let valueField = document.createElement("td");
 | |
|       valueField.appendChild(document.createTextNode(value + "\n"));
 | |
|       newRow.appendChild(valueField);
 | |
|     }
 | |
|   }
 | |
| };
 | |
| 
 | |
| var KeyedHistogram = {
 | |
|   render: function(parent, id, keyedHistogram) {
 | |
|     let outerDiv = document.createElement("div");
 | |
|     outerDiv.className = "keyed-histogram";
 | |
|     outerDiv.id = id;
 | |
| 
 | |
|     let divTitle = document.createElement("div");
 | |
|     divTitle.className = "keyed-histogram-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("h2");
 | |
|       let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
 | |
|       providerSection.appendChild(document.createTextNode(titleText));
 | |
|       addonSection.appendChild(providerSection);
 | |
|       addonSection.appendChild(
 | |
|         KeyValueTable.render(addonDetails[provider],
 | |
|                              this.tableIDTitle, this.tableDetailsTitle));
 | |
|     }
 | |
|   }
 | |
| };
 | |
| 
 | |
| 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: function(aPayload) {
 | |
|     let scalarsSection = document.getElementById("scalars");
 | |
|     removeAllChildNodes(scalarsSection);
 | |
| 
 | |
|     if (!aPayload.processes || !aPayload.processes.parent) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let scalars = aPayload.processes.parent.scalars;
 | |
|     const hasData = scalars && Object.keys(scalars).length > 0;
 | |
|     setHasData("scalars-section", hasData);
 | |
|     if (!hasData) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const headingName = bundle.GetStringFromName("namesHeader");
 | |
|     const headingValue = bundle.GetStringFromName("valuesHeader");
 | |
|     const table = KeyValueTable.render(scalars, headingName, headingValue);
 | |
|     scalarsSection.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");
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Helper function that expands and collapses sections +
 | |
|  * changes caption on the toggle text
 | |
|  */
 | |
| function toggleSection(aEvent) {
 | |
|   let parentElement = aEvent.target.parentElement;
 | |
|   if (!parentElement.classList.contains("has-data") &&
 | |
|       !parentElement.classList.contains("has-subdata")) {
 | |
|     return; // nothing to toggle
 | |
|   }
 | |
| 
 | |
|   parentElement.classList.toggle("expanded");
 | |
| 
 | |
|   // Store section opened/closed state in a hidden checkbox (which is then used on reload)
 | |
|   let statebox = parentElement.getElementsByClassName("statebox")[0];
 | |
|   if (statebox) {
 | |
|     statebox.checked = parentElement.classList.contains("expanded");
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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));
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Initializes load/unload, pref change and mouse-click listeners
 | |
|  */
 | |
| function setupListeners() {
 | |
|   Settings.attachObservers();
 | |
|   PingPicker.attachObservers();
 | |
| 
 | |
|   // Clean up observers when page is closed
 | |
|   window.addEventListener("unload",
 | |
|     function unloadHandler(aEvent) {
 | |
|       window.removeEventListener("unload", unloadHandler);
 | |
|       Settings.detachObservers();
 | |
|   }, false);
 | |
| 
 | |
|   document.getElementById("chrome-hangs-fetch-symbols").addEventListener("click",
 | |
|     function () {
 | |
|       if (!gPingData) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let hangs = gPingData.payload.chromeHangs;
 | |
|       let req = new SymbolicationRequest("chrome-hangs",
 | |
|                                          ChromeHangs.renderHangHeader,
 | |
|                                          hangs.memoryMap,
 | |
|                                          hangs.stacks,
 | |
|                                          hangs.durations);
 | |
|       req.fetchSymbols();
 | |
|   }, false);
 | |
| 
 | |
|   document.getElementById("chrome-hangs-hide-symbols").addEventListener("click",
 | |
|     function () {
 | |
|       if (!gPingData) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       ChromeHangs.render(gPingData);
 | |
|   }, false);
 | |
| 
 | |
|   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();
 | |
|   }, false);
 | |
| 
 | |
|   document.getElementById("late-writes-hide-symbols").addEventListener("click",
 | |
|     function () {
 | |
|       if (!gPingData) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
 | |
|   }, false);
 | |
| 
 | |
|   // Clicking on the section name will toggle its state
 | |
|   let sectionHeaders = document.getElementsByClassName("section-name");
 | |
|   for (let sectionHeader of sectionHeaders) {
 | |
|     sectionHeader.addEventListener("click", toggleSection, false);
 | |
|   }
 | |
| 
 | |
|   // Clicking on the "toggle" text will also toggle section's state
 | |
|   let toggleLinks = document.getElementsByClassName("toggle-caption");
 | |
|   for (let toggleLink of toggleLinks) {
 | |
|     toggleLink.addEventListener("click", toggleSection, false);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function onLoad() {
 | |
|   window.removeEventListener("load", onLoad);
 | |
| 
 | |
|   // Set the text in the page header
 | |
|   setupPageHeader();
 | |
| 
 | |
|   // Set up event listeners
 | |
|   setupListeners();
 | |
| 
 | |
|   // Render settings.
 | |
|   Settings.render();
 | |
| 
 | |
|   // Restore sections states
 | |
|   let stateboxes = document.getElementsByClassName("statebox");
 | |
|   for (let box of stateboxes) {
 | |
|     if (box.checked) { // Was open. Will still display as empty if not has-data
 | |
|         box.parentElement.classList.add("expanded");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Update ping data when async Telemetry init is finished.
 | |
|   Telemetry.asyncFetchTelemetryData(() => 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);
 | |
|   }
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * 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
 | |
|  */
 | |
| function 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 renderPayloadList(ping) {
 | |
|   // Rebuild the payload select with options:
 | |
|   //   Parent Payload (selected)
 | |
|   //   Child Payload 1..ping.payload.childPayloads.length
 | |
|   let listEl = document.getElementById("choose-payload");
 | |
|   removeAllChildNodes(listEl);
 | |
| 
 | |
|   let option = document.createElement("option");
 | |
|   let text = bundle.GetStringFromName("parentPayload");
 | |
|   let content = document.createTextNode(text);
 | |
|   let payloadIndex = 0;
 | |
|   option.appendChild(content);
 | |
|   option.setAttribute("value", payloadIndex++);
 | |
|   option.selected = true;
 | |
|   listEl.appendChild(option);
 | |
| 
 | |
|   if (!ping.payload.childPayloads) {
 | |
|     listEl.disabled = true;
 | |
|     return
 | |
|   }
 | |
|   listEl.disabled = false;
 | |
| 
 | |
|   for (; payloadIndex <= ping.payload.childPayloads.length; ++payloadIndex) {
 | |
|     option = document.createElement("option");
 | |
|     text = bundle.formatStringFromName("childPayloadN", [payloadIndex], 1);
 | |
|     content = document.createTextNode(text);
 | |
|     option.appendChild(content);
 | |
|     option.setAttribute("value", payloadIndex);
 | |
|     listEl.appendChild(option);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function toggleElementHidden(element, isHidden) {
 | |
|   if (isHidden) {
 | |
|     element.classList.add("hidden");
 | |
|   } else {
 | |
|     element.classList.remove("hidden");
 | |
|   }
 | |
| }
 | |
| 
 | |
| function togglePingSections(isMainPing) {
 | |
|   // We always show the sections that are "common" to all pings.
 | |
|   // The raw payload section is only used for pings other than "main" and "saved-session".
 | |
|   let commonSections = new Set(["general-data-section", "environment-data-section"]);
 | |
|   let otherPingSections = new Set(["raw-payload-section"]);
 | |
| 
 | |
|   let elements = document.getElementById("structured-ping-data-section").children;
 | |
|   for (let section of elements) {
 | |
|     if (commonSections.has(section.id)) {
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     let showElement = isMainPing != otherPingSections.has(section.id);
 | |
|     toggleElementHidden(section, !showElement);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function displayPingData(ping, updatePayloadList = false) {
 | |
|   gPingData = ping;
 | |
| 
 | |
|   // Render raw ping data.
 | |
|   let pre = document.getElementById("raw-ping-data");
 | |
|   pre.textContent = JSON.stringify(gPingData, null, 2);
 | |
| 
 | |
|   // Update the structured data rendering.
 | |
|   const keysHeader = bundle.GetStringFromName("keysHeader");
 | |
|   const valuesHeader = bundle.GetStringFromName("valuesHeader");
 | |
| 
 | |
|   // Update the payload list
 | |
|   if (updatePayloadList) {
 | |
|     renderPayloadList(ping);
 | |
|   }
 | |
| 
 | |
|   // Show general data.
 | |
|   GeneralData.render(ping);
 | |
| 
 | |
|   // Show environment data.
 | |
|   EnvironmentData.render(ping);
 | |
| 
 | |
|   // We only have special rendering code for the payloads from "main" pings.
 | |
|   // For any other pings we just render the raw JSON payload.
 | |
|   let isMainPing = (ping.type == "main" || ping.type == "saved-session");
 | |
|   togglePingSections(isMainPing);
 | |
| 
 | |
|   if (!isMainPing) {
 | |
|     RawPayload.render(ping);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   // Show telemetry log.
 | |
|   TelLog.render(ping);
 | |
| 
 | |
|   // Show slow SQL stats
 | |
|   SlowSQL.render(ping);
 | |
| 
 | |
|   // Show chrome hang stacks
 | |
|   ChromeHangs.render(ping);
 | |
| 
 | |
|   // Render Addon details.
 | |
|   AddonDetails.render(ping);
 | |
| 
 | |
|   // Select payload to render
 | |
|   let payloadSelect = document.getElementById("choose-payload");
 | |
|   let payloadOption = payloadSelect.selectedOptions.item(0);
 | |
|   let payloadIndex = payloadOption.getAttribute("value");
 | |
| 
 | |
|   let payload = ping.payload;
 | |
|   if (payloadIndex > 0) {
 | |
|     payload = ping.payload.childPayloads[payloadIndex - 1];
 | |
|   }
 | |
| 
 | |
|   // Show thread hang stats
 | |
|   ThreadHangStats.render(payload);
 | |
| 
 | |
|   // Show simple measurements
 | |
|   let simpleMeasurements = sortStartupMilestones(payload.simpleMeasurements);
 | |
|   let hasData = Object.keys(simpleMeasurements).length > 0;
 | |
|   setHasData("simple-measurements-section", hasData);
 | |
|   let simpleSection = document.getElementById("simple-measurements");
 | |
|   removeAllChildNodes(simpleSection);
 | |
| 
 | |
|   if (hasData) {
 | |
|     simpleSection.appendChild(KeyValueTable.render(simpleMeasurements,
 | |
|                                                    keysHeader, valuesHeader));
 | |
|   }
 | |
| 
 | |
|   LateWritesSingleton.renderLateWrites(payload.lateWrites);
 | |
| 
 | |
|   // Show basic session info gathered
 | |
|   hasData = Object.keys(ping.payload.info).length > 0;
 | |
|   setHasData("session-info-section", hasData);
 | |
|   let infoSection = document.getElementById("session-info");
 | |
|   removeAllChildNodes(infoSection);
 | |
| 
 | |
|   if (hasData) {
 | |
|     infoSection.appendChild(KeyValueTable.render(ping.payload.info,
 | |
|                                                  keysHeader, valuesHeader));
 | |
|   }
 | |
| 
 | |
|   // Show scalar data.
 | |
|   Scalars.render(payload);
 | |
| 
 | |
|   // Show histogram data
 | |
|   let hgramDiv = document.getElementById("histograms");
 | |
|   removeAllChildNodes(hgramDiv);
 | |
| 
 | |
|   let histograms = payload.histograms;
 | |
|   hasData = Object.keys(histograms).length > 0;
 | |
|   setHasData("histograms-section", hasData);
 | |
| 
 | |
|   if (hasData) {
 | |
|     for (let [name, hgram] of Object.entries(histograms)) {
 | |
|       Histogram.render(hgramDiv, name, hgram, {unpacked: true});
 | |
|     }
 | |
| 
 | |
|     let filterBox = document.getElementById("histograms-filter");
 | |
|     filterBox.addEventListener("input", Histogram.histogramFilterChanged, false);
 | |
|     if (filterBox.value.trim() != "") { // on load, no need to filter if empty
 | |
|       Histogram.filterHistograms(hgramDiv, filterBox.value);
 | |
|     }
 | |
| 
 | |
|     setHasData("histograms-section", true);
 | |
|   }
 | |
| 
 | |
|   // Show keyed histogram data
 | |
|   let keyedDiv = document.getElementById("keyed-histograms");
 | |
|   removeAllChildNodes(keyedDiv);
 | |
| 
 | |
|   setHasData("keyed-histograms-section", false);
 | |
|   let keyedHistograms = payload.keyedHistograms;
 | |
|   if (keyedHistograms) {
 | |
|     let hasData = false;
 | |
|     for (let [id, keyed] of Object.entries(keyedHistograms)) {
 | |
|       if (Object.keys(keyed).length > 0) {
 | |
|         hasData = true;
 | |
|         KeyedHistogram.render(keyedDiv, id, keyed, {unpacked: true});
 | |
|       }
 | |
|     }
 | |
|     setHasData("keyed-histograms-section", hasData);
 | |
|   }
 | |
| 
 | |
|   // Show addon histogram data
 | |
|   let addonDiv = document.getElementById("addon-histograms");
 | |
|   removeAllChildNodes(addonDiv);
 | |
| 
 | |
|   let addonHistogramsRendered = false;
 | |
|   let addonData = payload.addonHistograms;
 | |
|   if (addonData) {
 | |
|     for (let [addon, histograms] of Object.entries(addonData)) {
 | |
|       for (let [name, hgram] of Object.entries(histograms)) {
 | |
|         addonHistogramsRendered = true;
 | |
|         Histogram.render(addonDiv, addon + ": " + name, hgram, {unpacked: true});
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   setHasData("addon-histograms-section", addonHistogramsRendered);
 | |
| }
 | |
| 
 | |
| window.addEventListener("load", onLoad, false);
 | 
