Sat, 03 Jan 2015 20:18:00 +0100
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);
1008 }
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];
1034 }
1036 return result;
1037 }
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);
1052 }
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);
1062 }
1064 let addonDetails = ping.addonDetails;
1065 if (Object.keys(addonDetails).length) {
1066 AddonDetails.render(addonDetails);
1067 setHasData("addon-details-section", true);
1068 }
1069 }
1071 window.addEventListener("load", onLoad, false);