toolkit/content/aboutTelemetry.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 'use strict';
     7 const Ci = Components.interfaces;
     8 const Cc = Components.classes;
     9 const Cu = Components.utils;
    11 Cu.import("resource://gre/modules/Services.jsm");
    12 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm");
    13 Cu.import("resource://gre/modules/TelemetryPing.jsm");
    15 const Telemetry = Services.telemetry;
    16 const bundle = Services.strings.createBundle(
    17   "chrome://global/locale/aboutTelemetry.properties");
    18 const brandBundle = Services.strings.createBundle(
    19   "chrome://branding/locale/brand.properties");
    21 // Maximum height of a histogram bar (in em for html, in chars for text)
    22 const MAX_BAR_HEIGHT = 18;
    23 const MAX_BAR_CHARS = 25;
    24 const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner";
    25 const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
    26 const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql";
    27 const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl";
    28 const DEFAULT_SYMBOL_SERVER_URI = "http://symbolapi.mozilla.org";
    30 // ms idle before applying the filter (allow uninterrupted typing)
    31 const FILTER_IDLE_TIMEOUT = 500;
    33 #ifdef XP_WIN
    34 const EOL = "\r\n";
    35 #else
    36 const EOL = "\n";
    37 #endif
    39 // Cached value of document's RTL mode
    40 let documentRTLMode = "";
    42 /**
    43  * Helper function for fetching a config pref
    44  *
    45  * @param aPrefName Name of config pref to fetch.
    46  * @param aDefault Default value to return if pref isn't set.
    47  * @return Value of pref
    48  */
    49 function getPref(aPrefName, aDefault) {
    50   let result = aDefault;
    52   try {
    53     let prefType = Services.prefs.getPrefType(aPrefName);
    54     if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
    55       result = Services.prefs.getBoolPref(aPrefName);
    56     } else if (prefType == Ci.nsIPrefBranch.PREF_STRING) {
    57       result = Services.prefs.getCharPref(aPrefName);
    58     }
    59   } catch (e) {
    60     // Return default if Prefs service throws exception
    61   }
    63   return result;
    64 }
    66 /**
    67  * Helper function for determining whether the document direction is RTL.
    68  * Caches result of check on first invocation.
    69  */
    70 function isRTL() {
    71   if (!documentRTLMode)
    72     documentRTLMode = window.getComputedStyle(document.body).direction;
    73   return (documentRTLMode == "rtl");
    74 }
    76 let observer = {
    78   enableTelemetry: bundle.GetStringFromName("enableTelemetry"),
    80   disableTelemetry: bundle.GetStringFromName("disableTelemetry"),
    82   /**
    83    * Observer is called whenever Telemetry is enabled or disabled
    84    */
    85   observe: function observe(aSubject, aTopic, aData) {
    86     if (aData == PREF_TELEMETRY_ENABLED) {
    87       this.updatePrefStatus();
    88     }
    89   },
    91   /**
    92    * Updates the button & text at the top of the page to reflect Telemetry state.
    93    */
    94   updatePrefStatus: function updatePrefStatus() {
    95     // Notify user whether Telemetry is enabled
    96     let enabledElement = document.getElementById("description-enabled");
    97     let disabledElement = document.getElementById("description-disabled");
    98     let toggleElement = document.getElementById("toggle-telemetry");
    99     if (getPref(PREF_TELEMETRY_ENABLED, false)) {
   100       enabledElement.classList.remove("hidden");
   101       disabledElement.classList.add("hidden");
   102       toggleElement.innerHTML = this.disableTelemetry;
   103     } else {
   104       enabledElement.classList.add("hidden");
   105       disabledElement.classList.remove("hidden");
   106       toggleElement.innerHTML = this.enableTelemetry;
   107     }
   108   }
   109 };
   111 let SlowSQL = {
   113   slowSqlHits: bundle.GetStringFromName("slowSqlHits"),
   115   slowSqlAverage: bundle.GetStringFromName("slowSqlAverage"),
   117   slowSqlStatement: bundle.GetStringFromName("slowSqlStatement"),
   119   mainThreadTitle: bundle.GetStringFromName("slowSqlMain"),
   121   otherThreadTitle: bundle.GetStringFromName("slowSqlOther"),
   123   /**
   124    * Render slow SQL statistics
   125    */
   126   render: function SlowSQL_render() {
   127     let debugSlowSql = getPref(PREF_DEBUG_SLOW_SQL, false);
   128     let {mainThread, otherThreads} =
   129       Telemetry[debugSlowSql ? "debugSlowSQL" : "slowSQL"];
   131     let mainThreadCount = Object.keys(mainThread).length;
   132     let otherThreadCount = Object.keys(otherThreads).length;
   133     if (mainThreadCount == 0 && otherThreadCount == 0) {
   134       return;
   135     }
   137     setHasData("slow-sql-section", true);
   139     if (debugSlowSql) {
   140       document.getElementById("sql-warning").classList.remove("hidden");
   141     }
   143     let slowSqlDiv = document.getElementById("slow-sql-tables");
   145     // Main thread
   146     if (mainThreadCount > 0) {
   147       let table = document.createElement("table");
   148       this.renderTableHeader(table, this.mainThreadTitle);
   149       this.renderTable(table, mainThread);
   151       slowSqlDiv.appendChild(table);
   152       slowSqlDiv.appendChild(document.createElement("hr"));
   153     }
   155     // Other threads
   156     if (otherThreadCount > 0) {
   157       let table = document.createElement("table");
   158       this.renderTableHeader(table, this.otherThreadTitle);
   159       this.renderTable(table, otherThreads);
   161       slowSqlDiv.appendChild(table);
   162       slowSqlDiv.appendChild(document.createElement("hr"));
   163     }
   164   },
   166   /**
   167    * Creates a header row for a Slow SQL table
   168    * Tabs & newlines added to cells to make it easier to copy-paste.
   169    *
   170    * @param aTable Parent table element
   171    * @param aTitle Table's title
   172    */
   173   renderTableHeader: function SlowSQL_renderTableHeader(aTable, aTitle) {
   174     let caption = document.createElement("caption");
   175     caption.appendChild(document.createTextNode(aTitle + "\n"));
   176     aTable.appendChild(caption);
   178     let headings = document.createElement("tr");
   179     this.appendColumn(headings, "th", this.slowSqlHits + "\t");
   180     this.appendColumn(headings, "th", this.slowSqlAverage + "\t");
   181     this.appendColumn(headings, "th", this.slowSqlStatement + "\n");
   182     aTable.appendChild(headings);
   183   },
   185   /**
   186    * Fills out the table body
   187    * Tabs & newlines added to cells to make it easier to copy-paste.
   188    *
   189    * @param aTable Parent table element
   190    * @param aSql SQL stats object
   191    */
   192   renderTable: function SlowSQL_renderTable(aTable, aSql) {
   193     for (let [sql, [hitCount, totalTime]] of Iterator(aSql)) {
   194       let averageTime = totalTime / hitCount;
   196       let sqlRow = document.createElement("tr");
   198       this.appendColumn(sqlRow, "td", hitCount + "\t");
   199       this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
   200       this.appendColumn(sqlRow, "td", sql + "\n");
   202       aTable.appendChild(sqlRow);
   203     }
   204   },
   206   /**
   207    * Helper function for appending a column to a Slow SQL table.
   208    *
   209    * @param aRowElement Parent row element
   210    * @param aColType Column's tag name
   211    * @param aColText Column contents
   212    */
   213   appendColumn: function SlowSQL_appendColumn(aRowElement, aColType, aColText) {
   214     let colElement = document.createElement(aColType);
   215     let colTextElement = document.createTextNode(aColText);
   216     colElement.appendChild(colTextElement);
   217     aRowElement.appendChild(colElement);
   218   }
   219 };
   221 /**
   222  * Removes child elements from the supplied div
   223  *
   224  * @param aDiv Element to be cleared
   225  */
   226 function clearDivData(aDiv) {
   227   while (aDiv.hasChildNodes()) {
   228     aDiv.removeChild(aDiv.lastChild);
   229   }
   230 };
   232 let StackRenderer = {
   234   stackTitle: bundle.GetStringFromName("stackTitle"),
   236   memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"),
   238   /**
   239    * Outputs the memory map associated with this hang report
   240    *
   241    * @param aDiv Output div
   242    */
   243   renderMemoryMap: function StackRenderer_renderMemoryMap(aDiv, memoryMap) {
   244     aDiv.appendChild(document.createTextNode(this.memoryMapTitle));
   245     aDiv.appendChild(document.createElement("br"));
   247     for (let currentModule of memoryMap) {
   248       aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
   249       aDiv.appendChild(document.createElement("br"));
   250     }
   252     aDiv.appendChild(document.createElement("br"));
   253   },
   255   /**
   256    * Outputs the raw PCs from the hang's stack
   257    *
   258    * @param aDiv Output div
   259    * @param aStack Array of PCs from the hang stack
   260    */
   261   renderStack: function StackRenderer_renderStack(aDiv, aStack) {
   262     aDiv.appendChild(document.createTextNode(this.stackTitle));
   263     let stackText = " " + aStack.join(" ");
   264     aDiv.appendChild(document.createTextNode(stackText));
   266     aDiv.appendChild(document.createElement("br"));
   267     aDiv.appendChild(document.createElement("br"));
   268   },
   269   renderStacks: function StackRenderer_renderStacks(aPrefix, aStacks,
   270                                                     aMemoryMap, aRenderHeader) {
   271     let div = document.getElementById(aPrefix + '-data');
   272     clearDivData(div);
   274     let fetchE = document.getElementById(aPrefix + '-fetch-symbols');
   275     if (fetchE) {
   276       fetchE.classList.remove("hidden");
   277     }
   278     let hideE = document.getElementById(aPrefix + '-hide-symbols');
   279     if (hideE) {
   280       hideE.classList.add("hidden");
   281     }
   283     if (aStacks.length == 0) {
   284       return;
   285     }
   287     setHasData(aPrefix + '-section', true);
   289     this.renderMemoryMap(div, aMemoryMap);
   291     for (let i = 0; i < aStacks.length; ++i) {
   292       let stack = aStacks[i];
   293       aRenderHeader(i);
   294       this.renderStack(div, stack)
   295     }
   296   },
   298   /**
   299    * Renders the title of the stack: e.g. "Late Write #1" or
   300    * "Hang Report #1 (6 seconds)".
   301    *
   302    * @param aFormatArgs formating args to be passed to formatStringFromName.
   303    */
   304   renderHeader: function StackRenderer_renderHeader(aPrefix, aFormatArgs) {
   305     let div = document.getElementById(aPrefix + "-data");
   307     let titleElement = document.createElement("span");
   308     titleElement.className = "stack-title";
   310     let titleText = bundle.formatStringFromName(
   311       aPrefix + "-title", aFormatArgs, aFormatArgs.length);
   312     titleElement.appendChild(document.createTextNode(titleText));
   314     div.appendChild(titleElement);
   315     div.appendChild(document.createElement("br"));
   316   }
   317 };
   319 function SymbolicationRequest(aPrefix, aRenderHeader, aMemoryMap, aStacks) {
   320   this.prefix = aPrefix;
   321   this.renderHeader = aRenderHeader;
   322   this.memoryMap = aMemoryMap;
   323   this.stacks = aStacks;
   324 }
   325 /**
   326  * A callback for onreadystatechange. It replaces the numeric stack with
   327  * the symbolicated one returned by the symbolication server.
   328  */
   329 SymbolicationRequest.prototype.handleSymbolResponse =
   330 function SymbolicationRequest_handleSymbolResponse() {
   331   if (this.symbolRequest.readyState != 4)
   332     return;
   334   let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
   335   fetchElement.classList.add("hidden");
   336   let hideElement = document.getElementById(this.prefix + "-hide-symbols");
   337   hideElement.classList.remove("hidden");
   338   let div = document.getElementById(this.prefix + "-data");
   339   clearDivData(div);
   340   let errorMessage = bundle.GetStringFromName("errorFetchingSymbols");
   342   if (this.symbolRequest.status != 200) {
   343     div.appendChild(document.createTextNode(errorMessage));
   344     return;
   345   }
   347   let jsonResponse = {};
   348   try {
   349     jsonResponse = JSON.parse(this.symbolRequest.responseText);
   350   } catch (e) {
   351     div.appendChild(document.createTextNode(errorMessage));
   352     return;
   353   }
   355   for (let i = 0; i < jsonResponse.length; ++i) {
   356     let stack = jsonResponse[i];
   357     this.renderHeader(i);
   359     for (let symbol of stack) {
   360       div.appendChild(document.createTextNode(symbol));
   361       div.appendChild(document.createElement("br"));
   362     }
   363     div.appendChild(document.createElement("br"));
   364   }
   365 };
   366 /**
   367  * Send a request to the symbolication server to symbolicate this stack.
   368  */
   369 SymbolicationRequest.prototype.fetchSymbols =
   370 function SymbolicationRequest_fetchSymbols() {
   371   let symbolServerURI =
   372     getPref(PREF_SYMBOL_SERVER_URI, DEFAULT_SYMBOL_SERVER_URI);
   373   let request = {"memoryMap" : this.memoryMap, "stacks" : this.stacks,
   374                  "version" : 3};
   375   let requestJSON = JSON.stringify(request);
   377   this.symbolRequest = new XMLHttpRequest();
   378   this.symbolRequest.open("POST", symbolServerURI, true);
   379   this.symbolRequest.setRequestHeader("Content-type", "application/json");
   380   this.symbolRequest.setRequestHeader("Content-length",
   381                                       requestJSON.length);
   382   this.symbolRequest.setRequestHeader("Connection", "close");
   383   this.symbolRequest.onreadystatechange = this.handleSymbolResponse.bind(this);
   384   this.symbolRequest.send(requestJSON);
   385 }
   387 let ChromeHangs = {
   389   symbolRequest: null,
   391   /**
   392    * Renders raw chrome hang data
   393    */
   394   render: function ChromeHangs_render() {
   395     let hangs = Telemetry.chromeHangs;
   396     let stacks = hangs.stacks;
   397     let memoryMap = hangs.memoryMap;
   399     StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap,
   400 			       this.renderHangHeader);
   401   },
   403   renderHangHeader: function ChromeHangs_renderHangHeader(aIndex) {
   404     let durations = Telemetry.chromeHangs.durations;
   405     StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, durations[aIndex]]);
   406   }
   407 };
   409 let ThreadHangStats = {
   411   /**
   412    * Renders raw thread hang stats data
   413    */
   414   render: function() {
   415     let div = document.getElementById("thread-hang-stats");
   416     clearDivData(div);
   418     let stats = Telemetry.threadHangStats;
   419     stats.forEach((thread) => {
   420       div.appendChild(this.renderThread(thread));
   421     });
   422     if (stats.length) {
   423       setHasData("thread-hang-stats-section", true);
   424     }
   425   },
   427   /**
   428    * Creates and fills data corresponding to a thread
   429    */
   430   renderThread: function(aThread) {
   431     let div = document.createElement("div");
   433     let title = document.createElement("h2");
   434     title.textContent = aThread.name;
   435     div.appendChild(title);
   437     // Don't localize the histogram name, because the
   438     // name is also used as the div element's ID
   439     Histogram.render(div, aThread.name + "-Activity",
   440                      aThread.activity, {exponential: true});
   441     aThread.hangs.forEach((hang, index) => {
   442       let hangName = aThread.name + "-Hang-" + (index + 1);
   443       let hangDiv = Histogram.render(
   444         div, hangName, hang.histogram, {exponential: true});
   445       let stackDiv = document.createElement("div");
   446       hang.stack.forEach((frame) => {
   447         stackDiv.appendChild(document.createTextNode(frame));
   448         // Leave an extra <br> at the end of the stack listing
   449         stackDiv.appendChild(document.createElement("br"));
   450       });
   451       // Insert stack after the histogram title
   452       hangDiv.insertBefore(stackDiv, hangDiv.childNodes[1]);
   453     });
   454     return div;
   455   },
   456 };
   458 let Histogram = {
   460   hgramSamplesCaption: bundle.GetStringFromName("histogramSamples"),
   462   hgramAverageCaption: bundle.GetStringFromName("histogramAverage"),
   464   hgramSumCaption: bundle.GetStringFromName("histogramSum"),
   466   hgramCopyCaption: bundle.GetStringFromName("histogramCopy"),
   468   /**
   469    * Renders a single Telemetry histogram
   470    *
   471    * @param aParent Parent element
   472    * @param aName Histogram name
   473    * @param aHgram Histogram information
   474    * @param aOptions Object with render options
   475    *                 * exponential: bars follow logarithmic scale
   476    */
   477   render: function Histogram_render(aParent, aName, aHgram, aOptions) {
   478     let hgram = this.unpack(aHgram);
   479     let options = aOptions || {};
   481     let outerDiv = document.createElement("div");
   482     outerDiv.className = "histogram";
   483     outerDiv.id = aName;
   485     let divTitle = document.createElement("div");
   486     divTitle.className = "histogram-title";
   487     divTitle.appendChild(document.createTextNode(aName));
   488     outerDiv.appendChild(divTitle);
   490     let stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " +
   491                 this.hgramAverageCaption + " = " + hgram.pretty_average + ", " +
   492                 this.hgramSumCaption + " = " + hgram.sum;
   494     let divStats = document.createElement("div");
   495     divStats.appendChild(document.createTextNode(stats));
   496     outerDiv.appendChild(divStats);
   498     if (isRTL())
   499       hgram.values.reverse();
   501     let textData = this.renderValues(outerDiv, hgram.values, hgram.max,
   502                                      hgram.sample_count, options);
   504     // The 'Copy' button contains the textual data, copied to clipboard on click
   505     let copyButton = document.createElement("button");
   506     copyButton.className = "copy-node";
   507     copyButton.appendChild(document.createTextNode(this.hgramCopyCaption));
   508     copyButton.histogramText = aName + EOL + stats + EOL + EOL + textData;
   509     copyButton.addEventListener("click", function(){
   510       Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper)
   511                                                  .copyString(this.histogramText);
   512     });
   513     outerDiv.appendChild(copyButton);
   515     aParent.appendChild(outerDiv);
   516     return outerDiv;
   517   },
   519   /**
   520    * Unpacks histogram values
   521    *
   522    * @param aHgram Packed histogram
   523    *
   524    * @return Unpacked histogram representation
   525    */
   526   unpack: function Histogram_unpack(aHgram) {
   527     let sample_count = aHgram.counts.reduceRight(function (a, b) a + b);
   528     let buckets = [0, 1];
   529     if (aHgram.histogram_type != Telemetry.HISTOGRAM_BOOLEAN) {
   530       buckets = aHgram.ranges;
   531     }
   533     let average =  Math.round(aHgram.sum * 10 / sample_count) / 10;
   534     let max_value = Math.max.apply(Math, aHgram.counts);
   536     let first = true;
   537     let last = 0;
   538     let values = [];
   539     for (let i = 0; i < buckets.length; i++) {
   540       let count = aHgram.counts[i];
   541       if (!count)
   542         continue;
   543       if (first) {
   544         first = false;
   545         if (i) {
   546           values.push([buckets[i - 1], 0]);
   547         }
   548       }
   549       last = i + 1;
   550       values.push([buckets[i], count]);
   551     }
   552     if (last && last < buckets.length) {
   553       values.push([buckets[last], 0]);
   554     }
   556     let result = {
   557       values: values,
   558       pretty_average: average,
   559       max: max_value,
   560       sample_count: sample_count,
   561       sum: aHgram.sum
   562     };
   564     return result;
   565   },
   567   /**
   568    * Return a non-negative, logarithmic representation of a non-negative number.
   569    * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
   570    *
   571    * @param aNumber Non-negative number
   572    */
   573   getLogValue: function(aNumber) {
   574     return Math.max(0, Math.log10(aNumber) + 1);
   575   },
   577   /**
   578    * Create histogram HTML bars, also returns a textual representation
   579    * Both aMaxValue and aSumValues must be positive.
   580    * Values are assumed to use 0 as baseline.
   581    *
   582    * @param aDiv Outer parent div
   583    * @param aValues Histogram values
   584    * @param aMaxValue Value of the longest bar (length, not label)
   585    * @param aSumValues Sum of all bar values
   586    * @param aOptions Object with render options (@see #render)
   587    */
   588   renderValues: function Histogram_renderValues(aDiv, aValues, aMaxValue, aSumValues, aOptions) {
   589     let text = "";
   590     // If the last label is not the longest string, alignment will break a little
   591     let labelPadTo = String(aValues[aValues.length -1][0]).length;
   592     let maxBarValue = aOptions.exponential ? this.getLogValue(aMaxValue) : aMaxValue;
   594     for (let [label, value] of aValues) {
   595       let barValue = aOptions.exponential ? this.getLogValue(value) : value;
   597       // Create a text representation: <right-aligned-label> |<bar-of-#><value>  <percentage>
   598       text += EOL
   599               + " ".repeat(Math.max(0, labelPadTo - String(label).length)) + label // Right-aligned label
   600               + " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar
   601               + "  " + value // Value
   602               + "  " + Math.round(100 * value / aSumValues) + "%"; // Percentage
   604       // Construct the HTML labels + bars
   605       let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
   606       let aboveEm = MAX_BAR_HEIGHT - belowEm;
   608       let barDiv = document.createElement("div");
   609       barDiv.className = "bar";
   610       barDiv.style.paddingTop = aboveEm + "em";
   612       // Add value label or an nbsp if no value
   613       barDiv.appendChild(document.createTextNode(value ? value : '\u00A0'));
   615       // Create the blue bar
   616       let bar = document.createElement("div");
   617       bar.className = "bar-inner";
   618       bar.style.height = belowEm + "em";
   619       barDiv.appendChild(bar);
   621       // Add bucket label
   622       barDiv.appendChild(document.createTextNode(label));
   624       aDiv.appendChild(barDiv);
   625     }
   627     return text.substr(EOL.length); // Trim the EOL before the first line
   628   },
   630   /**
   631    * Helper function for filtering histogram elements by their id
   632    * Adds the "filter-blocked" class to histogram nodes whose IDs don't match the filter.
   633    *
   634    * @param aContainerNode Container node containing the histogram class nodes to filter
   635    * @param aFilterText either text or /RegEx/. If text, case-insensitive and AND words
   636    */
   637   filterHistograms: function _filterHistograms(aContainerNode, aFilterText) {
   638     let filter = aFilterText.toString();
   640     // Pass if: all non-empty array items match (case-sensitive)
   641     function isPassText(subject, filter) {
   642       for (let item of filter) {
   643         if (item.length && subject.indexOf(item) < 0) {
   644           return false; // mismatch and not a spurious space
   645         }
   646       }
   647       return true;
   648     }
   650     function isPassRegex(subject, filter) {
   651       return filter.test(subject);
   652     }
   654     // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
   655     let isPassFunc; // filter function, set once, then applied to all elements
   656     filter = filter.trim();
   657     if (filter[0] != "/") { // Plain text: case insensitive, AND if multi-string
   658       isPassFunc = isPassText;
   659       filter = filter.toLowerCase().split(" ");
   660     } else {
   661       isPassFunc = isPassRegex;
   662       var r = filter.match(/^\/(.*)\/(i?)$/);
   663       try {
   664         filter = RegExp(r[1], r[2]);
   665       }
   666       catch (e) { // Incomplete or bad RegExp - always no match
   667         isPassFunc = function() {
   668           return false;
   669         };
   670       }
   671     }
   673     let needLower = (isPassFunc === isPassText);
   675     let histograms = aContainerNode.getElementsByClassName("histogram");
   676     for (let hist of histograms) {
   677       hist.classList[isPassFunc((needLower ? hist.id.toLowerCase() : hist.id), filter) ? "remove" : "add"]("filter-blocked");
   678     }
   679   },
   681   /**
   682    * Event handler for change at histograms filter input
   683    *
   684    * When invoked, 'this' is expected to be the filter HTML node.
   685    */
   686   histogramFilterChanged: function _histogramFilterChanged() {
   687     if (this.idleTimeout) {
   688       clearTimeout(this.idleTimeout);
   689     }
   691     this.idleTimeout = setTimeout( () => {
   692       Histogram.filterHistograms(document.getElementById(this.getAttribute("target_id")), this.value);
   693     }, FILTER_IDLE_TIMEOUT);
   694   }
   695 };
   697 /*
   698  * Helper function to render JS objects with white space between top level elements
   699  * so that they look better in the browser
   700  * @param   aObject JavaScript object or array to render
   701  * @return  String
   702  */
   703 function RenderObject(aObject) {
   704   let output = "";
   705   if (Array.isArray(aObject)) {
   706     if (aObject.length == 0) {
   707       return "[]";
   708     }
   709     output = "[" + JSON.stringify(aObject[0]);
   710     for (let i = 1; i < aObject.length; i++) {
   711       output += ", " + JSON.stringify(aObject[i]);
   712     }
   713     return output + "]";
   714   }
   715   let keys = Object.keys(aObject);
   716   if (keys.length == 0) {
   717     return "{}";
   718   }
   719   output = "{\"" + keys[0] + "\":\u00A0" + JSON.stringify(aObject[keys[0]]);
   720   for (let i = 1; i < keys.length; i++) {
   721     output += ", \"" + keys[i] + "\":\u00A0" + JSON.stringify(aObject[keys[i]]);
   722   }
   723   return output + "}";
   724 };
   726 let KeyValueTable = {
   727   /**
   728    * Returns a 2-column table with keys and values
   729    * @param aMeasurements Each key in this JS object is rendered as a row in
   730    *                      the table with its corresponding value
   731    * @param aKeysLabel    Column header for the keys column
   732    * @param aValuesLabel  Column header for the values column
   733    */
   734   render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) {
   735     let table = document.createElement("table");
   736     this.renderHeader(table, aKeysLabel, aValuesLabel);
   737     this.renderBody(table, aMeasurements);
   738     return table;
   739   },
   741   /**
   742    * Create the table header
   743    * Tabs & newlines added to cells to make it easier to copy-paste.
   744    *
   745    * @param aTable Table element
   746    * @param aKeysLabel    Column header for the keys column
   747    * @param aValuesLabel  Column header for the values column
   748    */
   749   renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) {
   750     let headerRow = document.createElement("tr");
   751     aTable.appendChild(headerRow);
   753     let keysColumn = document.createElement("th");
   754     keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t"));
   755     let valuesColumn = document.createElement("th");
   756     valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n"));
   758     headerRow.appendChild(keysColumn);
   759     headerRow.appendChild(valuesColumn);
   760   },
   762   /**
   763    * Create the table body
   764    * Tabs & newlines added to cells to make it easier to copy-paste.
   765    *
   766    * @param aTable Table element
   767    * @param aMeasurements Key/value map
   768    */
   769   renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) {
   770     for (let [key, value] of Iterator(aMeasurements)) {
   771       // use .valueOf() to unbox Number, String, etc. objects
   772       if ((typeof value == "object") && (typeof value.valueOf() == "object")) {
   773         value = RenderObject(value);
   774       }
   776       let newRow = document.createElement("tr");
   777       aTable.appendChild(newRow);
   779       let keyField = document.createElement("td");
   780       keyField.appendChild(document.createTextNode(key + "\t"));
   781       newRow.appendChild(keyField);
   783       let valueField = document.createElement("td");
   784       valueField.appendChild(document.createTextNode(value + "\n"));
   785       newRow.appendChild(valueField);
   786     }
   787   }
   788 };
   790 let AddonDetails = {
   791   tableIDTitle: bundle.GetStringFromName("addonTableID"),
   792   tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"),
   794   /**
   795    * Render the addon details section as a series of headers followed by key/value tables
   796    * @param aSections Object containing the details sections to render
   797    */
   798   render: function AddonDetails_render(aSections) {
   799     let addonSection = document.getElementById("addon-details");
   800     for (let provider in aSections) {
   801       let providerSection = document.createElement("h2");
   802       let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
   803       providerSection.appendChild(document.createTextNode(titleText));
   804       addonSection.appendChild(providerSection);
   805       addonSection.appendChild(
   806         KeyValueTable.render(aSections[provider],
   807                              this.tableIDTitle, this.tableDetailsTitle));
   808     }
   809   }
   810 };
   812 /**
   813  * Helper function for showing either the toggle element or "No data collected" message for a section
   814  *
   815  * @param aSectionID ID of the section element that needs to be changed
   816  * @param aHasData true (default) indicates that toggle should be displayed
   817  */
   818 function setHasData(aSectionID, aHasData) {
   819   let sectionElement = document.getElementById(aSectionID);
   820   sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
   821 }
   823 /**
   824  * Helper function that expands and collapses sections +
   825  * changes caption on the toggle text
   826  */
   827 function toggleSection(aEvent) {
   828   let parentElement = aEvent.target.parentElement;
   829   if (!parentElement.classList.contains("has-data")) {
   830     return; // nothing to toggle
   831   }
   833   parentElement.classList.toggle("expanded");
   835   // Store section opened/closed state in a hidden checkbox (which is then used on reload)
   836   let statebox = parentElement.getElementsByClassName("statebox")[0];
   837   statebox.checked = parentElement.classList.contains("expanded");
   838 }
   840 /**
   841  * Sets the text of the page header based on a config pref + bundle strings
   842  */
   843 function setupPageHeader()
   844 {
   845   let serverOwner = getPref(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
   846   let brandName = brandBundle.GetStringFromName("brandFullName");
   847   let subtitleText = bundle.formatStringFromName(
   848     "pageSubtitle", [serverOwner, brandName], 2);
   850   let subtitleElement = document.getElementById("page-subtitle");
   851   subtitleElement.appendChild(document.createTextNode(subtitleText));
   852 }
   854 /**
   855  * Initializes load/unload, pref change and mouse-click listeners
   856  */
   857 function setupListeners() {
   858   Services.prefs.addObserver(PREF_TELEMETRY_ENABLED, observer, false);
   859   observer.updatePrefStatus();
   861   // Clean up observers when page is closed
   862   window.addEventListener("unload",
   863     function unloadHandler(aEvent) {
   864       window.removeEventListener("unload", unloadHandler);
   865       Services.prefs.removeObserver(PREF_TELEMETRY_ENABLED, observer);
   866   }, false);
   868   document.getElementById("toggle-telemetry").addEventListener("click",
   869     function () {
   870       let value = getPref(PREF_TELEMETRY_ENABLED, false);
   871       Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, !value);
   872   }, false);
   874   document.getElementById("chrome-hangs-fetch-symbols").addEventListener("click",
   875     function () {
   876       let hangs = Telemetry.chromeHangs;
   877       let req = new SymbolicationRequest("chrome-hangs",
   878                                          ChromeHangs.renderHangHeader,
   879                                          hangs.memoryMap, hangs.stacks);
   880       req.fetchSymbols();
   881   }, false);
   883   document.getElementById("chrome-hangs-hide-symbols").addEventListener("click",
   884     function () {
   885       ChromeHangs.render();
   886   }, false);
   888   document.getElementById("late-writes-fetch-symbols").addEventListener("click",
   889     function () {
   890       let lateWrites = TelemetryPing.getPayload().lateWrites;
   891       let req = new SymbolicationRequest("late-writes",
   892                                          LateWritesSingleton.renderHeader,
   893                                          lateWrites.memoryMap,
   894                                          lateWrites.stacks);
   895       req.fetchSymbols();
   896   }, false);
   898   document.getElementById("late-writes-hide-symbols").addEventListener("click",
   899     function () {
   900       let ping = TelemetryPing.getPayload();
   901       LateWritesSingleton.renderLateWrites(ping.lateWrites);
   902   }, false);
   905   // Clicking on the section name will toggle its state
   906   let sectionHeaders = document.getElementsByClassName("section-name");
   907   for (let sectionHeader of sectionHeaders) {
   908     sectionHeader.addEventListener("click", toggleSection, false);
   909   }
   911   // Clicking on the "toggle" text will also toggle section's state
   912   let toggleLinks = document.getElementsByClassName("toggle-caption");
   913   for (let toggleLink of toggleLinks) {
   914     toggleLink.addEventListener("click", toggleSection, false);
   915   }
   916 }
   919 function onLoad() {
   920   window.removeEventListener("load", onLoad);
   922   // Set the text in the page header
   923   setupPageHeader();
   925   // Set up event listeners
   926   setupListeners();
   928   // Show slow SQL stats
   929   SlowSQL.render();
   931   // Show chrome hang stacks
   932   ChromeHangs.render();
   934   // Show thread hang stats
   935   ThreadHangStats.render();
   937   // Show histogram data
   938   let histograms = Telemetry.histogramSnapshots;
   939   if (Object.keys(histograms).length) {
   940     let hgramDiv = document.getElementById("histograms");
   941     for (let [name, hgram] of Iterator(histograms)) {
   942       Histogram.render(hgramDiv, name, hgram);
   943     }
   945     let filterBox = document.getElementById("histograms-filter");
   946     filterBox.addEventListener("input", Histogram.histogramFilterChanged, false);
   947     if (filterBox.value.trim() != "") { // on load, no need to filter if empty
   948       Histogram.filterHistograms(hgramDiv, filterBox.value);
   949     }
   951     setHasData("histograms-section", true);
   952   }
   954   // Show addon histogram data
   955   let addonDiv = document.getElementById("addon-histograms");
   956   let addonHistogramsRendered = false;
   957   let addonData = Telemetry.addonHistogramSnapshots;
   958   for (let [addon, histograms] of Iterator(addonData)) {
   959     for (let [name, hgram] of Iterator(histograms)) {
   960       addonHistogramsRendered = true;
   961       Histogram.render(addonDiv, addon + ": " + name, hgram);
   962     }
   963   }
   965   if (addonHistogramsRendered) {
   966    setHasData("addon-histograms-section", true);
   967   }
   969   // Get the Telemetry Ping payload
   970   Telemetry.asyncFetchTelemetryData(displayPingData);
   972   // Restore sections states
   973   let stateboxes = document.getElementsByClassName("statebox");
   974   for (let box of stateboxes) {
   975     if (box.checked) { // Was open. Will still display as empty if not has-data
   976         box.parentElement.classList.add("expanded");
   977     }
   978   }
   979 }
   981 let LateWritesSingleton = {
   982   renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
   983     StackRenderer.renderHeader("late-writes", [aIndex + 1]);
   984   },
   986   renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
   987     let stacks = lateWrites.stacks;
   988     let memoryMap = lateWrites.memoryMap;
   989     StackRenderer.renderStacks('late-writes', stacks, memoryMap,
   990                                LateWritesSingleton.renderHeader);
   991   }
   992 };
   994 /**
   995  * Helper function for sorting the startup milestones in the Simple Measurements
   996  * section into temporal order.
   997  *
   998  * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
   999  * @return Sorted measurements
  1000  */
  1001 function sortStartupMilestones(aSimpleMeasurements) {
  1002   const telemetryTimestamps = TelemetryTimestamps.get();
  1003   let startupEvents = Services.startup.getStartupInfo();
  1004   delete startupEvents['process'];
  1006   function keyIsMilestone(k) {
  1007     return (k in startupEvents) || (k in telemetryTimestamps);
  1010   let sortedKeys = Object.keys(aSimpleMeasurements);
  1012   // Sort the measurements, with startup milestones at the front + ordered by time
  1013   sortedKeys.sort(function keyCompare(keyA, keyB) {
  1014     let isKeyAMilestone = keyIsMilestone(keyA);
  1015     let isKeyBMilestone = keyIsMilestone(keyB);
  1017     // First order by startup vs non-startup measurement
  1018     if (isKeyAMilestone && !isKeyBMilestone)
  1019       return -1;
  1020     if (!isKeyAMilestone && isKeyBMilestone)
  1021       return 1;
  1022     // Don't change order of non-startup measurements
  1023     if (!isKeyAMilestone && !isKeyBMilestone)
  1024       return 0;
  1026     // If both keys are startup measurements, order them by value
  1027     return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
  1028   });
  1030   // Insert measurements into a result object in sort-order
  1031   let result = {};
  1032   for (let key of sortedKeys) {
  1033     result[key] = aSimpleMeasurements[key];
  1036   return result;
  1039 function displayPingData() {
  1040   let ping = TelemetryPing.getPayload();
  1042   let keysHeader = bundle.GetStringFromName("keysHeader");
  1043   let valuesHeader = bundle.GetStringFromName("valuesHeader");
  1045   // Show simple measurements
  1046   let simpleMeasurements = sortStartupMilestones(ping.simpleMeasurements);
  1047   if (Object.keys(simpleMeasurements).length) {
  1048     let simpleSection = document.getElementById("simple-measurements");
  1049     simpleSection.appendChild(KeyValueTable.render(simpleMeasurements,
  1050                                                    keysHeader, valuesHeader));
  1051     setHasData("simple-measurements-section", true);
  1054   LateWritesSingleton.renderLateWrites(ping.lateWrites);
  1056   // Show basic system info gathered
  1057   if (Object.keys(ping.info).length) {
  1058     let infoSection = document.getElementById("system-info");
  1059     infoSection.appendChild(KeyValueTable.render(ping.info,
  1060                                                  keysHeader, valuesHeader));
  1061     setHasData("system-info-section", true);
  1064   let addonDetails = ping.addonDetails;
  1065   if (Object.keys(addonDetails).length) {
  1066     AddonDetails.render(addonDetails);
  1067     setHasData("addon-details-section", true);
  1071 window.addEventListener("load", onLoad, false);

mercurial