|
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/. */ |
|
4 |
|
5 'use strict'; |
|
6 |
|
7 const Ci = Components.interfaces; |
|
8 const Cc = Components.classes; |
|
9 const Cu = Components.utils; |
|
10 |
|
11 Cu.import("resource://gre/modules/Services.jsm"); |
|
12 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm"); |
|
13 Cu.import("resource://gre/modules/TelemetryPing.jsm"); |
|
14 |
|
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"); |
|
20 |
|
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"; |
|
29 |
|
30 // ms idle before applying the filter (allow uninterrupted typing) |
|
31 const FILTER_IDLE_TIMEOUT = 500; |
|
32 |
|
33 #ifdef XP_WIN |
|
34 const EOL = "\r\n"; |
|
35 #else |
|
36 const EOL = "\n"; |
|
37 #endif |
|
38 |
|
39 // Cached value of document's RTL mode |
|
40 let documentRTLMode = ""; |
|
41 |
|
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; |
|
51 |
|
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 } |
|
62 |
|
63 return result; |
|
64 } |
|
65 |
|
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 } |
|
75 |
|
76 let observer = { |
|
77 |
|
78 enableTelemetry: bundle.GetStringFromName("enableTelemetry"), |
|
79 |
|
80 disableTelemetry: bundle.GetStringFromName("disableTelemetry"), |
|
81 |
|
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 }, |
|
90 |
|
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 }; |
|
110 |
|
111 let SlowSQL = { |
|
112 |
|
113 slowSqlHits: bundle.GetStringFromName("slowSqlHits"), |
|
114 |
|
115 slowSqlAverage: bundle.GetStringFromName("slowSqlAverage"), |
|
116 |
|
117 slowSqlStatement: bundle.GetStringFromName("slowSqlStatement"), |
|
118 |
|
119 mainThreadTitle: bundle.GetStringFromName("slowSqlMain"), |
|
120 |
|
121 otherThreadTitle: bundle.GetStringFromName("slowSqlOther"), |
|
122 |
|
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"]; |
|
130 |
|
131 let mainThreadCount = Object.keys(mainThread).length; |
|
132 let otherThreadCount = Object.keys(otherThreads).length; |
|
133 if (mainThreadCount == 0 && otherThreadCount == 0) { |
|
134 return; |
|
135 } |
|
136 |
|
137 setHasData("slow-sql-section", true); |
|
138 |
|
139 if (debugSlowSql) { |
|
140 document.getElementById("sql-warning").classList.remove("hidden"); |
|
141 } |
|
142 |
|
143 let slowSqlDiv = document.getElementById("slow-sql-tables"); |
|
144 |
|
145 // Main thread |
|
146 if (mainThreadCount > 0) { |
|
147 let table = document.createElement("table"); |
|
148 this.renderTableHeader(table, this.mainThreadTitle); |
|
149 this.renderTable(table, mainThread); |
|
150 |
|
151 slowSqlDiv.appendChild(table); |
|
152 slowSqlDiv.appendChild(document.createElement("hr")); |
|
153 } |
|
154 |
|
155 // Other threads |
|
156 if (otherThreadCount > 0) { |
|
157 let table = document.createElement("table"); |
|
158 this.renderTableHeader(table, this.otherThreadTitle); |
|
159 this.renderTable(table, otherThreads); |
|
160 |
|
161 slowSqlDiv.appendChild(table); |
|
162 slowSqlDiv.appendChild(document.createElement("hr")); |
|
163 } |
|
164 }, |
|
165 |
|
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); |
|
177 |
|
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 }, |
|
184 |
|
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; |
|
195 |
|
196 let sqlRow = document.createElement("tr"); |
|
197 |
|
198 this.appendColumn(sqlRow, "td", hitCount + "\t"); |
|
199 this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t"); |
|
200 this.appendColumn(sqlRow, "td", sql + "\n"); |
|
201 |
|
202 aTable.appendChild(sqlRow); |
|
203 } |
|
204 }, |
|
205 |
|
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 }; |
|
220 |
|
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 }; |
|
231 |
|
232 let StackRenderer = { |
|
233 |
|
234 stackTitle: bundle.GetStringFromName("stackTitle"), |
|
235 |
|
236 memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"), |
|
237 |
|
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")); |
|
246 |
|
247 for (let currentModule of memoryMap) { |
|
248 aDiv.appendChild(document.createTextNode(currentModule.join(" "))); |
|
249 aDiv.appendChild(document.createElement("br")); |
|
250 } |
|
251 |
|
252 aDiv.appendChild(document.createElement("br")); |
|
253 }, |
|
254 |
|
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)); |
|
265 |
|
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); |
|
273 |
|
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 } |
|
282 |
|
283 if (aStacks.length == 0) { |
|
284 return; |
|
285 } |
|
286 |
|
287 setHasData(aPrefix + '-section', true); |
|
288 |
|
289 this.renderMemoryMap(div, aMemoryMap); |
|
290 |
|
291 for (let i = 0; i < aStacks.length; ++i) { |
|
292 let stack = aStacks[i]; |
|
293 aRenderHeader(i); |
|
294 this.renderStack(div, stack) |
|
295 } |
|
296 }, |
|
297 |
|
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"); |
|
306 |
|
307 let titleElement = document.createElement("span"); |
|
308 titleElement.className = "stack-title"; |
|
309 |
|
310 let titleText = bundle.formatStringFromName( |
|
311 aPrefix + "-title", aFormatArgs, aFormatArgs.length); |
|
312 titleElement.appendChild(document.createTextNode(titleText)); |
|
313 |
|
314 div.appendChild(titleElement); |
|
315 div.appendChild(document.createElement("br")); |
|
316 } |
|
317 }; |
|
318 |
|
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; |
|
333 |
|
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"); |
|
341 |
|
342 if (this.symbolRequest.status != 200) { |
|
343 div.appendChild(document.createTextNode(errorMessage)); |
|
344 return; |
|
345 } |
|
346 |
|
347 let jsonResponse = {}; |
|
348 try { |
|
349 jsonResponse = JSON.parse(this.symbolRequest.responseText); |
|
350 } catch (e) { |
|
351 div.appendChild(document.createTextNode(errorMessage)); |
|
352 return; |
|
353 } |
|
354 |
|
355 for (let i = 0; i < jsonResponse.length; ++i) { |
|
356 let stack = jsonResponse[i]; |
|
357 this.renderHeader(i); |
|
358 |
|
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); |
|
376 |
|
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 } |
|
386 |
|
387 let ChromeHangs = { |
|
388 |
|
389 symbolRequest: null, |
|
390 |
|
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; |
|
398 |
|
399 StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap, |
|
400 this.renderHangHeader); |
|
401 }, |
|
402 |
|
403 renderHangHeader: function ChromeHangs_renderHangHeader(aIndex) { |
|
404 let durations = Telemetry.chromeHangs.durations; |
|
405 StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, durations[aIndex]]); |
|
406 } |
|
407 }; |
|
408 |
|
409 let ThreadHangStats = { |
|
410 |
|
411 /** |
|
412 * Renders raw thread hang stats data |
|
413 */ |
|
414 render: function() { |
|
415 let div = document.getElementById("thread-hang-stats"); |
|
416 clearDivData(div); |
|
417 |
|
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 }, |
|
426 |
|
427 /** |
|
428 * Creates and fills data corresponding to a thread |
|
429 */ |
|
430 renderThread: function(aThread) { |
|
431 let div = document.createElement("div"); |
|
432 |
|
433 let title = document.createElement("h2"); |
|
434 title.textContent = aThread.name; |
|
435 div.appendChild(title); |
|
436 |
|
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 }; |
|
457 |
|
458 let Histogram = { |
|
459 |
|
460 hgramSamplesCaption: bundle.GetStringFromName("histogramSamples"), |
|
461 |
|
462 hgramAverageCaption: bundle.GetStringFromName("histogramAverage"), |
|
463 |
|
464 hgramSumCaption: bundle.GetStringFromName("histogramSum"), |
|
465 |
|
466 hgramCopyCaption: bundle.GetStringFromName("histogramCopy"), |
|
467 |
|
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 || {}; |
|
480 |
|
481 let outerDiv = document.createElement("div"); |
|
482 outerDiv.className = "histogram"; |
|
483 outerDiv.id = aName; |
|
484 |
|
485 let divTitle = document.createElement("div"); |
|
486 divTitle.className = "histogram-title"; |
|
487 divTitle.appendChild(document.createTextNode(aName)); |
|
488 outerDiv.appendChild(divTitle); |
|
489 |
|
490 let stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " + |
|
491 this.hgramAverageCaption + " = " + hgram.pretty_average + ", " + |
|
492 this.hgramSumCaption + " = " + hgram.sum; |
|
493 |
|
494 let divStats = document.createElement("div"); |
|
495 divStats.appendChild(document.createTextNode(stats)); |
|
496 outerDiv.appendChild(divStats); |
|
497 |
|
498 if (isRTL()) |
|
499 hgram.values.reverse(); |
|
500 |
|
501 let textData = this.renderValues(outerDiv, hgram.values, hgram.max, |
|
502 hgram.sample_count, options); |
|
503 |
|
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); |
|
514 |
|
515 aParent.appendChild(outerDiv); |
|
516 return outerDiv; |
|
517 }, |
|
518 |
|
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 } |
|
532 |
|
533 let average = Math.round(aHgram.sum * 10 / sample_count) / 10; |
|
534 let max_value = Math.max.apply(Math, aHgram.counts); |
|
535 |
|
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 } |
|
555 |
|
556 let result = { |
|
557 values: values, |
|
558 pretty_average: average, |
|
559 max: max_value, |
|
560 sample_count: sample_count, |
|
561 sum: aHgram.sum |
|
562 }; |
|
563 |
|
564 return result; |
|
565 }, |
|
566 |
|
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 }, |
|
576 |
|
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; |
|
593 |
|
594 for (let [label, value] of aValues) { |
|
595 let barValue = aOptions.exponential ? this.getLogValue(value) : value; |
|
596 |
|
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 |
|
603 |
|
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; |
|
607 |
|
608 let barDiv = document.createElement("div"); |
|
609 barDiv.className = "bar"; |
|
610 barDiv.style.paddingTop = aboveEm + "em"; |
|
611 |
|
612 // Add value label or an nbsp if no value |
|
613 barDiv.appendChild(document.createTextNode(value ? value : '\u00A0')); |
|
614 |
|
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); |
|
620 |
|
621 // Add bucket label |
|
622 barDiv.appendChild(document.createTextNode(label)); |
|
623 |
|
624 aDiv.appendChild(barDiv); |
|
625 } |
|
626 |
|
627 return text.substr(EOL.length); // Trim the EOL before the first line |
|
628 }, |
|
629 |
|
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(); |
|
639 |
|
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 } |
|
649 |
|
650 function isPassRegex(subject, filter) { |
|
651 return filter.test(subject); |
|
652 } |
|
653 |
|
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 } |
|
672 |
|
673 let needLower = (isPassFunc === isPassText); |
|
674 |
|
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 }, |
|
680 |
|
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 } |
|
690 |
|
691 this.idleTimeout = setTimeout( () => { |
|
692 Histogram.filterHistograms(document.getElementById(this.getAttribute("target_id")), this.value); |
|
693 }, FILTER_IDLE_TIMEOUT); |
|
694 } |
|
695 }; |
|
696 |
|
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 }; |
|
725 |
|
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 }, |
|
740 |
|
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); |
|
752 |
|
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")); |
|
757 |
|
758 headerRow.appendChild(keysColumn); |
|
759 headerRow.appendChild(valuesColumn); |
|
760 }, |
|
761 |
|
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 } |
|
775 |
|
776 let newRow = document.createElement("tr"); |
|
777 aTable.appendChild(newRow); |
|
778 |
|
779 let keyField = document.createElement("td"); |
|
780 keyField.appendChild(document.createTextNode(key + "\t")); |
|
781 newRow.appendChild(keyField); |
|
782 |
|
783 let valueField = document.createElement("td"); |
|
784 valueField.appendChild(document.createTextNode(value + "\n")); |
|
785 newRow.appendChild(valueField); |
|
786 } |
|
787 } |
|
788 }; |
|
789 |
|
790 let AddonDetails = { |
|
791 tableIDTitle: bundle.GetStringFromName("addonTableID"), |
|
792 tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"), |
|
793 |
|
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 }; |
|
811 |
|
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 } |
|
822 |
|
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 } |
|
832 |
|
833 parentElement.classList.toggle("expanded"); |
|
834 |
|
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 } |
|
839 |
|
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); |
|
849 |
|
850 let subtitleElement = document.getElementById("page-subtitle"); |
|
851 subtitleElement.appendChild(document.createTextNode(subtitleText)); |
|
852 } |
|
853 |
|
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(); |
|
860 |
|
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); |
|
867 |
|
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); |
|
873 |
|
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); |
|
882 |
|
883 document.getElementById("chrome-hangs-hide-symbols").addEventListener("click", |
|
884 function () { |
|
885 ChromeHangs.render(); |
|
886 }, false); |
|
887 |
|
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); |
|
897 |
|
898 document.getElementById("late-writes-hide-symbols").addEventListener("click", |
|
899 function () { |
|
900 let ping = TelemetryPing.getPayload(); |
|
901 LateWritesSingleton.renderLateWrites(ping.lateWrites); |
|
902 }, false); |
|
903 |
|
904 |
|
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 } |
|
910 |
|
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 } |
|
917 |
|
918 |
|
919 function onLoad() { |
|
920 window.removeEventListener("load", onLoad); |
|
921 |
|
922 // Set the text in the page header |
|
923 setupPageHeader(); |
|
924 |
|
925 // Set up event listeners |
|
926 setupListeners(); |
|
927 |
|
928 // Show slow SQL stats |
|
929 SlowSQL.render(); |
|
930 |
|
931 // Show chrome hang stacks |
|
932 ChromeHangs.render(); |
|
933 |
|
934 // Show thread hang stats |
|
935 ThreadHangStats.render(); |
|
936 |
|
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 } |
|
944 |
|
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 } |
|
950 |
|
951 setHasData("histograms-section", true); |
|
952 } |
|
953 |
|
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 } |
|
964 |
|
965 if (addonHistogramsRendered) { |
|
966 setHasData("addon-histograms-section", true); |
|
967 } |
|
968 |
|
969 // Get the Telemetry Ping payload |
|
970 Telemetry.asyncFetchTelemetryData(displayPingData); |
|
971 |
|
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 } |
|
980 |
|
981 let LateWritesSingleton = { |
|
982 renderHeader: function LateWritesSingleton_renderHeader(aIndex) { |
|
983 StackRenderer.renderHeader("late-writes", [aIndex + 1]); |
|
984 }, |
|
985 |
|
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 }; |
|
993 |
|
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']; |
|
1005 |
|
1006 function keyIsMilestone(k) { |
|
1007 return (k in startupEvents) || (k in telemetryTimestamps); |
|
1008 } |
|
1009 |
|
1010 let sortedKeys = Object.keys(aSimpleMeasurements); |
|
1011 |
|
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); |
|
1016 |
|
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; |
|
1025 |
|
1026 // If both keys are startup measurements, order them by value |
|
1027 return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB]; |
|
1028 }); |
|
1029 |
|
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 } |
|
1035 |
|
1036 return result; |
|
1037 } |
|
1038 |
|
1039 function displayPingData() { |
|
1040 let ping = TelemetryPing.getPayload(); |
|
1041 |
|
1042 let keysHeader = bundle.GetStringFromName("keysHeader"); |
|
1043 let valuesHeader = bundle.GetStringFromName("valuesHeader"); |
|
1044 |
|
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 } |
|
1053 |
|
1054 LateWritesSingleton.renderLateWrites(ping.lateWrites); |
|
1055 |
|
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 } |
|
1063 |
|
1064 let addonDetails = ping.addonDetails; |
|
1065 if (Object.keys(addonDetails).length) { |
|
1066 AddonDetails.render(addonDetails); |
|
1067 setHasData("addon-details-section", true); |
|
1068 } |
|
1069 } |
|
1070 |
|
1071 window.addEventListener("load", onLoad, false); |