michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: 'use strict';
michael@0:
michael@0: const Ci = Components.interfaces;
michael@0: const Cc = Components.classes;
michael@0: const Cu = Components.utils;
michael@0:
michael@0: Cu.import("resource://gre/modules/Services.jsm");
michael@0: Cu.import("resource://gre/modules/TelemetryTimestamps.jsm");
michael@0: Cu.import("resource://gre/modules/TelemetryPing.jsm");
michael@0:
michael@0: const Telemetry = Services.telemetry;
michael@0: const bundle = Services.strings.createBundle(
michael@0: "chrome://global/locale/aboutTelemetry.properties");
michael@0: const brandBundle = Services.strings.createBundle(
michael@0: "chrome://branding/locale/brand.properties");
michael@0:
michael@0: // Maximum height of a histogram bar (in em for html, in chars for text)
michael@0: const MAX_BAR_HEIGHT = 18;
michael@0: const MAX_BAR_CHARS = 25;
michael@0: const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner";
michael@0: const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
michael@0: const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql";
michael@0: const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl";
michael@0: const DEFAULT_SYMBOL_SERVER_URI = "http://symbolapi.mozilla.org";
michael@0:
michael@0: // ms idle before applying the filter (allow uninterrupted typing)
michael@0: const FILTER_IDLE_TIMEOUT = 500;
michael@0:
michael@0: #ifdef XP_WIN
michael@0: const EOL = "\r\n";
michael@0: #else
michael@0: const EOL = "\n";
michael@0: #endif
michael@0:
michael@0: // Cached value of document's RTL mode
michael@0: let documentRTLMode = "";
michael@0:
michael@0: /**
michael@0: * Helper function for fetching a config pref
michael@0: *
michael@0: * @param aPrefName Name of config pref to fetch.
michael@0: * @param aDefault Default value to return if pref isn't set.
michael@0: * @return Value of pref
michael@0: */
michael@0: function getPref(aPrefName, aDefault) {
michael@0: let result = aDefault;
michael@0:
michael@0: try {
michael@0: let prefType = Services.prefs.getPrefType(aPrefName);
michael@0: if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
michael@0: result = Services.prefs.getBoolPref(aPrefName);
michael@0: } else if (prefType == Ci.nsIPrefBranch.PREF_STRING) {
michael@0: result = Services.prefs.getCharPref(aPrefName);
michael@0: }
michael@0: } catch (e) {
michael@0: // Return default if Prefs service throws exception
michael@0: }
michael@0:
michael@0: return result;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Helper function for determining whether the document direction is RTL.
michael@0: * Caches result of check on first invocation.
michael@0: */
michael@0: function isRTL() {
michael@0: if (!documentRTLMode)
michael@0: documentRTLMode = window.getComputedStyle(document.body).direction;
michael@0: return (documentRTLMode == "rtl");
michael@0: }
michael@0:
michael@0: let observer = {
michael@0:
michael@0: enableTelemetry: bundle.GetStringFromName("enableTelemetry"),
michael@0:
michael@0: disableTelemetry: bundle.GetStringFromName("disableTelemetry"),
michael@0:
michael@0: /**
michael@0: * Observer is called whenever Telemetry is enabled or disabled
michael@0: */
michael@0: observe: function observe(aSubject, aTopic, aData) {
michael@0: if (aData == PREF_TELEMETRY_ENABLED) {
michael@0: this.updatePrefStatus();
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Updates the button & text at the top of the page to reflect Telemetry state.
michael@0: */
michael@0: updatePrefStatus: function updatePrefStatus() {
michael@0: // Notify user whether Telemetry is enabled
michael@0: let enabledElement = document.getElementById("description-enabled");
michael@0: let disabledElement = document.getElementById("description-disabled");
michael@0: let toggleElement = document.getElementById("toggle-telemetry");
michael@0: if (getPref(PREF_TELEMETRY_ENABLED, false)) {
michael@0: enabledElement.classList.remove("hidden");
michael@0: disabledElement.classList.add("hidden");
michael@0: toggleElement.innerHTML = this.disableTelemetry;
michael@0: } else {
michael@0: enabledElement.classList.add("hidden");
michael@0: disabledElement.classList.remove("hidden");
michael@0: toggleElement.innerHTML = this.enableTelemetry;
michael@0: }
michael@0: }
michael@0: };
michael@0:
michael@0: let SlowSQL = {
michael@0:
michael@0: slowSqlHits: bundle.GetStringFromName("slowSqlHits"),
michael@0:
michael@0: slowSqlAverage: bundle.GetStringFromName("slowSqlAverage"),
michael@0:
michael@0: slowSqlStatement: bundle.GetStringFromName("slowSqlStatement"),
michael@0:
michael@0: mainThreadTitle: bundle.GetStringFromName("slowSqlMain"),
michael@0:
michael@0: otherThreadTitle: bundle.GetStringFromName("slowSqlOther"),
michael@0:
michael@0: /**
michael@0: * Render slow SQL statistics
michael@0: */
michael@0: render: function SlowSQL_render() {
michael@0: let debugSlowSql = getPref(PREF_DEBUG_SLOW_SQL, false);
michael@0: let {mainThread, otherThreads} =
michael@0: Telemetry[debugSlowSql ? "debugSlowSQL" : "slowSQL"];
michael@0:
michael@0: let mainThreadCount = Object.keys(mainThread).length;
michael@0: let otherThreadCount = Object.keys(otherThreads).length;
michael@0: if (mainThreadCount == 0 && otherThreadCount == 0) {
michael@0: return;
michael@0: }
michael@0:
michael@0: setHasData("slow-sql-section", true);
michael@0:
michael@0: if (debugSlowSql) {
michael@0: document.getElementById("sql-warning").classList.remove("hidden");
michael@0: }
michael@0:
michael@0: let slowSqlDiv = document.getElementById("slow-sql-tables");
michael@0:
michael@0: // Main thread
michael@0: if (mainThreadCount > 0) {
michael@0: let table = document.createElement("table");
michael@0: this.renderTableHeader(table, this.mainThreadTitle);
michael@0: this.renderTable(table, mainThread);
michael@0:
michael@0: slowSqlDiv.appendChild(table);
michael@0: slowSqlDiv.appendChild(document.createElement("hr"));
michael@0: }
michael@0:
michael@0: // Other threads
michael@0: if (otherThreadCount > 0) {
michael@0: let table = document.createElement("table");
michael@0: this.renderTableHeader(table, this.otherThreadTitle);
michael@0: this.renderTable(table, otherThreads);
michael@0:
michael@0: slowSqlDiv.appendChild(table);
michael@0: slowSqlDiv.appendChild(document.createElement("hr"));
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Creates a header row for a Slow SQL table
michael@0: * Tabs & newlines added to cells to make it easier to copy-paste.
michael@0: *
michael@0: * @param aTable Parent table element
michael@0: * @param aTitle Table's title
michael@0: */
michael@0: renderTableHeader: function SlowSQL_renderTableHeader(aTable, aTitle) {
michael@0: let caption = document.createElement("caption");
michael@0: caption.appendChild(document.createTextNode(aTitle + "\n"));
michael@0: aTable.appendChild(caption);
michael@0:
michael@0: let headings = document.createElement("tr");
michael@0: this.appendColumn(headings, "th", this.slowSqlHits + "\t");
michael@0: this.appendColumn(headings, "th", this.slowSqlAverage + "\t");
michael@0: this.appendColumn(headings, "th", this.slowSqlStatement + "\n");
michael@0: aTable.appendChild(headings);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Fills out the table body
michael@0: * Tabs & newlines added to cells to make it easier to copy-paste.
michael@0: *
michael@0: * @param aTable Parent table element
michael@0: * @param aSql SQL stats object
michael@0: */
michael@0: renderTable: function SlowSQL_renderTable(aTable, aSql) {
michael@0: for (let [sql, [hitCount, totalTime]] of Iterator(aSql)) {
michael@0: let averageTime = totalTime / hitCount;
michael@0:
michael@0: let sqlRow = document.createElement("tr");
michael@0:
michael@0: this.appendColumn(sqlRow, "td", hitCount + "\t");
michael@0: this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
michael@0: this.appendColumn(sqlRow, "td", sql + "\n");
michael@0:
michael@0: aTable.appendChild(sqlRow);
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Helper function for appending a column to a Slow SQL table.
michael@0: *
michael@0: * @param aRowElement Parent row element
michael@0: * @param aColType Column's tag name
michael@0: * @param aColText Column contents
michael@0: */
michael@0: appendColumn: function SlowSQL_appendColumn(aRowElement, aColType, aColText) {
michael@0: let colElement = document.createElement(aColType);
michael@0: let colTextElement = document.createTextNode(aColText);
michael@0: colElement.appendChild(colTextElement);
michael@0: aRowElement.appendChild(colElement);
michael@0: }
michael@0: };
michael@0:
michael@0: /**
michael@0: * Removes child elements from the supplied div
michael@0: *
michael@0: * @param aDiv Element to be cleared
michael@0: */
michael@0: function clearDivData(aDiv) {
michael@0: while (aDiv.hasChildNodes()) {
michael@0: aDiv.removeChild(aDiv.lastChild);
michael@0: }
michael@0: };
michael@0:
michael@0: let StackRenderer = {
michael@0:
michael@0: stackTitle: bundle.GetStringFromName("stackTitle"),
michael@0:
michael@0: memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"),
michael@0:
michael@0: /**
michael@0: * Outputs the memory map associated with this hang report
michael@0: *
michael@0: * @param aDiv Output div
michael@0: */
michael@0: renderMemoryMap: function StackRenderer_renderMemoryMap(aDiv, memoryMap) {
michael@0: aDiv.appendChild(document.createTextNode(this.memoryMapTitle));
michael@0: aDiv.appendChild(document.createElement("br"));
michael@0:
michael@0: for (let currentModule of memoryMap) {
michael@0: aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
michael@0: aDiv.appendChild(document.createElement("br"));
michael@0: }
michael@0:
michael@0: aDiv.appendChild(document.createElement("br"));
michael@0: },
michael@0:
michael@0: /**
michael@0: * Outputs the raw PCs from the hang's stack
michael@0: *
michael@0: * @param aDiv Output div
michael@0: * @param aStack Array of PCs from the hang stack
michael@0: */
michael@0: renderStack: function StackRenderer_renderStack(aDiv, aStack) {
michael@0: aDiv.appendChild(document.createTextNode(this.stackTitle));
michael@0: let stackText = " " + aStack.join(" ");
michael@0: aDiv.appendChild(document.createTextNode(stackText));
michael@0:
michael@0: aDiv.appendChild(document.createElement("br"));
michael@0: aDiv.appendChild(document.createElement("br"));
michael@0: },
michael@0: renderStacks: function StackRenderer_renderStacks(aPrefix, aStacks,
michael@0: aMemoryMap, aRenderHeader) {
michael@0: let div = document.getElementById(aPrefix + '-data');
michael@0: clearDivData(div);
michael@0:
michael@0: let fetchE = document.getElementById(aPrefix + '-fetch-symbols');
michael@0: if (fetchE) {
michael@0: fetchE.classList.remove("hidden");
michael@0: }
michael@0: let hideE = document.getElementById(aPrefix + '-hide-symbols');
michael@0: if (hideE) {
michael@0: hideE.classList.add("hidden");
michael@0: }
michael@0:
michael@0: if (aStacks.length == 0) {
michael@0: return;
michael@0: }
michael@0:
michael@0: setHasData(aPrefix + '-section', true);
michael@0:
michael@0: this.renderMemoryMap(div, aMemoryMap);
michael@0:
michael@0: for (let i = 0; i < aStacks.length; ++i) {
michael@0: let stack = aStacks[i];
michael@0: aRenderHeader(i);
michael@0: this.renderStack(div, stack)
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Renders the title of the stack: e.g. "Late Write #1" or
michael@0: * "Hang Report #1 (6 seconds)".
michael@0: *
michael@0: * @param aFormatArgs formating args to be passed to formatStringFromName.
michael@0: */
michael@0: renderHeader: function StackRenderer_renderHeader(aPrefix, aFormatArgs) {
michael@0: let div = document.getElementById(aPrefix + "-data");
michael@0:
michael@0: let titleElement = document.createElement("span");
michael@0: titleElement.className = "stack-title";
michael@0:
michael@0: let titleText = bundle.formatStringFromName(
michael@0: aPrefix + "-title", aFormatArgs, aFormatArgs.length);
michael@0: titleElement.appendChild(document.createTextNode(titleText));
michael@0:
michael@0: div.appendChild(titleElement);
michael@0: div.appendChild(document.createElement("br"));
michael@0: }
michael@0: };
michael@0:
michael@0: function SymbolicationRequest(aPrefix, aRenderHeader, aMemoryMap, aStacks) {
michael@0: this.prefix = aPrefix;
michael@0: this.renderHeader = aRenderHeader;
michael@0: this.memoryMap = aMemoryMap;
michael@0: this.stacks = aStacks;
michael@0: }
michael@0: /**
michael@0: * A callback for onreadystatechange. It replaces the numeric stack with
michael@0: * the symbolicated one returned by the symbolication server.
michael@0: */
michael@0: SymbolicationRequest.prototype.handleSymbolResponse =
michael@0: function SymbolicationRequest_handleSymbolResponse() {
michael@0: if (this.symbolRequest.readyState != 4)
michael@0: return;
michael@0:
michael@0: let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
michael@0: fetchElement.classList.add("hidden");
michael@0: let hideElement = document.getElementById(this.prefix + "-hide-symbols");
michael@0: hideElement.classList.remove("hidden");
michael@0: let div = document.getElementById(this.prefix + "-data");
michael@0: clearDivData(div);
michael@0: let errorMessage = bundle.GetStringFromName("errorFetchingSymbols");
michael@0:
michael@0: if (this.symbolRequest.status != 200) {
michael@0: div.appendChild(document.createTextNode(errorMessage));
michael@0: return;
michael@0: }
michael@0:
michael@0: let jsonResponse = {};
michael@0: try {
michael@0: jsonResponse = JSON.parse(this.symbolRequest.responseText);
michael@0: } catch (e) {
michael@0: div.appendChild(document.createTextNode(errorMessage));
michael@0: return;
michael@0: }
michael@0:
michael@0: for (let i = 0; i < jsonResponse.length; ++i) {
michael@0: let stack = jsonResponse[i];
michael@0: this.renderHeader(i);
michael@0:
michael@0: for (let symbol of stack) {
michael@0: div.appendChild(document.createTextNode(symbol));
michael@0: div.appendChild(document.createElement("br"));
michael@0: }
michael@0: div.appendChild(document.createElement("br"));
michael@0: }
michael@0: };
michael@0: /**
michael@0: * Send a request to the symbolication server to symbolicate this stack.
michael@0: */
michael@0: SymbolicationRequest.prototype.fetchSymbols =
michael@0: function SymbolicationRequest_fetchSymbols() {
michael@0: let symbolServerURI =
michael@0: getPref(PREF_SYMBOL_SERVER_URI, DEFAULT_SYMBOL_SERVER_URI);
michael@0: let request = {"memoryMap" : this.memoryMap, "stacks" : this.stacks,
michael@0: "version" : 3};
michael@0: let requestJSON = JSON.stringify(request);
michael@0:
michael@0: this.symbolRequest = new XMLHttpRequest();
michael@0: this.symbolRequest.open("POST", symbolServerURI, true);
michael@0: this.symbolRequest.setRequestHeader("Content-type", "application/json");
michael@0: this.symbolRequest.setRequestHeader("Content-length",
michael@0: requestJSON.length);
michael@0: this.symbolRequest.setRequestHeader("Connection", "close");
michael@0: this.symbolRequest.onreadystatechange = this.handleSymbolResponse.bind(this);
michael@0: this.symbolRequest.send(requestJSON);
michael@0: }
michael@0:
michael@0: let ChromeHangs = {
michael@0:
michael@0: symbolRequest: null,
michael@0:
michael@0: /**
michael@0: * Renders raw chrome hang data
michael@0: */
michael@0: render: function ChromeHangs_render() {
michael@0: let hangs = Telemetry.chromeHangs;
michael@0: let stacks = hangs.stacks;
michael@0: let memoryMap = hangs.memoryMap;
michael@0:
michael@0: StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap,
michael@0: this.renderHangHeader);
michael@0: },
michael@0:
michael@0: renderHangHeader: function ChromeHangs_renderHangHeader(aIndex) {
michael@0: let durations = Telemetry.chromeHangs.durations;
michael@0: StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, durations[aIndex]]);
michael@0: }
michael@0: };
michael@0:
michael@0: let ThreadHangStats = {
michael@0:
michael@0: /**
michael@0: * Renders raw thread hang stats data
michael@0: */
michael@0: render: function() {
michael@0: let div = document.getElementById("thread-hang-stats");
michael@0: clearDivData(div);
michael@0:
michael@0: let stats = Telemetry.threadHangStats;
michael@0: stats.forEach((thread) => {
michael@0: div.appendChild(this.renderThread(thread));
michael@0: });
michael@0: if (stats.length) {
michael@0: setHasData("thread-hang-stats-section", true);
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Creates and fills data corresponding to a thread
michael@0: */
michael@0: renderThread: function(aThread) {
michael@0: let div = document.createElement("div");
michael@0:
michael@0: let title = document.createElement("h2");
michael@0: title.textContent = aThread.name;
michael@0: div.appendChild(title);
michael@0:
michael@0: // Don't localize the histogram name, because the
michael@0: // name is also used as the div element's ID
michael@0: Histogram.render(div, aThread.name + "-Activity",
michael@0: aThread.activity, {exponential: true});
michael@0: aThread.hangs.forEach((hang, index) => {
michael@0: let hangName = aThread.name + "-Hang-" + (index + 1);
michael@0: let hangDiv = Histogram.render(
michael@0: div, hangName, hang.histogram, {exponential: true});
michael@0: let stackDiv = document.createElement("div");
michael@0: hang.stack.forEach((frame) => {
michael@0: stackDiv.appendChild(document.createTextNode(frame));
michael@0: // Leave an extra
at the end of the stack listing
michael@0: stackDiv.appendChild(document.createElement("br"));
michael@0: });
michael@0: // Insert stack after the histogram title
michael@0: hangDiv.insertBefore(stackDiv, hangDiv.childNodes[1]);
michael@0: });
michael@0: return div;
michael@0: },
michael@0: };
michael@0:
michael@0: let Histogram = {
michael@0:
michael@0: hgramSamplesCaption: bundle.GetStringFromName("histogramSamples"),
michael@0:
michael@0: hgramAverageCaption: bundle.GetStringFromName("histogramAverage"),
michael@0:
michael@0: hgramSumCaption: bundle.GetStringFromName("histogramSum"),
michael@0:
michael@0: hgramCopyCaption: bundle.GetStringFromName("histogramCopy"),
michael@0:
michael@0: /**
michael@0: * Renders a single Telemetry histogram
michael@0: *
michael@0: * @param aParent Parent element
michael@0: * @param aName Histogram name
michael@0: * @param aHgram Histogram information
michael@0: * @param aOptions Object with render options
michael@0: * * exponential: bars follow logarithmic scale
michael@0: */
michael@0: render: function Histogram_render(aParent, aName, aHgram, aOptions) {
michael@0: let hgram = this.unpack(aHgram);
michael@0: let options = aOptions || {};
michael@0:
michael@0: let outerDiv = document.createElement("div");
michael@0: outerDiv.className = "histogram";
michael@0: outerDiv.id = aName;
michael@0:
michael@0: let divTitle = document.createElement("div");
michael@0: divTitle.className = "histogram-title";
michael@0: divTitle.appendChild(document.createTextNode(aName));
michael@0: outerDiv.appendChild(divTitle);
michael@0:
michael@0: let stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " +
michael@0: this.hgramAverageCaption + " = " + hgram.pretty_average + ", " +
michael@0: this.hgramSumCaption + " = " + hgram.sum;
michael@0:
michael@0: let divStats = document.createElement("div");
michael@0: divStats.appendChild(document.createTextNode(stats));
michael@0: outerDiv.appendChild(divStats);
michael@0:
michael@0: if (isRTL())
michael@0: hgram.values.reverse();
michael@0:
michael@0: let textData = this.renderValues(outerDiv, hgram.values, hgram.max,
michael@0: hgram.sample_count, options);
michael@0:
michael@0: // The 'Copy' button contains the textual data, copied to clipboard on click
michael@0: let copyButton = document.createElement("button");
michael@0: copyButton.className = "copy-node";
michael@0: copyButton.appendChild(document.createTextNode(this.hgramCopyCaption));
michael@0: copyButton.histogramText = aName + EOL + stats + EOL + EOL + textData;
michael@0: copyButton.addEventListener("click", function(){
michael@0: Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper)
michael@0: .copyString(this.histogramText);
michael@0: });
michael@0: outerDiv.appendChild(copyButton);
michael@0:
michael@0: aParent.appendChild(outerDiv);
michael@0: return outerDiv;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Unpacks histogram values
michael@0: *
michael@0: * @param aHgram Packed histogram
michael@0: *
michael@0: * @return Unpacked histogram representation
michael@0: */
michael@0: unpack: function Histogram_unpack(aHgram) {
michael@0: let sample_count = aHgram.counts.reduceRight(function (a, b) a + b);
michael@0: let buckets = [0, 1];
michael@0: if (aHgram.histogram_type != Telemetry.HISTOGRAM_BOOLEAN) {
michael@0: buckets = aHgram.ranges;
michael@0: }
michael@0:
michael@0: let average = Math.round(aHgram.sum * 10 / sample_count) / 10;
michael@0: let max_value = Math.max.apply(Math, aHgram.counts);
michael@0:
michael@0: let first = true;
michael@0: let last = 0;
michael@0: let values = [];
michael@0: for (let i = 0; i < buckets.length; i++) {
michael@0: let count = aHgram.counts[i];
michael@0: if (!count)
michael@0: continue;
michael@0: if (first) {
michael@0: first = false;
michael@0: if (i) {
michael@0: values.push([buckets[i - 1], 0]);
michael@0: }
michael@0: }
michael@0: last = i + 1;
michael@0: values.push([buckets[i], count]);
michael@0: }
michael@0: if (last && last < buckets.length) {
michael@0: values.push([buckets[last], 0]);
michael@0: }
michael@0:
michael@0: let result = {
michael@0: values: values,
michael@0: pretty_average: average,
michael@0: max: max_value,
michael@0: sample_count: sample_count,
michael@0: sum: aHgram.sum
michael@0: };
michael@0:
michael@0: return result;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Return a non-negative, logarithmic representation of a non-negative number.
michael@0: * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
michael@0: *
michael@0: * @param aNumber Non-negative number
michael@0: */
michael@0: getLogValue: function(aNumber) {
michael@0: return Math.max(0, Math.log10(aNumber) + 1);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Create histogram HTML bars, also returns a textual representation
michael@0: * Both aMaxValue and aSumValues must be positive.
michael@0: * Values are assumed to use 0 as baseline.
michael@0: *
michael@0: * @param aDiv Outer parent div
michael@0: * @param aValues Histogram values
michael@0: * @param aMaxValue Value of the longest bar (length, not label)
michael@0: * @param aSumValues Sum of all bar values
michael@0: * @param aOptions Object with render options (@see #render)
michael@0: */
michael@0: renderValues: function Histogram_renderValues(aDiv, aValues, aMaxValue, aSumValues, aOptions) {
michael@0: let text = "";
michael@0: // If the last label is not the longest string, alignment will break a little
michael@0: let labelPadTo = String(aValues[aValues.length -1][0]).length;
michael@0: let maxBarValue = aOptions.exponential ? this.getLogValue(aMaxValue) : aMaxValue;
michael@0:
michael@0: for (let [label, value] of aValues) {
michael@0: let barValue = aOptions.exponential ? this.getLogValue(value) : value;
michael@0:
michael@0: // Create a text representation: |
michael@0: text += EOL
michael@0: + " ".repeat(Math.max(0, labelPadTo - String(label).length)) + label // Right-aligned label
michael@0: + " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar
michael@0: + " " + value // Value
michael@0: + " " + Math.round(100 * value / aSumValues) + "%"; // Percentage
michael@0:
michael@0: // Construct the HTML labels + bars
michael@0: let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
michael@0: let aboveEm = MAX_BAR_HEIGHT - belowEm;
michael@0:
michael@0: let barDiv = document.createElement("div");
michael@0: barDiv.className = "bar";
michael@0: barDiv.style.paddingTop = aboveEm + "em";
michael@0:
michael@0: // Add value label or an nbsp if no value
michael@0: barDiv.appendChild(document.createTextNode(value ? value : '\u00A0'));
michael@0:
michael@0: // Create the blue bar
michael@0: let bar = document.createElement("div");
michael@0: bar.className = "bar-inner";
michael@0: bar.style.height = belowEm + "em";
michael@0: barDiv.appendChild(bar);
michael@0:
michael@0: // Add bucket label
michael@0: barDiv.appendChild(document.createTextNode(label));
michael@0:
michael@0: aDiv.appendChild(barDiv);
michael@0: }
michael@0:
michael@0: return text.substr(EOL.length); // Trim the EOL before the first line
michael@0: },
michael@0:
michael@0: /**
michael@0: * Helper function for filtering histogram elements by their id
michael@0: * Adds the "filter-blocked" class to histogram nodes whose IDs don't match the filter.
michael@0: *
michael@0: * @param aContainerNode Container node containing the histogram class nodes to filter
michael@0: * @param aFilterText either text or /RegEx/. If text, case-insensitive and AND words
michael@0: */
michael@0: filterHistograms: function _filterHistograms(aContainerNode, aFilterText) {
michael@0: let filter = aFilterText.toString();
michael@0:
michael@0: // Pass if: all non-empty array items match (case-sensitive)
michael@0: function isPassText(subject, filter) {
michael@0: for (let item of filter) {
michael@0: if (item.length && subject.indexOf(item) < 0) {
michael@0: return false; // mismatch and not a spurious space
michael@0: }
michael@0: }
michael@0: return true;
michael@0: }
michael@0:
michael@0: function isPassRegex(subject, filter) {
michael@0: return filter.test(subject);
michael@0: }
michael@0:
michael@0: // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
michael@0: let isPassFunc; // filter function, set once, then applied to all elements
michael@0: filter = filter.trim();
michael@0: if (filter[0] != "/") { // Plain text: case insensitive, AND if multi-string
michael@0: isPassFunc = isPassText;
michael@0: filter = filter.toLowerCase().split(" ");
michael@0: } else {
michael@0: isPassFunc = isPassRegex;
michael@0: var r = filter.match(/^\/(.*)\/(i?)$/);
michael@0: try {
michael@0: filter = RegExp(r[1], r[2]);
michael@0: }
michael@0: catch (e) { // Incomplete or bad RegExp - always no match
michael@0: isPassFunc = function() {
michael@0: return false;
michael@0: };
michael@0: }
michael@0: }
michael@0:
michael@0: let needLower = (isPassFunc === isPassText);
michael@0:
michael@0: let histograms = aContainerNode.getElementsByClassName("histogram");
michael@0: for (let hist of histograms) {
michael@0: hist.classList[isPassFunc((needLower ? hist.id.toLowerCase() : hist.id), filter) ? "remove" : "add"]("filter-blocked");
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Event handler for change at histograms filter input
michael@0: *
michael@0: * When invoked, 'this' is expected to be the filter HTML node.
michael@0: */
michael@0: histogramFilterChanged: function _histogramFilterChanged() {
michael@0: if (this.idleTimeout) {
michael@0: clearTimeout(this.idleTimeout);
michael@0: }
michael@0:
michael@0: this.idleTimeout = setTimeout( () => {
michael@0: Histogram.filterHistograms(document.getElementById(this.getAttribute("target_id")), this.value);
michael@0: }, FILTER_IDLE_TIMEOUT);
michael@0: }
michael@0: };
michael@0:
michael@0: /*
michael@0: * Helper function to render JS objects with white space between top level elements
michael@0: * so that they look better in the browser
michael@0: * @param aObject JavaScript object or array to render
michael@0: * @return String
michael@0: */
michael@0: function RenderObject(aObject) {
michael@0: let output = "";
michael@0: if (Array.isArray(aObject)) {
michael@0: if (aObject.length == 0) {
michael@0: return "[]";
michael@0: }
michael@0: output = "[" + JSON.stringify(aObject[0]);
michael@0: for (let i = 1; i < aObject.length; i++) {
michael@0: output += ", " + JSON.stringify(aObject[i]);
michael@0: }
michael@0: return output + "]";
michael@0: }
michael@0: let keys = Object.keys(aObject);
michael@0: if (keys.length == 0) {
michael@0: return "{}";
michael@0: }
michael@0: output = "{\"" + keys[0] + "\":\u00A0" + JSON.stringify(aObject[keys[0]]);
michael@0: for (let i = 1; i < keys.length; i++) {
michael@0: output += ", \"" + keys[i] + "\":\u00A0" + JSON.stringify(aObject[keys[i]]);
michael@0: }
michael@0: return output + "}";
michael@0: };
michael@0:
michael@0: let KeyValueTable = {
michael@0: /**
michael@0: * Returns a 2-column table with keys and values
michael@0: * @param aMeasurements Each key in this JS object is rendered as a row in
michael@0: * the table with its corresponding value
michael@0: * @param aKeysLabel Column header for the keys column
michael@0: * @param aValuesLabel Column header for the values column
michael@0: */
michael@0: render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) {
michael@0: let table = document.createElement("table");
michael@0: this.renderHeader(table, aKeysLabel, aValuesLabel);
michael@0: this.renderBody(table, aMeasurements);
michael@0: return table;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Create the table header
michael@0: * Tabs & newlines added to cells to make it easier to copy-paste.
michael@0: *
michael@0: * @param aTable Table element
michael@0: * @param aKeysLabel Column header for the keys column
michael@0: * @param aValuesLabel Column header for the values column
michael@0: */
michael@0: renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) {
michael@0: let headerRow = document.createElement("tr");
michael@0: aTable.appendChild(headerRow);
michael@0:
michael@0: let keysColumn = document.createElement("th");
michael@0: keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t"));
michael@0: let valuesColumn = document.createElement("th");
michael@0: valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n"));
michael@0:
michael@0: headerRow.appendChild(keysColumn);
michael@0: headerRow.appendChild(valuesColumn);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Create the table body
michael@0: * Tabs & newlines added to cells to make it easier to copy-paste.
michael@0: *
michael@0: * @param aTable Table element
michael@0: * @param aMeasurements Key/value map
michael@0: */
michael@0: renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) {
michael@0: for (let [key, value] of Iterator(aMeasurements)) {
michael@0: // use .valueOf() to unbox Number, String, etc. objects
michael@0: if ((typeof value == "object") && (typeof value.valueOf() == "object")) {
michael@0: value = RenderObject(value);
michael@0: }
michael@0:
michael@0: let newRow = document.createElement("tr");
michael@0: aTable.appendChild(newRow);
michael@0:
michael@0: let keyField = document.createElement("td");
michael@0: keyField.appendChild(document.createTextNode(key + "\t"));
michael@0: newRow.appendChild(keyField);
michael@0:
michael@0: let valueField = document.createElement("td");
michael@0: valueField.appendChild(document.createTextNode(value + "\n"));
michael@0: newRow.appendChild(valueField);
michael@0: }
michael@0: }
michael@0: };
michael@0:
michael@0: let AddonDetails = {
michael@0: tableIDTitle: bundle.GetStringFromName("addonTableID"),
michael@0: tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"),
michael@0:
michael@0: /**
michael@0: * Render the addon details section as a series of headers followed by key/value tables
michael@0: * @param aSections Object containing the details sections to render
michael@0: */
michael@0: render: function AddonDetails_render(aSections) {
michael@0: let addonSection = document.getElementById("addon-details");
michael@0: for (let provider in aSections) {
michael@0: let providerSection = document.createElement("h2");
michael@0: let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
michael@0: providerSection.appendChild(document.createTextNode(titleText));
michael@0: addonSection.appendChild(providerSection);
michael@0: addonSection.appendChild(
michael@0: KeyValueTable.render(aSections[provider],
michael@0: this.tableIDTitle, this.tableDetailsTitle));
michael@0: }
michael@0: }
michael@0: };
michael@0:
michael@0: /**
michael@0: * Helper function for showing either the toggle element or "No data collected" message for a section
michael@0: *
michael@0: * @param aSectionID ID of the section element that needs to be changed
michael@0: * @param aHasData true (default) indicates that toggle should be displayed
michael@0: */
michael@0: function setHasData(aSectionID, aHasData) {
michael@0: let sectionElement = document.getElementById(aSectionID);
michael@0: sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
michael@0: }
michael@0:
michael@0: /**
michael@0: * Helper function that expands and collapses sections +
michael@0: * changes caption on the toggle text
michael@0: */
michael@0: function toggleSection(aEvent) {
michael@0: let parentElement = aEvent.target.parentElement;
michael@0: if (!parentElement.classList.contains("has-data")) {
michael@0: return; // nothing to toggle
michael@0: }
michael@0:
michael@0: parentElement.classList.toggle("expanded");
michael@0:
michael@0: // Store section opened/closed state in a hidden checkbox (which is then used on reload)
michael@0: let statebox = parentElement.getElementsByClassName("statebox")[0];
michael@0: statebox.checked = parentElement.classList.contains("expanded");
michael@0: }
michael@0:
michael@0: /**
michael@0: * Sets the text of the page header based on a config pref + bundle strings
michael@0: */
michael@0: function setupPageHeader()
michael@0: {
michael@0: let serverOwner = getPref(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
michael@0: let brandName = brandBundle.GetStringFromName("brandFullName");
michael@0: let subtitleText = bundle.formatStringFromName(
michael@0: "pageSubtitle", [serverOwner, brandName], 2);
michael@0:
michael@0: let subtitleElement = document.getElementById("page-subtitle");
michael@0: subtitleElement.appendChild(document.createTextNode(subtitleText));
michael@0: }
michael@0:
michael@0: /**
michael@0: * Initializes load/unload, pref change and mouse-click listeners
michael@0: */
michael@0: function setupListeners() {
michael@0: Services.prefs.addObserver(PREF_TELEMETRY_ENABLED, observer, false);
michael@0: observer.updatePrefStatus();
michael@0:
michael@0: // Clean up observers when page is closed
michael@0: window.addEventListener("unload",
michael@0: function unloadHandler(aEvent) {
michael@0: window.removeEventListener("unload", unloadHandler);
michael@0: Services.prefs.removeObserver(PREF_TELEMETRY_ENABLED, observer);
michael@0: }, false);
michael@0:
michael@0: document.getElementById("toggle-telemetry").addEventListener("click",
michael@0: function () {
michael@0: let value = getPref(PREF_TELEMETRY_ENABLED, false);
michael@0: Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, !value);
michael@0: }, false);
michael@0:
michael@0: document.getElementById("chrome-hangs-fetch-symbols").addEventListener("click",
michael@0: function () {
michael@0: let hangs = Telemetry.chromeHangs;
michael@0: let req = new SymbolicationRequest("chrome-hangs",
michael@0: ChromeHangs.renderHangHeader,
michael@0: hangs.memoryMap, hangs.stacks);
michael@0: req.fetchSymbols();
michael@0: }, false);
michael@0:
michael@0: document.getElementById("chrome-hangs-hide-symbols").addEventListener("click",
michael@0: function () {
michael@0: ChromeHangs.render();
michael@0: }, false);
michael@0:
michael@0: document.getElementById("late-writes-fetch-symbols").addEventListener("click",
michael@0: function () {
michael@0: let lateWrites = TelemetryPing.getPayload().lateWrites;
michael@0: let req = new SymbolicationRequest("late-writes",
michael@0: LateWritesSingleton.renderHeader,
michael@0: lateWrites.memoryMap,
michael@0: lateWrites.stacks);
michael@0: req.fetchSymbols();
michael@0: }, false);
michael@0:
michael@0: document.getElementById("late-writes-hide-symbols").addEventListener("click",
michael@0: function () {
michael@0: let ping = TelemetryPing.getPayload();
michael@0: LateWritesSingleton.renderLateWrites(ping.lateWrites);
michael@0: }, false);
michael@0:
michael@0:
michael@0: // Clicking on the section name will toggle its state
michael@0: let sectionHeaders = document.getElementsByClassName("section-name");
michael@0: for (let sectionHeader of sectionHeaders) {
michael@0: sectionHeader.addEventListener("click", toggleSection, false);
michael@0: }
michael@0:
michael@0: // Clicking on the "toggle" text will also toggle section's state
michael@0: let toggleLinks = document.getElementsByClassName("toggle-caption");
michael@0: for (let toggleLink of toggleLinks) {
michael@0: toggleLink.addEventListener("click", toggleSection, false);
michael@0: }
michael@0: }
michael@0:
michael@0:
michael@0: function onLoad() {
michael@0: window.removeEventListener("load", onLoad);
michael@0:
michael@0: // Set the text in the page header
michael@0: setupPageHeader();
michael@0:
michael@0: // Set up event listeners
michael@0: setupListeners();
michael@0:
michael@0: // Show slow SQL stats
michael@0: SlowSQL.render();
michael@0:
michael@0: // Show chrome hang stacks
michael@0: ChromeHangs.render();
michael@0:
michael@0: // Show thread hang stats
michael@0: ThreadHangStats.render();
michael@0:
michael@0: // Show histogram data
michael@0: let histograms = Telemetry.histogramSnapshots;
michael@0: if (Object.keys(histograms).length) {
michael@0: let hgramDiv = document.getElementById("histograms");
michael@0: for (let [name, hgram] of Iterator(histograms)) {
michael@0: Histogram.render(hgramDiv, name, hgram);
michael@0: }
michael@0:
michael@0: let filterBox = document.getElementById("histograms-filter");
michael@0: filterBox.addEventListener("input", Histogram.histogramFilterChanged, false);
michael@0: if (filterBox.value.trim() != "") { // on load, no need to filter if empty
michael@0: Histogram.filterHistograms(hgramDiv, filterBox.value);
michael@0: }
michael@0:
michael@0: setHasData("histograms-section", true);
michael@0: }
michael@0:
michael@0: // Show addon histogram data
michael@0: let addonDiv = document.getElementById("addon-histograms");
michael@0: let addonHistogramsRendered = false;
michael@0: let addonData = Telemetry.addonHistogramSnapshots;
michael@0: for (let [addon, histograms] of Iterator(addonData)) {
michael@0: for (let [name, hgram] of Iterator(histograms)) {
michael@0: addonHistogramsRendered = true;
michael@0: Histogram.render(addonDiv, addon + ": " + name, hgram);
michael@0: }
michael@0: }
michael@0:
michael@0: if (addonHistogramsRendered) {
michael@0: setHasData("addon-histograms-section", true);
michael@0: }
michael@0:
michael@0: // Get the Telemetry Ping payload
michael@0: Telemetry.asyncFetchTelemetryData(displayPingData);
michael@0:
michael@0: // Restore sections states
michael@0: let stateboxes = document.getElementsByClassName("statebox");
michael@0: for (let box of stateboxes) {
michael@0: if (box.checked) { // Was open. Will still display as empty if not has-data
michael@0: box.parentElement.classList.add("expanded");
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: let LateWritesSingleton = {
michael@0: renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
michael@0: StackRenderer.renderHeader("late-writes", [aIndex + 1]);
michael@0: },
michael@0:
michael@0: renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
michael@0: let stacks = lateWrites.stacks;
michael@0: let memoryMap = lateWrites.memoryMap;
michael@0: StackRenderer.renderStacks('late-writes', stacks, memoryMap,
michael@0: LateWritesSingleton.renderHeader);
michael@0: }
michael@0: };
michael@0:
michael@0: /**
michael@0: * Helper function for sorting the startup milestones in the Simple Measurements
michael@0: * section into temporal order.
michael@0: *
michael@0: * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
michael@0: * @return Sorted measurements
michael@0: */
michael@0: function sortStartupMilestones(aSimpleMeasurements) {
michael@0: const telemetryTimestamps = TelemetryTimestamps.get();
michael@0: let startupEvents = Services.startup.getStartupInfo();
michael@0: delete startupEvents['process'];
michael@0:
michael@0: function keyIsMilestone(k) {
michael@0: return (k in startupEvents) || (k in telemetryTimestamps);
michael@0: }
michael@0:
michael@0: let sortedKeys = Object.keys(aSimpleMeasurements);
michael@0:
michael@0: // Sort the measurements, with startup milestones at the front + ordered by time
michael@0: sortedKeys.sort(function keyCompare(keyA, keyB) {
michael@0: let isKeyAMilestone = keyIsMilestone(keyA);
michael@0: let isKeyBMilestone = keyIsMilestone(keyB);
michael@0:
michael@0: // First order by startup vs non-startup measurement
michael@0: if (isKeyAMilestone && !isKeyBMilestone)
michael@0: return -1;
michael@0: if (!isKeyAMilestone && isKeyBMilestone)
michael@0: return 1;
michael@0: // Don't change order of non-startup measurements
michael@0: if (!isKeyAMilestone && !isKeyBMilestone)
michael@0: return 0;
michael@0:
michael@0: // If both keys are startup measurements, order them by value
michael@0: return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
michael@0: });
michael@0:
michael@0: // Insert measurements into a result object in sort-order
michael@0: let result = {};
michael@0: for (let key of sortedKeys) {
michael@0: result[key] = aSimpleMeasurements[key];
michael@0: }
michael@0:
michael@0: return result;
michael@0: }
michael@0:
michael@0: function displayPingData() {
michael@0: let ping = TelemetryPing.getPayload();
michael@0:
michael@0: let keysHeader = bundle.GetStringFromName("keysHeader");
michael@0: let valuesHeader = bundle.GetStringFromName("valuesHeader");
michael@0:
michael@0: // Show simple measurements
michael@0: let simpleMeasurements = sortStartupMilestones(ping.simpleMeasurements);
michael@0: if (Object.keys(simpleMeasurements).length) {
michael@0: let simpleSection = document.getElementById("simple-measurements");
michael@0: simpleSection.appendChild(KeyValueTable.render(simpleMeasurements,
michael@0: keysHeader, valuesHeader));
michael@0: setHasData("simple-measurements-section", true);
michael@0: }
michael@0:
michael@0: LateWritesSingleton.renderLateWrites(ping.lateWrites);
michael@0:
michael@0: // Show basic system info gathered
michael@0: if (Object.keys(ping.info).length) {
michael@0: let infoSection = document.getElementById("system-info");
michael@0: infoSection.appendChild(KeyValueTable.render(ping.info,
michael@0: keysHeader, valuesHeader));
michael@0: setHasData("system-info-section", true);
michael@0: }
michael@0:
michael@0: let addonDetails = ping.addonDetails;
michael@0: if (Object.keys(addonDetails).length) {
michael@0: AddonDetails.render(addonDetails);
michael@0: setHasData("addon-details-section", true);
michael@0: }
michael@0: }
michael@0:
michael@0: window.addEventListener("load", onLoad, false);