1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/content/aboutTelemetry.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1071 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +'use strict'; 1.9 + 1.10 +const Ci = Components.interfaces; 1.11 +const Cc = Components.classes; 1.12 +const Cu = Components.utils; 1.13 + 1.14 +Cu.import("resource://gre/modules/Services.jsm"); 1.15 +Cu.import("resource://gre/modules/TelemetryTimestamps.jsm"); 1.16 +Cu.import("resource://gre/modules/TelemetryPing.jsm"); 1.17 + 1.18 +const Telemetry = Services.telemetry; 1.19 +const bundle = Services.strings.createBundle( 1.20 + "chrome://global/locale/aboutTelemetry.properties"); 1.21 +const brandBundle = Services.strings.createBundle( 1.22 + "chrome://branding/locale/brand.properties"); 1.23 + 1.24 +// Maximum height of a histogram bar (in em for html, in chars for text) 1.25 +const MAX_BAR_HEIGHT = 18; 1.26 +const MAX_BAR_CHARS = 25; 1.27 +const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner"; 1.28 +const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; 1.29 +const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql"; 1.30 +const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl"; 1.31 +const DEFAULT_SYMBOL_SERVER_URI = "http://symbolapi.mozilla.org"; 1.32 + 1.33 +// ms idle before applying the filter (allow uninterrupted typing) 1.34 +const FILTER_IDLE_TIMEOUT = 500; 1.35 + 1.36 +#ifdef XP_WIN 1.37 +const EOL = "\r\n"; 1.38 +#else 1.39 +const EOL = "\n"; 1.40 +#endif 1.41 + 1.42 +// Cached value of document's RTL mode 1.43 +let documentRTLMode = ""; 1.44 + 1.45 +/** 1.46 + * Helper function for fetching a config pref 1.47 + * 1.48 + * @param aPrefName Name of config pref to fetch. 1.49 + * @param aDefault Default value to return if pref isn't set. 1.50 + * @return Value of pref 1.51 + */ 1.52 +function getPref(aPrefName, aDefault) { 1.53 + let result = aDefault; 1.54 + 1.55 + try { 1.56 + let prefType = Services.prefs.getPrefType(aPrefName); 1.57 + if (prefType == Ci.nsIPrefBranch.PREF_BOOL) { 1.58 + result = Services.prefs.getBoolPref(aPrefName); 1.59 + } else if (prefType == Ci.nsIPrefBranch.PREF_STRING) { 1.60 + result = Services.prefs.getCharPref(aPrefName); 1.61 + } 1.62 + } catch (e) { 1.63 + // Return default if Prefs service throws exception 1.64 + } 1.65 + 1.66 + return result; 1.67 +} 1.68 + 1.69 +/** 1.70 + * Helper function for determining whether the document direction is RTL. 1.71 + * Caches result of check on first invocation. 1.72 + */ 1.73 +function isRTL() { 1.74 + if (!documentRTLMode) 1.75 + documentRTLMode = window.getComputedStyle(document.body).direction; 1.76 + return (documentRTLMode == "rtl"); 1.77 +} 1.78 + 1.79 +let observer = { 1.80 + 1.81 + enableTelemetry: bundle.GetStringFromName("enableTelemetry"), 1.82 + 1.83 + disableTelemetry: bundle.GetStringFromName("disableTelemetry"), 1.84 + 1.85 + /** 1.86 + * Observer is called whenever Telemetry is enabled or disabled 1.87 + */ 1.88 + observe: function observe(aSubject, aTopic, aData) { 1.89 + if (aData == PREF_TELEMETRY_ENABLED) { 1.90 + this.updatePrefStatus(); 1.91 + } 1.92 + }, 1.93 + 1.94 + /** 1.95 + * Updates the button & text at the top of the page to reflect Telemetry state. 1.96 + */ 1.97 + updatePrefStatus: function updatePrefStatus() { 1.98 + // Notify user whether Telemetry is enabled 1.99 + let enabledElement = document.getElementById("description-enabled"); 1.100 + let disabledElement = document.getElementById("description-disabled"); 1.101 + let toggleElement = document.getElementById("toggle-telemetry"); 1.102 + if (getPref(PREF_TELEMETRY_ENABLED, false)) { 1.103 + enabledElement.classList.remove("hidden"); 1.104 + disabledElement.classList.add("hidden"); 1.105 + toggleElement.innerHTML = this.disableTelemetry; 1.106 + } else { 1.107 + enabledElement.classList.add("hidden"); 1.108 + disabledElement.classList.remove("hidden"); 1.109 + toggleElement.innerHTML = this.enableTelemetry; 1.110 + } 1.111 + } 1.112 +}; 1.113 + 1.114 +let SlowSQL = { 1.115 + 1.116 + slowSqlHits: bundle.GetStringFromName("slowSqlHits"), 1.117 + 1.118 + slowSqlAverage: bundle.GetStringFromName("slowSqlAverage"), 1.119 + 1.120 + slowSqlStatement: bundle.GetStringFromName("slowSqlStatement"), 1.121 + 1.122 + mainThreadTitle: bundle.GetStringFromName("slowSqlMain"), 1.123 + 1.124 + otherThreadTitle: bundle.GetStringFromName("slowSqlOther"), 1.125 + 1.126 + /** 1.127 + * Render slow SQL statistics 1.128 + */ 1.129 + render: function SlowSQL_render() { 1.130 + let debugSlowSql = getPref(PREF_DEBUG_SLOW_SQL, false); 1.131 + let {mainThread, otherThreads} = 1.132 + Telemetry[debugSlowSql ? "debugSlowSQL" : "slowSQL"]; 1.133 + 1.134 + let mainThreadCount = Object.keys(mainThread).length; 1.135 + let otherThreadCount = Object.keys(otherThreads).length; 1.136 + if (mainThreadCount == 0 && otherThreadCount == 0) { 1.137 + return; 1.138 + } 1.139 + 1.140 + setHasData("slow-sql-section", true); 1.141 + 1.142 + if (debugSlowSql) { 1.143 + document.getElementById("sql-warning").classList.remove("hidden"); 1.144 + } 1.145 + 1.146 + let slowSqlDiv = document.getElementById("slow-sql-tables"); 1.147 + 1.148 + // Main thread 1.149 + if (mainThreadCount > 0) { 1.150 + let table = document.createElement("table"); 1.151 + this.renderTableHeader(table, this.mainThreadTitle); 1.152 + this.renderTable(table, mainThread); 1.153 + 1.154 + slowSqlDiv.appendChild(table); 1.155 + slowSqlDiv.appendChild(document.createElement("hr")); 1.156 + } 1.157 + 1.158 + // Other threads 1.159 + if (otherThreadCount > 0) { 1.160 + let table = document.createElement("table"); 1.161 + this.renderTableHeader(table, this.otherThreadTitle); 1.162 + this.renderTable(table, otherThreads); 1.163 + 1.164 + slowSqlDiv.appendChild(table); 1.165 + slowSqlDiv.appendChild(document.createElement("hr")); 1.166 + } 1.167 + }, 1.168 + 1.169 + /** 1.170 + * Creates a header row for a Slow SQL table 1.171 + * Tabs & newlines added to cells to make it easier to copy-paste. 1.172 + * 1.173 + * @param aTable Parent table element 1.174 + * @param aTitle Table's title 1.175 + */ 1.176 + renderTableHeader: function SlowSQL_renderTableHeader(aTable, aTitle) { 1.177 + let caption = document.createElement("caption"); 1.178 + caption.appendChild(document.createTextNode(aTitle + "\n")); 1.179 + aTable.appendChild(caption); 1.180 + 1.181 + let headings = document.createElement("tr"); 1.182 + this.appendColumn(headings, "th", this.slowSqlHits + "\t"); 1.183 + this.appendColumn(headings, "th", this.slowSqlAverage + "\t"); 1.184 + this.appendColumn(headings, "th", this.slowSqlStatement + "\n"); 1.185 + aTable.appendChild(headings); 1.186 + }, 1.187 + 1.188 + /** 1.189 + * Fills out the table body 1.190 + * Tabs & newlines added to cells to make it easier to copy-paste. 1.191 + * 1.192 + * @param aTable Parent table element 1.193 + * @param aSql SQL stats object 1.194 + */ 1.195 + renderTable: function SlowSQL_renderTable(aTable, aSql) { 1.196 + for (let [sql, [hitCount, totalTime]] of Iterator(aSql)) { 1.197 + let averageTime = totalTime / hitCount; 1.198 + 1.199 + let sqlRow = document.createElement("tr"); 1.200 + 1.201 + this.appendColumn(sqlRow, "td", hitCount + "\t"); 1.202 + this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t"); 1.203 + this.appendColumn(sqlRow, "td", sql + "\n"); 1.204 + 1.205 + aTable.appendChild(sqlRow); 1.206 + } 1.207 + }, 1.208 + 1.209 + /** 1.210 + * Helper function for appending a column to a Slow SQL table. 1.211 + * 1.212 + * @param aRowElement Parent row element 1.213 + * @param aColType Column's tag name 1.214 + * @param aColText Column contents 1.215 + */ 1.216 + appendColumn: function SlowSQL_appendColumn(aRowElement, aColType, aColText) { 1.217 + let colElement = document.createElement(aColType); 1.218 + let colTextElement = document.createTextNode(aColText); 1.219 + colElement.appendChild(colTextElement); 1.220 + aRowElement.appendChild(colElement); 1.221 + } 1.222 +}; 1.223 + 1.224 +/** 1.225 + * Removes child elements from the supplied div 1.226 + * 1.227 + * @param aDiv Element to be cleared 1.228 + */ 1.229 +function clearDivData(aDiv) { 1.230 + while (aDiv.hasChildNodes()) { 1.231 + aDiv.removeChild(aDiv.lastChild); 1.232 + } 1.233 +}; 1.234 + 1.235 +let StackRenderer = { 1.236 + 1.237 + stackTitle: bundle.GetStringFromName("stackTitle"), 1.238 + 1.239 + memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"), 1.240 + 1.241 + /** 1.242 + * Outputs the memory map associated with this hang report 1.243 + * 1.244 + * @param aDiv Output div 1.245 + */ 1.246 + renderMemoryMap: function StackRenderer_renderMemoryMap(aDiv, memoryMap) { 1.247 + aDiv.appendChild(document.createTextNode(this.memoryMapTitle)); 1.248 + aDiv.appendChild(document.createElement("br")); 1.249 + 1.250 + for (let currentModule of memoryMap) { 1.251 + aDiv.appendChild(document.createTextNode(currentModule.join(" "))); 1.252 + aDiv.appendChild(document.createElement("br")); 1.253 + } 1.254 + 1.255 + aDiv.appendChild(document.createElement("br")); 1.256 + }, 1.257 + 1.258 + /** 1.259 + * Outputs the raw PCs from the hang's stack 1.260 + * 1.261 + * @param aDiv Output div 1.262 + * @param aStack Array of PCs from the hang stack 1.263 + */ 1.264 + renderStack: function StackRenderer_renderStack(aDiv, aStack) { 1.265 + aDiv.appendChild(document.createTextNode(this.stackTitle)); 1.266 + let stackText = " " + aStack.join(" "); 1.267 + aDiv.appendChild(document.createTextNode(stackText)); 1.268 + 1.269 + aDiv.appendChild(document.createElement("br")); 1.270 + aDiv.appendChild(document.createElement("br")); 1.271 + }, 1.272 + renderStacks: function StackRenderer_renderStacks(aPrefix, aStacks, 1.273 + aMemoryMap, aRenderHeader) { 1.274 + let div = document.getElementById(aPrefix + '-data'); 1.275 + clearDivData(div); 1.276 + 1.277 + let fetchE = document.getElementById(aPrefix + '-fetch-symbols'); 1.278 + if (fetchE) { 1.279 + fetchE.classList.remove("hidden"); 1.280 + } 1.281 + let hideE = document.getElementById(aPrefix + '-hide-symbols'); 1.282 + if (hideE) { 1.283 + hideE.classList.add("hidden"); 1.284 + } 1.285 + 1.286 + if (aStacks.length == 0) { 1.287 + return; 1.288 + } 1.289 + 1.290 + setHasData(aPrefix + '-section', true); 1.291 + 1.292 + this.renderMemoryMap(div, aMemoryMap); 1.293 + 1.294 + for (let i = 0; i < aStacks.length; ++i) { 1.295 + let stack = aStacks[i]; 1.296 + aRenderHeader(i); 1.297 + this.renderStack(div, stack) 1.298 + } 1.299 + }, 1.300 + 1.301 + /** 1.302 + * Renders the title of the stack: e.g. "Late Write #1" or 1.303 + * "Hang Report #1 (6 seconds)". 1.304 + * 1.305 + * @param aFormatArgs formating args to be passed to formatStringFromName. 1.306 + */ 1.307 + renderHeader: function StackRenderer_renderHeader(aPrefix, aFormatArgs) { 1.308 + let div = document.getElementById(aPrefix + "-data"); 1.309 + 1.310 + let titleElement = document.createElement("span"); 1.311 + titleElement.className = "stack-title"; 1.312 + 1.313 + let titleText = bundle.formatStringFromName( 1.314 + aPrefix + "-title", aFormatArgs, aFormatArgs.length); 1.315 + titleElement.appendChild(document.createTextNode(titleText)); 1.316 + 1.317 + div.appendChild(titleElement); 1.318 + div.appendChild(document.createElement("br")); 1.319 + } 1.320 +}; 1.321 + 1.322 +function SymbolicationRequest(aPrefix, aRenderHeader, aMemoryMap, aStacks) { 1.323 + this.prefix = aPrefix; 1.324 + this.renderHeader = aRenderHeader; 1.325 + this.memoryMap = aMemoryMap; 1.326 + this.stacks = aStacks; 1.327 +} 1.328 +/** 1.329 + * A callback for onreadystatechange. It replaces the numeric stack with 1.330 + * the symbolicated one returned by the symbolication server. 1.331 + */ 1.332 +SymbolicationRequest.prototype.handleSymbolResponse = 1.333 +function SymbolicationRequest_handleSymbolResponse() { 1.334 + if (this.symbolRequest.readyState != 4) 1.335 + return; 1.336 + 1.337 + let fetchElement = document.getElementById(this.prefix + "-fetch-symbols"); 1.338 + fetchElement.classList.add("hidden"); 1.339 + let hideElement = document.getElementById(this.prefix + "-hide-symbols"); 1.340 + hideElement.classList.remove("hidden"); 1.341 + let div = document.getElementById(this.prefix + "-data"); 1.342 + clearDivData(div); 1.343 + let errorMessage = bundle.GetStringFromName("errorFetchingSymbols"); 1.344 + 1.345 + if (this.symbolRequest.status != 200) { 1.346 + div.appendChild(document.createTextNode(errorMessage)); 1.347 + return; 1.348 + } 1.349 + 1.350 + let jsonResponse = {}; 1.351 + try { 1.352 + jsonResponse = JSON.parse(this.symbolRequest.responseText); 1.353 + } catch (e) { 1.354 + div.appendChild(document.createTextNode(errorMessage)); 1.355 + return; 1.356 + } 1.357 + 1.358 + for (let i = 0; i < jsonResponse.length; ++i) { 1.359 + let stack = jsonResponse[i]; 1.360 + this.renderHeader(i); 1.361 + 1.362 + for (let symbol of stack) { 1.363 + div.appendChild(document.createTextNode(symbol)); 1.364 + div.appendChild(document.createElement("br")); 1.365 + } 1.366 + div.appendChild(document.createElement("br")); 1.367 + } 1.368 +}; 1.369 +/** 1.370 + * Send a request to the symbolication server to symbolicate this stack. 1.371 + */ 1.372 +SymbolicationRequest.prototype.fetchSymbols = 1.373 +function SymbolicationRequest_fetchSymbols() { 1.374 + let symbolServerURI = 1.375 + getPref(PREF_SYMBOL_SERVER_URI, DEFAULT_SYMBOL_SERVER_URI); 1.376 + let request = {"memoryMap" : this.memoryMap, "stacks" : this.stacks, 1.377 + "version" : 3}; 1.378 + let requestJSON = JSON.stringify(request); 1.379 + 1.380 + this.symbolRequest = new XMLHttpRequest(); 1.381 + this.symbolRequest.open("POST", symbolServerURI, true); 1.382 + this.symbolRequest.setRequestHeader("Content-type", "application/json"); 1.383 + this.symbolRequest.setRequestHeader("Content-length", 1.384 + requestJSON.length); 1.385 + this.symbolRequest.setRequestHeader("Connection", "close"); 1.386 + this.symbolRequest.onreadystatechange = this.handleSymbolResponse.bind(this); 1.387 + this.symbolRequest.send(requestJSON); 1.388 +} 1.389 + 1.390 +let ChromeHangs = { 1.391 + 1.392 + symbolRequest: null, 1.393 + 1.394 + /** 1.395 + * Renders raw chrome hang data 1.396 + */ 1.397 + render: function ChromeHangs_render() { 1.398 + let hangs = Telemetry.chromeHangs; 1.399 + let stacks = hangs.stacks; 1.400 + let memoryMap = hangs.memoryMap; 1.401 + 1.402 + StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap, 1.403 + this.renderHangHeader); 1.404 + }, 1.405 + 1.406 + renderHangHeader: function ChromeHangs_renderHangHeader(aIndex) { 1.407 + let durations = Telemetry.chromeHangs.durations; 1.408 + StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, durations[aIndex]]); 1.409 + } 1.410 +}; 1.411 + 1.412 +let ThreadHangStats = { 1.413 + 1.414 + /** 1.415 + * Renders raw thread hang stats data 1.416 + */ 1.417 + render: function() { 1.418 + let div = document.getElementById("thread-hang-stats"); 1.419 + clearDivData(div); 1.420 + 1.421 + let stats = Telemetry.threadHangStats; 1.422 + stats.forEach((thread) => { 1.423 + div.appendChild(this.renderThread(thread)); 1.424 + }); 1.425 + if (stats.length) { 1.426 + setHasData("thread-hang-stats-section", true); 1.427 + } 1.428 + }, 1.429 + 1.430 + /** 1.431 + * Creates and fills data corresponding to a thread 1.432 + */ 1.433 + renderThread: function(aThread) { 1.434 + let div = document.createElement("div"); 1.435 + 1.436 + let title = document.createElement("h2"); 1.437 + title.textContent = aThread.name; 1.438 + div.appendChild(title); 1.439 + 1.440 + // Don't localize the histogram name, because the 1.441 + // name is also used as the div element's ID 1.442 + Histogram.render(div, aThread.name + "-Activity", 1.443 + aThread.activity, {exponential: true}); 1.444 + aThread.hangs.forEach((hang, index) => { 1.445 + let hangName = aThread.name + "-Hang-" + (index + 1); 1.446 + let hangDiv = Histogram.render( 1.447 + div, hangName, hang.histogram, {exponential: true}); 1.448 + let stackDiv = document.createElement("div"); 1.449 + hang.stack.forEach((frame) => { 1.450 + stackDiv.appendChild(document.createTextNode(frame)); 1.451 + // Leave an extra <br> at the end of the stack listing 1.452 + stackDiv.appendChild(document.createElement("br")); 1.453 + }); 1.454 + // Insert stack after the histogram title 1.455 + hangDiv.insertBefore(stackDiv, hangDiv.childNodes[1]); 1.456 + }); 1.457 + return div; 1.458 + }, 1.459 +}; 1.460 + 1.461 +let Histogram = { 1.462 + 1.463 + hgramSamplesCaption: bundle.GetStringFromName("histogramSamples"), 1.464 + 1.465 + hgramAverageCaption: bundle.GetStringFromName("histogramAverage"), 1.466 + 1.467 + hgramSumCaption: bundle.GetStringFromName("histogramSum"), 1.468 + 1.469 + hgramCopyCaption: bundle.GetStringFromName("histogramCopy"), 1.470 + 1.471 + /** 1.472 + * Renders a single Telemetry histogram 1.473 + * 1.474 + * @param aParent Parent element 1.475 + * @param aName Histogram name 1.476 + * @param aHgram Histogram information 1.477 + * @param aOptions Object with render options 1.478 + * * exponential: bars follow logarithmic scale 1.479 + */ 1.480 + render: function Histogram_render(aParent, aName, aHgram, aOptions) { 1.481 + let hgram = this.unpack(aHgram); 1.482 + let options = aOptions || {}; 1.483 + 1.484 + let outerDiv = document.createElement("div"); 1.485 + outerDiv.className = "histogram"; 1.486 + outerDiv.id = aName; 1.487 + 1.488 + let divTitle = document.createElement("div"); 1.489 + divTitle.className = "histogram-title"; 1.490 + divTitle.appendChild(document.createTextNode(aName)); 1.491 + outerDiv.appendChild(divTitle); 1.492 + 1.493 + let stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " + 1.494 + this.hgramAverageCaption + " = " + hgram.pretty_average + ", " + 1.495 + this.hgramSumCaption + " = " + hgram.sum; 1.496 + 1.497 + let divStats = document.createElement("div"); 1.498 + divStats.appendChild(document.createTextNode(stats)); 1.499 + outerDiv.appendChild(divStats); 1.500 + 1.501 + if (isRTL()) 1.502 + hgram.values.reverse(); 1.503 + 1.504 + let textData = this.renderValues(outerDiv, hgram.values, hgram.max, 1.505 + hgram.sample_count, options); 1.506 + 1.507 + // The 'Copy' button contains the textual data, copied to clipboard on click 1.508 + let copyButton = document.createElement("button"); 1.509 + copyButton.className = "copy-node"; 1.510 + copyButton.appendChild(document.createTextNode(this.hgramCopyCaption)); 1.511 + copyButton.histogramText = aName + EOL + stats + EOL + EOL + textData; 1.512 + copyButton.addEventListener("click", function(){ 1.513 + Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper) 1.514 + .copyString(this.histogramText); 1.515 + }); 1.516 + outerDiv.appendChild(copyButton); 1.517 + 1.518 + aParent.appendChild(outerDiv); 1.519 + return outerDiv; 1.520 + }, 1.521 + 1.522 + /** 1.523 + * Unpacks histogram values 1.524 + * 1.525 + * @param aHgram Packed histogram 1.526 + * 1.527 + * @return Unpacked histogram representation 1.528 + */ 1.529 + unpack: function Histogram_unpack(aHgram) { 1.530 + let sample_count = aHgram.counts.reduceRight(function (a, b) a + b); 1.531 + let buckets = [0, 1]; 1.532 + if (aHgram.histogram_type != Telemetry.HISTOGRAM_BOOLEAN) { 1.533 + buckets = aHgram.ranges; 1.534 + } 1.535 + 1.536 + let average = Math.round(aHgram.sum * 10 / sample_count) / 10; 1.537 + let max_value = Math.max.apply(Math, aHgram.counts); 1.538 + 1.539 + let first = true; 1.540 + let last = 0; 1.541 + let values = []; 1.542 + for (let i = 0; i < buckets.length; i++) { 1.543 + let count = aHgram.counts[i]; 1.544 + if (!count) 1.545 + continue; 1.546 + if (first) { 1.547 + first = false; 1.548 + if (i) { 1.549 + values.push([buckets[i - 1], 0]); 1.550 + } 1.551 + } 1.552 + last = i + 1; 1.553 + values.push([buckets[i], count]); 1.554 + } 1.555 + if (last && last < buckets.length) { 1.556 + values.push([buckets[last], 0]); 1.557 + } 1.558 + 1.559 + let result = { 1.560 + values: values, 1.561 + pretty_average: average, 1.562 + max: max_value, 1.563 + sample_count: sample_count, 1.564 + sum: aHgram.sum 1.565 + }; 1.566 + 1.567 + return result; 1.568 + }, 1.569 + 1.570 + /** 1.571 + * Return a non-negative, logarithmic representation of a non-negative number. 1.572 + * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3 1.573 + * 1.574 + * @param aNumber Non-negative number 1.575 + */ 1.576 + getLogValue: function(aNumber) { 1.577 + return Math.max(0, Math.log10(aNumber) + 1); 1.578 + }, 1.579 + 1.580 + /** 1.581 + * Create histogram HTML bars, also returns a textual representation 1.582 + * Both aMaxValue and aSumValues must be positive. 1.583 + * Values are assumed to use 0 as baseline. 1.584 + * 1.585 + * @param aDiv Outer parent div 1.586 + * @param aValues Histogram values 1.587 + * @param aMaxValue Value of the longest bar (length, not label) 1.588 + * @param aSumValues Sum of all bar values 1.589 + * @param aOptions Object with render options (@see #render) 1.590 + */ 1.591 + renderValues: function Histogram_renderValues(aDiv, aValues, aMaxValue, aSumValues, aOptions) { 1.592 + let text = ""; 1.593 + // If the last label is not the longest string, alignment will break a little 1.594 + let labelPadTo = String(aValues[aValues.length -1][0]).length; 1.595 + let maxBarValue = aOptions.exponential ? this.getLogValue(aMaxValue) : aMaxValue; 1.596 + 1.597 + for (let [label, value] of aValues) { 1.598 + let barValue = aOptions.exponential ? this.getLogValue(value) : value; 1.599 + 1.600 + // Create a text representation: <right-aligned-label> |<bar-of-#><value> <percentage> 1.601 + text += EOL 1.602 + + " ".repeat(Math.max(0, labelPadTo - String(label).length)) + label // Right-aligned label 1.603 + + " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar 1.604 + + " " + value // Value 1.605 + + " " + Math.round(100 * value / aSumValues) + "%"; // Percentage 1.606 + 1.607 + // Construct the HTML labels + bars 1.608 + let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10; 1.609 + let aboveEm = MAX_BAR_HEIGHT - belowEm; 1.610 + 1.611 + let barDiv = document.createElement("div"); 1.612 + barDiv.className = "bar"; 1.613 + barDiv.style.paddingTop = aboveEm + "em"; 1.614 + 1.615 + // Add value label or an nbsp if no value 1.616 + barDiv.appendChild(document.createTextNode(value ? value : '\u00A0')); 1.617 + 1.618 + // Create the blue bar 1.619 + let bar = document.createElement("div"); 1.620 + bar.className = "bar-inner"; 1.621 + bar.style.height = belowEm + "em"; 1.622 + barDiv.appendChild(bar); 1.623 + 1.624 + // Add bucket label 1.625 + barDiv.appendChild(document.createTextNode(label)); 1.626 + 1.627 + aDiv.appendChild(barDiv); 1.628 + } 1.629 + 1.630 + return text.substr(EOL.length); // Trim the EOL before the first line 1.631 + }, 1.632 + 1.633 + /** 1.634 + * Helper function for filtering histogram elements by their id 1.635 + * Adds the "filter-blocked" class to histogram nodes whose IDs don't match the filter. 1.636 + * 1.637 + * @param aContainerNode Container node containing the histogram class nodes to filter 1.638 + * @param aFilterText either text or /RegEx/. If text, case-insensitive and AND words 1.639 + */ 1.640 + filterHistograms: function _filterHistograms(aContainerNode, aFilterText) { 1.641 + let filter = aFilterText.toString(); 1.642 + 1.643 + // Pass if: all non-empty array items match (case-sensitive) 1.644 + function isPassText(subject, filter) { 1.645 + for (let item of filter) { 1.646 + if (item.length && subject.indexOf(item) < 0) { 1.647 + return false; // mismatch and not a spurious space 1.648 + } 1.649 + } 1.650 + return true; 1.651 + } 1.652 + 1.653 + function isPassRegex(subject, filter) { 1.654 + return filter.test(subject); 1.655 + } 1.656 + 1.657 + // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx) 1.658 + let isPassFunc; // filter function, set once, then applied to all elements 1.659 + filter = filter.trim(); 1.660 + if (filter[0] != "/") { // Plain text: case insensitive, AND if multi-string 1.661 + isPassFunc = isPassText; 1.662 + filter = filter.toLowerCase().split(" "); 1.663 + } else { 1.664 + isPassFunc = isPassRegex; 1.665 + var r = filter.match(/^\/(.*)\/(i?)$/); 1.666 + try { 1.667 + filter = RegExp(r[1], r[2]); 1.668 + } 1.669 + catch (e) { // Incomplete or bad RegExp - always no match 1.670 + isPassFunc = function() { 1.671 + return false; 1.672 + }; 1.673 + } 1.674 + } 1.675 + 1.676 + let needLower = (isPassFunc === isPassText); 1.677 + 1.678 + let histograms = aContainerNode.getElementsByClassName("histogram"); 1.679 + for (let hist of histograms) { 1.680 + hist.classList[isPassFunc((needLower ? hist.id.toLowerCase() : hist.id), filter) ? "remove" : "add"]("filter-blocked"); 1.681 + } 1.682 + }, 1.683 + 1.684 + /** 1.685 + * Event handler for change at histograms filter input 1.686 + * 1.687 + * When invoked, 'this' is expected to be the filter HTML node. 1.688 + */ 1.689 + histogramFilterChanged: function _histogramFilterChanged() { 1.690 + if (this.idleTimeout) { 1.691 + clearTimeout(this.idleTimeout); 1.692 + } 1.693 + 1.694 + this.idleTimeout = setTimeout( () => { 1.695 + Histogram.filterHistograms(document.getElementById(this.getAttribute("target_id")), this.value); 1.696 + }, FILTER_IDLE_TIMEOUT); 1.697 + } 1.698 +}; 1.699 + 1.700 +/* 1.701 + * Helper function to render JS objects with white space between top level elements 1.702 + * so that they look better in the browser 1.703 + * @param aObject JavaScript object or array to render 1.704 + * @return String 1.705 + */ 1.706 +function RenderObject(aObject) { 1.707 + let output = ""; 1.708 + if (Array.isArray(aObject)) { 1.709 + if (aObject.length == 0) { 1.710 + return "[]"; 1.711 + } 1.712 + output = "[" + JSON.stringify(aObject[0]); 1.713 + for (let i = 1; i < aObject.length; i++) { 1.714 + output += ", " + JSON.stringify(aObject[i]); 1.715 + } 1.716 + return output + "]"; 1.717 + } 1.718 + let keys = Object.keys(aObject); 1.719 + if (keys.length == 0) { 1.720 + return "{}"; 1.721 + } 1.722 + output = "{\"" + keys[0] + "\":\u00A0" + JSON.stringify(aObject[keys[0]]); 1.723 + for (let i = 1; i < keys.length; i++) { 1.724 + output += ", \"" + keys[i] + "\":\u00A0" + JSON.stringify(aObject[keys[i]]); 1.725 + } 1.726 + return output + "}"; 1.727 +}; 1.728 + 1.729 +let KeyValueTable = { 1.730 + /** 1.731 + * Returns a 2-column table with keys and values 1.732 + * @param aMeasurements Each key in this JS object is rendered as a row in 1.733 + * the table with its corresponding value 1.734 + * @param aKeysLabel Column header for the keys column 1.735 + * @param aValuesLabel Column header for the values column 1.736 + */ 1.737 + render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) { 1.738 + let table = document.createElement("table"); 1.739 + this.renderHeader(table, aKeysLabel, aValuesLabel); 1.740 + this.renderBody(table, aMeasurements); 1.741 + return table; 1.742 + }, 1.743 + 1.744 + /** 1.745 + * Create the table header 1.746 + * Tabs & newlines added to cells to make it easier to copy-paste. 1.747 + * 1.748 + * @param aTable Table element 1.749 + * @param aKeysLabel Column header for the keys column 1.750 + * @param aValuesLabel Column header for the values column 1.751 + */ 1.752 + renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) { 1.753 + let headerRow = document.createElement("tr"); 1.754 + aTable.appendChild(headerRow); 1.755 + 1.756 + let keysColumn = document.createElement("th"); 1.757 + keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t")); 1.758 + let valuesColumn = document.createElement("th"); 1.759 + valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n")); 1.760 + 1.761 + headerRow.appendChild(keysColumn); 1.762 + headerRow.appendChild(valuesColumn); 1.763 + }, 1.764 + 1.765 + /** 1.766 + * Create the table body 1.767 + * Tabs & newlines added to cells to make it easier to copy-paste. 1.768 + * 1.769 + * @param aTable Table element 1.770 + * @param aMeasurements Key/value map 1.771 + */ 1.772 + renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) { 1.773 + for (let [key, value] of Iterator(aMeasurements)) { 1.774 + // use .valueOf() to unbox Number, String, etc. objects 1.775 + if ((typeof value == "object") && (typeof value.valueOf() == "object")) { 1.776 + value = RenderObject(value); 1.777 + } 1.778 + 1.779 + let newRow = document.createElement("tr"); 1.780 + aTable.appendChild(newRow); 1.781 + 1.782 + let keyField = document.createElement("td"); 1.783 + keyField.appendChild(document.createTextNode(key + "\t")); 1.784 + newRow.appendChild(keyField); 1.785 + 1.786 + let valueField = document.createElement("td"); 1.787 + valueField.appendChild(document.createTextNode(value + "\n")); 1.788 + newRow.appendChild(valueField); 1.789 + } 1.790 + } 1.791 +}; 1.792 + 1.793 +let AddonDetails = { 1.794 + tableIDTitle: bundle.GetStringFromName("addonTableID"), 1.795 + tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"), 1.796 + 1.797 + /** 1.798 + * Render the addon details section as a series of headers followed by key/value tables 1.799 + * @param aSections Object containing the details sections to render 1.800 + */ 1.801 + render: function AddonDetails_render(aSections) { 1.802 + let addonSection = document.getElementById("addon-details"); 1.803 + for (let provider in aSections) { 1.804 + let providerSection = document.createElement("h2"); 1.805 + let titleText = bundle.formatStringFromName("addonProvider", [provider], 1); 1.806 + providerSection.appendChild(document.createTextNode(titleText)); 1.807 + addonSection.appendChild(providerSection); 1.808 + addonSection.appendChild( 1.809 + KeyValueTable.render(aSections[provider], 1.810 + this.tableIDTitle, this.tableDetailsTitle)); 1.811 + } 1.812 + } 1.813 +}; 1.814 + 1.815 +/** 1.816 + * Helper function for showing either the toggle element or "No data collected" message for a section 1.817 + * 1.818 + * @param aSectionID ID of the section element that needs to be changed 1.819 + * @param aHasData true (default) indicates that toggle should be displayed 1.820 + */ 1.821 +function setHasData(aSectionID, aHasData) { 1.822 + let sectionElement = document.getElementById(aSectionID); 1.823 + sectionElement.classList[aHasData ? "add" : "remove"]("has-data"); 1.824 +} 1.825 + 1.826 +/** 1.827 + * Helper function that expands and collapses sections + 1.828 + * changes caption on the toggle text 1.829 + */ 1.830 +function toggleSection(aEvent) { 1.831 + let parentElement = aEvent.target.parentElement; 1.832 + if (!parentElement.classList.contains("has-data")) { 1.833 + return; // nothing to toggle 1.834 + } 1.835 + 1.836 + parentElement.classList.toggle("expanded"); 1.837 + 1.838 + // Store section opened/closed state in a hidden checkbox (which is then used on reload) 1.839 + let statebox = parentElement.getElementsByClassName("statebox")[0]; 1.840 + statebox.checked = parentElement.classList.contains("expanded"); 1.841 +} 1.842 + 1.843 +/** 1.844 + * Sets the text of the page header based on a config pref + bundle strings 1.845 + */ 1.846 +function setupPageHeader() 1.847 +{ 1.848 + let serverOwner = getPref(PREF_TELEMETRY_SERVER_OWNER, "Mozilla"); 1.849 + let brandName = brandBundle.GetStringFromName("brandFullName"); 1.850 + let subtitleText = bundle.formatStringFromName( 1.851 + "pageSubtitle", [serverOwner, brandName], 2); 1.852 + 1.853 + let subtitleElement = document.getElementById("page-subtitle"); 1.854 + subtitleElement.appendChild(document.createTextNode(subtitleText)); 1.855 +} 1.856 + 1.857 +/** 1.858 + * Initializes load/unload, pref change and mouse-click listeners 1.859 + */ 1.860 +function setupListeners() { 1.861 + Services.prefs.addObserver(PREF_TELEMETRY_ENABLED, observer, false); 1.862 + observer.updatePrefStatus(); 1.863 + 1.864 + // Clean up observers when page is closed 1.865 + window.addEventListener("unload", 1.866 + function unloadHandler(aEvent) { 1.867 + window.removeEventListener("unload", unloadHandler); 1.868 + Services.prefs.removeObserver(PREF_TELEMETRY_ENABLED, observer); 1.869 + }, false); 1.870 + 1.871 + document.getElementById("toggle-telemetry").addEventListener("click", 1.872 + function () { 1.873 + let value = getPref(PREF_TELEMETRY_ENABLED, false); 1.874 + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, !value); 1.875 + }, false); 1.876 + 1.877 + document.getElementById("chrome-hangs-fetch-symbols").addEventListener("click", 1.878 + function () { 1.879 + let hangs = Telemetry.chromeHangs; 1.880 + let req = new SymbolicationRequest("chrome-hangs", 1.881 + ChromeHangs.renderHangHeader, 1.882 + hangs.memoryMap, hangs.stacks); 1.883 + req.fetchSymbols(); 1.884 + }, false); 1.885 + 1.886 + document.getElementById("chrome-hangs-hide-symbols").addEventListener("click", 1.887 + function () { 1.888 + ChromeHangs.render(); 1.889 + }, false); 1.890 + 1.891 + document.getElementById("late-writes-fetch-symbols").addEventListener("click", 1.892 + function () { 1.893 + let lateWrites = TelemetryPing.getPayload().lateWrites; 1.894 + let req = new SymbolicationRequest("late-writes", 1.895 + LateWritesSingleton.renderHeader, 1.896 + lateWrites.memoryMap, 1.897 + lateWrites.stacks); 1.898 + req.fetchSymbols(); 1.899 + }, false); 1.900 + 1.901 + document.getElementById("late-writes-hide-symbols").addEventListener("click", 1.902 + function () { 1.903 + let ping = TelemetryPing.getPayload(); 1.904 + LateWritesSingleton.renderLateWrites(ping.lateWrites); 1.905 + }, false); 1.906 + 1.907 + 1.908 + // Clicking on the section name will toggle its state 1.909 + let sectionHeaders = document.getElementsByClassName("section-name"); 1.910 + for (let sectionHeader of sectionHeaders) { 1.911 + sectionHeader.addEventListener("click", toggleSection, false); 1.912 + } 1.913 + 1.914 + // Clicking on the "toggle" text will also toggle section's state 1.915 + let toggleLinks = document.getElementsByClassName("toggle-caption"); 1.916 + for (let toggleLink of toggleLinks) { 1.917 + toggleLink.addEventListener("click", toggleSection, false); 1.918 + } 1.919 +} 1.920 + 1.921 + 1.922 +function onLoad() { 1.923 + window.removeEventListener("load", onLoad); 1.924 + 1.925 + // Set the text in the page header 1.926 + setupPageHeader(); 1.927 + 1.928 + // Set up event listeners 1.929 + setupListeners(); 1.930 + 1.931 + // Show slow SQL stats 1.932 + SlowSQL.render(); 1.933 + 1.934 + // Show chrome hang stacks 1.935 + ChromeHangs.render(); 1.936 + 1.937 + // Show thread hang stats 1.938 + ThreadHangStats.render(); 1.939 + 1.940 + // Show histogram data 1.941 + let histograms = Telemetry.histogramSnapshots; 1.942 + if (Object.keys(histograms).length) { 1.943 + let hgramDiv = document.getElementById("histograms"); 1.944 + for (let [name, hgram] of Iterator(histograms)) { 1.945 + Histogram.render(hgramDiv, name, hgram); 1.946 + } 1.947 + 1.948 + let filterBox = document.getElementById("histograms-filter"); 1.949 + filterBox.addEventListener("input", Histogram.histogramFilterChanged, false); 1.950 + if (filterBox.value.trim() != "") { // on load, no need to filter if empty 1.951 + Histogram.filterHistograms(hgramDiv, filterBox.value); 1.952 + } 1.953 + 1.954 + setHasData("histograms-section", true); 1.955 + } 1.956 + 1.957 + // Show addon histogram data 1.958 + let addonDiv = document.getElementById("addon-histograms"); 1.959 + let addonHistogramsRendered = false; 1.960 + let addonData = Telemetry.addonHistogramSnapshots; 1.961 + for (let [addon, histograms] of Iterator(addonData)) { 1.962 + for (let [name, hgram] of Iterator(histograms)) { 1.963 + addonHistogramsRendered = true; 1.964 + Histogram.render(addonDiv, addon + ": " + name, hgram); 1.965 + } 1.966 + } 1.967 + 1.968 + if (addonHistogramsRendered) { 1.969 + setHasData("addon-histograms-section", true); 1.970 + } 1.971 + 1.972 + // Get the Telemetry Ping payload 1.973 + Telemetry.asyncFetchTelemetryData(displayPingData); 1.974 + 1.975 + // Restore sections states 1.976 + let stateboxes = document.getElementsByClassName("statebox"); 1.977 + for (let box of stateboxes) { 1.978 + if (box.checked) { // Was open. Will still display as empty if not has-data 1.979 + box.parentElement.classList.add("expanded"); 1.980 + } 1.981 + } 1.982 +} 1.983 + 1.984 +let LateWritesSingleton = { 1.985 + renderHeader: function LateWritesSingleton_renderHeader(aIndex) { 1.986 + StackRenderer.renderHeader("late-writes", [aIndex + 1]); 1.987 + }, 1.988 + 1.989 + renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) { 1.990 + let stacks = lateWrites.stacks; 1.991 + let memoryMap = lateWrites.memoryMap; 1.992 + StackRenderer.renderStacks('late-writes', stacks, memoryMap, 1.993 + LateWritesSingleton.renderHeader); 1.994 + } 1.995 +}; 1.996 + 1.997 +/** 1.998 + * Helper function for sorting the startup milestones in the Simple Measurements 1.999 + * section into temporal order. 1.1000 + * 1.1001 + * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data 1.1002 + * @return Sorted measurements 1.1003 + */ 1.1004 +function sortStartupMilestones(aSimpleMeasurements) { 1.1005 + const telemetryTimestamps = TelemetryTimestamps.get(); 1.1006 + let startupEvents = Services.startup.getStartupInfo(); 1.1007 + delete startupEvents['process']; 1.1008 + 1.1009 + function keyIsMilestone(k) { 1.1010 + return (k in startupEvents) || (k in telemetryTimestamps); 1.1011 + } 1.1012 + 1.1013 + let sortedKeys = Object.keys(aSimpleMeasurements); 1.1014 + 1.1015 + // Sort the measurements, with startup milestones at the front + ordered by time 1.1016 + sortedKeys.sort(function keyCompare(keyA, keyB) { 1.1017 + let isKeyAMilestone = keyIsMilestone(keyA); 1.1018 + let isKeyBMilestone = keyIsMilestone(keyB); 1.1019 + 1.1020 + // First order by startup vs non-startup measurement 1.1021 + if (isKeyAMilestone && !isKeyBMilestone) 1.1022 + return -1; 1.1023 + if (!isKeyAMilestone && isKeyBMilestone) 1.1024 + return 1; 1.1025 + // Don't change order of non-startup measurements 1.1026 + if (!isKeyAMilestone && !isKeyBMilestone) 1.1027 + return 0; 1.1028 + 1.1029 + // If both keys are startup measurements, order them by value 1.1030 + return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB]; 1.1031 + }); 1.1032 + 1.1033 + // Insert measurements into a result object in sort-order 1.1034 + let result = {}; 1.1035 + for (let key of sortedKeys) { 1.1036 + result[key] = aSimpleMeasurements[key]; 1.1037 + } 1.1038 + 1.1039 + return result; 1.1040 +} 1.1041 + 1.1042 +function displayPingData() { 1.1043 + let ping = TelemetryPing.getPayload(); 1.1044 + 1.1045 + let keysHeader = bundle.GetStringFromName("keysHeader"); 1.1046 + let valuesHeader = bundle.GetStringFromName("valuesHeader"); 1.1047 + 1.1048 + // Show simple measurements 1.1049 + let simpleMeasurements = sortStartupMilestones(ping.simpleMeasurements); 1.1050 + if (Object.keys(simpleMeasurements).length) { 1.1051 + let simpleSection = document.getElementById("simple-measurements"); 1.1052 + simpleSection.appendChild(KeyValueTable.render(simpleMeasurements, 1.1053 + keysHeader, valuesHeader)); 1.1054 + setHasData("simple-measurements-section", true); 1.1055 + } 1.1056 + 1.1057 + LateWritesSingleton.renderLateWrites(ping.lateWrites); 1.1058 + 1.1059 + // Show basic system info gathered 1.1060 + if (Object.keys(ping.info).length) { 1.1061 + let infoSection = document.getElementById("system-info"); 1.1062 + infoSection.appendChild(KeyValueTable.render(ping.info, 1.1063 + keysHeader, valuesHeader)); 1.1064 + setHasData("system-info-section", true); 1.1065 + } 1.1066 + 1.1067 + let addonDetails = ping.addonDetails; 1.1068 + if (Object.keys(addonDetails).length) { 1.1069 + AddonDetails.render(addonDetails); 1.1070 + setHasData("addon-details-section", true); 1.1071 + } 1.1072 +} 1.1073 + 1.1074 +window.addEventListener("load", onLoad, false);