/* 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) { if (!obj) { return; } 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); document.getElementById("histograms-processes") .addEventListener("change", () => displayPingData(gPingData), false); document.getElementById("keyed-histograms-processes") .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 GenericTable = { /** * 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: function(rows, headings) { 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: function(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: function(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"); 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: 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); } }; 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: function(aPayload) { let scalarsSection = document.getElementById("keyed-scalars"); removeAllChildNodes(scalarsSection); if (!aPayload.processes || !aPayload.processes.parent) { return; } let keyedScalars = aPayload.processes.parent.keyedScalars; const hasData = keyedScalars && Object.keys(keyedScalars).length > 0; setHasData("keyed-scalars-section", hasData); if (!hasData) { return; } const headingName = bundle.GetStringFromName("namesHeader"); const headingValue = bundle.GetStringFromName("valuesHeader"); for (let scalar in keyedScalars) { // Add the name of the scalar. let scalarNameSection = document.createElement("h2"); scalarNameSection.appendChild(document.createTextNode(scalar)); scalarsSection.appendChild(scalarNameSection); // Populate the section with the key-value pairs from the scalar. const table = KeyValueTable.render(keyedScalars[scalar], headingName, headingValue); scalarsSection.appendChild(table); } } }; 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: function(aPayload) { let eventsSection = document.getElementById("events"); removeAllChildNodes(eventsSection); if (!aPayload.processes || !aPayload.processes.parent) { return; } const events = aPayload.processes.parent.events; const hasData = events && Object.keys(events).length > 0; setHasData("events-section", hasData); if (!hasData) { return; } const headings = [ "timestamp", "category", "method", "object", "value", "extra", ]; 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"); } /** * 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 renderProcessList(ping, selectEl) { removeAllChildNodes(selectEl); let option = document.createElement("option"); option.appendChild(document.createTextNode("parent")); option.setAttribute("value", ""); option.selected = true; selectEl.appendChild(option); if (!("processes" in ping.payload)) { selectEl.disabled = true; return; } selectEl.disabled = false; for (let process of Object.keys(ping.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 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 and process lists if (updatePayloadList) { renderPayloadList(ping); renderProcessList(ping, document.getElementById("histograms-processes")); renderProcessList(ping, document.getElementById("keyed-histograms-processes")); } // 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); KeyedScalars.render(payload); // Show histogram data let hgramDiv = document.getElementById("histograms"); removeAllChildNodes(hgramDiv); let histograms = payload.histograms; let hgramsSelect = document.getElementById("histograms-processes"); let hgramsOption = hgramsSelect.selectedOptions.item(0); let hgramsProcess = hgramsOption.getAttribute("value"); if (hgramsProcess && "processes" in ping.payload && hgramsProcess in ping.payload.processes) { histograms = ping.payload.processes[hgramsProcess].histograms; } hasData = Object.keys(histograms).length > 0; setHasData("histograms-section", hasData || hgramsSelect.options.length); 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); let keyedHistograms = payload.keyedHistograms; let keyedHgramsSelect = document.getElementById("keyed-histograms-processes"); let keyedHgramsOption = keyedHgramsSelect.selectedOptions.item(0); let keyedHgramsProcess = keyedHgramsOption.getAttribute("value"); if (keyedHgramsProcess && "processes" in ping.payload && keyedHgramsProcess in ping.payload.processes) { keyedHistograms = ping.payload.processes[keyedHgramsProcess].keyedHistograms; } setHasData("keyed-histograms-section", keyedHgramsSelect.options.length); 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 || keyedHgramsSelect.options.length); } // Show event data. Events.render(payload); // 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);