|
1 /* -*- Mode: js2; indent-tabs-mode: nil; js2-basic-offset: 2; -*- */ |
|
2 |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 importScripts("ProgressReporter.js"); |
|
10 |
|
11 var gProfiles = []; |
|
12 |
|
13 var partialTaskData = {}; |
|
14 |
|
15 var gNextProfileID = 0; |
|
16 |
|
17 var gLogLines = []; |
|
18 |
|
19 var gDebugLog = false; |
|
20 var gDebugTrace = false; |
|
21 // Use for verbose tracing, otherwise use log |
|
22 function PROFILDERTRACE(msg) { |
|
23 if (gDebugTrace) |
|
24 PROFILERLOG(msg); |
|
25 } |
|
26 function PROFILERLOG(msg) { |
|
27 if (gDebugLog) { |
|
28 msg = "Cleo: " + msg; |
|
29 //if (window.dump) |
|
30 // window.dump(msg + "\n"); |
|
31 } |
|
32 } |
|
33 function PROFILERERROR(msg) { |
|
34 msg = "Cleo: " + msg; |
|
35 //if (window.dump) |
|
36 // window.dump(msg + "\n"); |
|
37 } |
|
38 |
|
39 // http://stackoverflow.com/a/2548133 |
|
40 function endsWith(str, suffix) { |
|
41 return str.indexOf(suffix, this.length - suffix.length) !== -1; |
|
42 }; |
|
43 |
|
44 // https://bugzilla.mozilla.org/show_bug.cgi?id=728780 |
|
45 if (!String.prototype.startsWith) { |
|
46 String.prototype.startsWith = |
|
47 function(s) { return this.lastIndexOf(s, 0) === 0; } |
|
48 } |
|
49 |
|
50 // functions for which lr is unconditionally valid. These are |
|
51 // largely going to be atomics and other similar functions |
|
52 // that don't touch lr. This is currently populated with |
|
53 // some functions from bionic, largely via manual inspection |
|
54 // of the assembly in e.g. |
|
55 // http://androidxref.com/source/xref/bionic/libc/arch-arm/syscalls/ |
|
56 var sARMFunctionsWithValidLR = [ |
|
57 "__atomic_dec", |
|
58 "__atomic_inc", |
|
59 "__atomic_cmpxchg", |
|
60 "__atomic_swap", |
|
61 "__atomic_dec", |
|
62 "__atomic_inc", |
|
63 "__atomic_cmpxchg", |
|
64 "__atomic_swap", |
|
65 "__futex_syscall3", |
|
66 "__futex_wait", |
|
67 "__futex_wake", |
|
68 "__futex_syscall3", |
|
69 "__futex_wait", |
|
70 "__futex_wake", |
|
71 "__futex_syscall4", |
|
72 "__ioctl", |
|
73 "__brk", |
|
74 "__wait4", |
|
75 "epoll_wait", |
|
76 "fsync", |
|
77 "futex", |
|
78 "nanosleep", |
|
79 "pause", |
|
80 "sched_yield", |
|
81 "syscall" |
|
82 ]; |
|
83 |
|
84 function log() { |
|
85 var z = []; |
|
86 for (var i = 0; i < arguments.length; ++i) |
|
87 z.push(arguments[i]); |
|
88 gLogLines.push(z.join(" ")); |
|
89 } |
|
90 |
|
91 self.onmessage = function (msg) { |
|
92 try { |
|
93 var requestID = msg.data.requestID; |
|
94 var task = msg.data.task; |
|
95 var taskData = msg.data.taskData; |
|
96 if (!taskData && |
|
97 (["chunkedStart", "chunkedChunk", "chunkedEnd"].indexOf(task) == -1)) { |
|
98 taskData = partialTaskData[requestID]; |
|
99 delete partialTaskData[requestID]; |
|
100 } |
|
101 PROFILERLOG("Start task: " + task); |
|
102 |
|
103 gLogLines = []; |
|
104 |
|
105 switch (task) { |
|
106 case "initWorker": |
|
107 gDebugLog = taskData.debugLog; |
|
108 gDebugTrace = taskData.debugTrace; |
|
109 PROFILERLOG("Init logging in parserWorker"); |
|
110 return; |
|
111 case "chunkedStart": |
|
112 partialTaskData[requestID] = null; |
|
113 break; |
|
114 case "chunkedChunk": |
|
115 if (partialTaskData[requestID] === null) |
|
116 partialTaskData[requestID] = msg.data.chunk; |
|
117 else |
|
118 partialTaskData[requestID] = partialTaskData[requestID].concat(msg.data.chunk); |
|
119 break; |
|
120 case "chunkedEnd": |
|
121 break; |
|
122 case "parseRawProfile": |
|
123 parseRawProfile(requestID, msg.data.params, taskData); |
|
124 break; |
|
125 case "updateFilters": |
|
126 updateFilters(requestID, taskData.profileID, taskData.filters); |
|
127 break; |
|
128 case "updateViewOptions": |
|
129 updateViewOptions(requestID, taskData.profileID, taskData.options); |
|
130 break; |
|
131 case "getSerializedProfile": |
|
132 getSerializedProfile(requestID, taskData.profileID, taskData.complete); |
|
133 break; |
|
134 case "calculateHistogramData": |
|
135 calculateHistogramData(requestID, taskData.profileID); |
|
136 break; |
|
137 case "calculateDiagnosticItems": |
|
138 calculateDiagnosticItems(requestID, taskData.profileID, taskData.meta); |
|
139 break; |
|
140 default: |
|
141 sendError(requestID, "Unknown task " + task); |
|
142 break; |
|
143 } |
|
144 PROFILERLOG("Complete task: " + task); |
|
145 } catch (e) { |
|
146 PROFILERERROR("Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")"); |
|
147 sendError(requestID, "Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")"); |
|
148 } |
|
149 } |
|
150 |
|
151 function sendError(requestID, error) { |
|
152 // support sendError(msg) |
|
153 if (error == null) { |
|
154 error = requestID; |
|
155 requestID = null; |
|
156 } |
|
157 |
|
158 self.postMessage({ |
|
159 requestID: requestID, |
|
160 type: "error", |
|
161 error: error, |
|
162 log: gLogLines |
|
163 }); |
|
164 } |
|
165 |
|
166 function sendProgress(requestID, progress) { |
|
167 self.postMessage({ |
|
168 requestID: requestID, |
|
169 type: "progress", |
|
170 progress: progress |
|
171 }); |
|
172 } |
|
173 |
|
174 function sendFinished(requestID, result) { |
|
175 self.postMessage({ |
|
176 requestID: requestID, |
|
177 type: "finished", |
|
178 result: result, |
|
179 log: gLogLines |
|
180 }); |
|
181 } |
|
182 |
|
183 function bucketsBySplittingArray(array, maxCostPerBucket, costOfElementCallback) { |
|
184 var buckets = []; |
|
185 var currentBucket = []; |
|
186 var currentBucketCost = 0; |
|
187 for (var i = 0; i < array.length; i++) { |
|
188 var element = array[i]; |
|
189 var costOfCurrentElement = costOfElementCallback ? costOfElementCallback(element) : 1; |
|
190 if (currentBucketCost + costOfCurrentElement > maxCostPerBucket) { |
|
191 buckets.push(currentBucket); |
|
192 currentBucket = []; |
|
193 currentBucketCost = 0; |
|
194 } |
|
195 currentBucket.push(element); |
|
196 currentBucketCost += costOfCurrentElement; |
|
197 } |
|
198 buckets.push(currentBucket); |
|
199 return buckets; |
|
200 } |
|
201 |
|
202 function sendFinishedInChunks(requestID, result, maxChunkCost, costOfElementCallback) { |
|
203 if (result.length === undefined || result.slice === undefined) |
|
204 throw new Error("Can't slice result into chunks"); |
|
205 self.postMessage({ |
|
206 requestID: requestID, |
|
207 type: "finishedStart" |
|
208 }); |
|
209 var chunks = bucketsBySplittingArray(result, maxChunkCost, costOfElementCallback); |
|
210 for (var i = 0; i < chunks.length; i++) { |
|
211 self.postMessage({ |
|
212 requestID: requestID, |
|
213 type: "finishedChunk", |
|
214 chunk: chunks[i] |
|
215 }); |
|
216 } |
|
217 self.postMessage({ |
|
218 requestID: requestID, |
|
219 type: "finishedEnd", |
|
220 log: gLogLines |
|
221 }); |
|
222 } |
|
223 |
|
224 function makeSample(frames, extraInfo) { |
|
225 return { |
|
226 frames: frames, |
|
227 extraInfo: extraInfo |
|
228 }; |
|
229 } |
|
230 |
|
231 function cloneSample(sample) { |
|
232 return makeSample(sample.frames.slice(0), sample.extraInfo); |
|
233 } |
|
234 |
|
235 function parseRawProfile(requestID, params, rawProfile) { |
|
236 var progressReporter = new ProgressReporter(); |
|
237 progressReporter.addListener(function (r) { |
|
238 sendProgress(requestID, r.getProgress()); |
|
239 }); |
|
240 progressReporter.begin("Parsing..."); |
|
241 |
|
242 var symbolicationTable = {}; |
|
243 var symbols = []; |
|
244 var symbolIndices = {}; |
|
245 var resources = {}; |
|
246 var functions = []; |
|
247 var functionIndices = {}; |
|
248 var samples = []; |
|
249 var meta = {}; |
|
250 var armIncludePCIndex = {}; |
|
251 |
|
252 if (rawProfile == null) { |
|
253 throw "rawProfile is null"; |
|
254 } |
|
255 |
|
256 if (typeof rawProfile == "string" && rawProfile[0] == "{") { |
|
257 // rawProfile is a JSON string. |
|
258 rawProfile = JSON.parse(rawProfile); |
|
259 if (rawProfile === null) { |
|
260 throw "rawProfile couldn't not successfully be parsed using JSON.parse. Make sure that the profile is a valid JSON encoding."; |
|
261 } |
|
262 } |
|
263 |
|
264 if (rawProfile.profileJSON && !rawProfile.profileJSON.meta && rawProfile.meta) { |
|
265 rawProfile.profileJSON.meta = rawProfile.meta; |
|
266 } |
|
267 |
|
268 if (typeof rawProfile == "object") { |
|
269 switch (rawProfile.format || null) { |
|
270 case "profileStringWithSymbolicationTable,1": |
|
271 symbolicationTable = rawProfile.symbolicationTable; |
|
272 parseProfileString(rawProfile.profileString); |
|
273 break; |
|
274 case "profileJSONWithSymbolicationTable,1": |
|
275 symbolicationTable = rawProfile.symbolicationTable; |
|
276 parseProfileJSON(rawProfile.profileJSON); |
|
277 break; |
|
278 default: |
|
279 parseProfileJSON(rawProfile); |
|
280 } |
|
281 } else { |
|
282 parseProfileString(rawProfile); |
|
283 } |
|
284 |
|
285 if (params.profileId) { |
|
286 meta.profileId = params.profileId; |
|
287 } |
|
288 |
|
289 function cleanFunctionName(functionName) { |
|
290 var ignoredPrefix = "non-virtual thunk to "; |
|
291 if (functionName.startsWith(ignoredPrefix)) |
|
292 return functionName.substr(ignoredPrefix.length); |
|
293 return functionName; |
|
294 } |
|
295 |
|
296 function resourceNameForAddon(addon) { |
|
297 if (!addon) |
|
298 return ""; |
|
299 |
|
300 var iconHTML = ""; |
|
301 if (addon.iconURL) |
|
302 iconHTML = "<img src=\"" + addon.iconURL + "\" style='width:12px; height:12px;'> " |
|
303 return iconHTML + " " + (/@jetpack$/.exec(addon.id) ? "Jetpack: " : "") + addon.name; |
|
304 } |
|
305 |
|
306 function addonWithID(addonID) { |
|
307 return firstMatch(meta.addons, function addonHasID(addon) { |
|
308 return addon.id.toLowerCase() == addonID.toLowerCase(); |
|
309 }) |
|
310 } |
|
311 |
|
312 function resourceNameForAddonWithID(addonID) { |
|
313 return resourceNameForAddon(addonWithID(addonID)); |
|
314 } |
|
315 |
|
316 function findAddonForChromeURIHost(host) { |
|
317 return firstMatch(meta.addons, function addonUsesChromeURIHost(addon) { |
|
318 return addon.chromeURIHosts && addon.chromeURIHosts.indexOf(host) != -1; |
|
319 }); |
|
320 } |
|
321 |
|
322 function ensureResource(name, resourceDescription) { |
|
323 if (!(name in resources)) { |
|
324 resources[name] = resourceDescription; |
|
325 } |
|
326 return name; |
|
327 } |
|
328 |
|
329 function resourceNameFromLibrary(library) { |
|
330 return ensureResource("lib_" + library, { |
|
331 type: "library", |
|
332 name: library |
|
333 }); |
|
334 } |
|
335 |
|
336 function getAddonForScriptURI(url, host) { |
|
337 if (!meta || !meta.addons) |
|
338 return null; |
|
339 |
|
340 if (url.startsWith("resource:") && endsWith(host, "-at-jetpack")) { |
|
341 // Assume this is a jetpack url |
|
342 var jetpackID = host.substring(0, host.length - 11) + "@jetpack"; |
|
343 return addonWithID(jetpackID); |
|
344 } |
|
345 |
|
346 if (url.startsWith("file:///") && url.indexOf("/extensions/") != -1) { |
|
347 var unpackedAddonNameMatch = /\/extensions\/(.*?)\//.exec(url); |
|
348 if (unpackedAddonNameMatch) |
|
349 return addonWithID(decodeURIComponent(unpackedAddonNameMatch[1])); |
|
350 return null; |
|
351 } |
|
352 |
|
353 if (url.startsWith("jar:file:///") && url.indexOf("/extensions/") != -1) { |
|
354 var packedAddonNameMatch = /\/extensions\/(.*?).xpi/.exec(url); |
|
355 if (packedAddonNameMatch) |
|
356 return addonWithID(decodeURIComponent(packedAddonNameMatch[1])); |
|
357 return null; |
|
358 } |
|
359 |
|
360 if (url.startsWith("chrome://")) { |
|
361 var chromeURIMatch = /chrome\:\/\/(.*?)\//.exec(url); |
|
362 if (chromeURIMatch) |
|
363 return findAddonForChromeURIHost(chromeURIMatch[1]); |
|
364 return null; |
|
365 } |
|
366 |
|
367 return null; |
|
368 } |
|
369 |
|
370 function resourceNameFromURI(url) { |
|
371 if (!url) |
|
372 return ensureResource("unknown", {type: "unknown", name: "<unknown>"}); |
|
373 |
|
374 var match = /^(.*):\/\/(.*?)\//.exec(url); |
|
375 |
|
376 if (!match) { |
|
377 // Can this happen? If so, we should change the regular expression above. |
|
378 return ensureResource("url_" + url, {type: "url", name: url}); |
|
379 } |
|
380 |
|
381 var urlRoot = match[0]; |
|
382 var protocol = match[1]; |
|
383 var host = match[2]; |
|
384 |
|
385 var addon = getAddonForScriptURI(url, host); |
|
386 if (addon) { |
|
387 return ensureResource("addon_" + addon.id, { |
|
388 type: "addon", |
|
389 name: addon.name, |
|
390 addonID: addon.id, |
|
391 icon: addon.iconURL |
|
392 }); |
|
393 } |
|
394 |
|
395 if (protocol.startsWith("http")) { |
|
396 return ensureResource("webhost_" + host, { |
|
397 type: "webhost", |
|
398 name: host, |
|
399 icon: urlRoot + "favicon.ico" |
|
400 }); |
|
401 } |
|
402 |
|
403 if (protocol.startsWith("file")) { |
|
404 return ensureResource("file_" + host, { |
|
405 type: "file", |
|
406 name: host |
|
407 }); |
|
408 } |
|
409 |
|
410 return ensureResource("otherhost_" + host, { |
|
411 type: "otherhost", |
|
412 name: host |
|
413 }); |
|
414 } |
|
415 |
|
416 function parseScriptFile(url) { |
|
417 var match = /([^\/]*)$/.exec(url); |
|
418 if (match && match[1]) |
|
419 return match[1]; |
|
420 |
|
421 return url; |
|
422 } |
|
423 |
|
424 // JS File information sometimes comes with multiple URIs which are chained |
|
425 // with " -> ". We only want the last URI in this list. |
|
426 function getRealScriptURI(url) { |
|
427 if (url) { |
|
428 var urls = url.split(" -> "); |
|
429 return urls[urls.length - 1]; |
|
430 } |
|
431 return url; |
|
432 } |
|
433 |
|
434 function getFunctionInfo(fullName) { |
|
435 |
|
436 function getCPPFunctionInfo(fullName) { |
|
437 var match = |
|
438 /^(.*) \(in ([^\)]*)\) (\+ [0-9]+)$/.exec(fullName) || |
|
439 /^(.*) \(in ([^\)]*)\) (\(.*:.*\))$/.exec(fullName) || |
|
440 /^(.*) \(in ([^\)]*)\)$/.exec(fullName); |
|
441 |
|
442 if (!match) |
|
443 return null; |
|
444 |
|
445 return { |
|
446 functionName: cleanFunctionName(match[1]), |
|
447 libraryName: resourceNameFromLibrary(match[2]), |
|
448 lineInformation: match[3] || "", |
|
449 isRoot: false, |
|
450 isJSFrame: false |
|
451 }; |
|
452 } |
|
453 |
|
454 function getJSFunctionInfo(fullName) { |
|
455 var jsMatch = |
|
456 /^(.*) \((.*):([0-9]+)\)$/.exec(fullName) || |
|
457 /^()(.*):([0-9]+)$/.exec(fullName); |
|
458 |
|
459 if (!jsMatch) |
|
460 return null; |
|
461 |
|
462 var functionName = jsMatch[1] || "<Anonymous>"; |
|
463 var scriptURI = getRealScriptURI(jsMatch[2]); |
|
464 var lineNumber = jsMatch[3]; |
|
465 var scriptFile = parseScriptFile(scriptURI); |
|
466 var resourceName = resourceNameFromURI(scriptURI); |
|
467 |
|
468 return { |
|
469 functionName: functionName + "() @ " + scriptFile + ":" + lineNumber, |
|
470 libraryName: resourceName, |
|
471 lineInformation: "", |
|
472 isRoot: false, |
|
473 isJSFrame: true, |
|
474 scriptLocation: { |
|
475 scriptURI: scriptURI, |
|
476 lineInformation: lineNumber |
|
477 } |
|
478 }; |
|
479 } |
|
480 |
|
481 function getFallbackFunctionInfo(fullName) { |
|
482 return { |
|
483 functionName: cleanFunctionName(fullName), |
|
484 libraryName: "", |
|
485 lineInformation: "", |
|
486 isRoot: fullName == "(root)", |
|
487 isJSFrame: false |
|
488 }; |
|
489 } |
|
490 |
|
491 return getCPPFunctionInfo(fullName) || |
|
492 getJSFunctionInfo(fullName) || |
|
493 getFallbackFunctionInfo(fullName); |
|
494 } |
|
495 |
|
496 function indexForFunction(symbol, info) { |
|
497 var resolve = info.functionName + "__" + info.libraryName; |
|
498 if (resolve in functionIndices) |
|
499 return functionIndices[resolve]; |
|
500 var newIndex = functions.length; |
|
501 info.symbol = symbol; |
|
502 functions[newIndex] = info; |
|
503 functionIndices[resolve] = newIndex; |
|
504 return newIndex; |
|
505 } |
|
506 |
|
507 function parseSymbol(symbol) { |
|
508 var info = getFunctionInfo(symbol); |
|
509 //dump("Parse symbol: " + symbol + "\n"); |
|
510 return { |
|
511 symbolName: symbol, |
|
512 functionName: info.functionName, |
|
513 functionIndex: indexForFunction(symbol, info), |
|
514 lineInformation: info.lineInformation, |
|
515 isRoot: info.isRoot, |
|
516 isJSFrame: info.isJSFrame, |
|
517 scriptLocation: info.scriptLocation |
|
518 }; |
|
519 } |
|
520 |
|
521 function translatedSymbol(symbol) { |
|
522 return symbolicationTable[symbol] || symbol; |
|
523 } |
|
524 |
|
525 function indexForSymbol(symbol) { |
|
526 if (symbol in symbolIndices) |
|
527 return symbolIndices[symbol]; |
|
528 var newIndex = symbols.length; |
|
529 symbols[newIndex] = parseSymbol(translatedSymbol(symbol)); |
|
530 symbolIndices[symbol] = newIndex; |
|
531 return newIndex; |
|
532 } |
|
533 |
|
534 function clearRegExpLastMatch() { |
|
535 /./.exec(" "); |
|
536 } |
|
537 |
|
538 function shouldIncludeARMLRForPC(pcIndex) { |
|
539 if (pcIndex in armIncludePCIndex) |
|
540 return armIncludePCIndex[pcIndex]; |
|
541 |
|
542 var pcName = symbols[pcIndex].functionName; |
|
543 var include = sARMFunctionsWithValidLR.indexOf(pcName) != -1; |
|
544 armIncludePCIndex[pcIndex] = include; |
|
545 return include; |
|
546 } |
|
547 |
|
548 function parseProfileString(data) { |
|
549 var extraInfo = {}; |
|
550 var lines = data.split("\n"); |
|
551 var sample = null; |
|
552 for (var i = 0; i < lines.length; ++i) { |
|
553 var line = lines[i]; |
|
554 if (line.length < 2 || line[1] != '-') { |
|
555 // invalid line, ignore it |
|
556 continue; |
|
557 } |
|
558 var info = line.substring(2); |
|
559 switch (line[0]) { |
|
560 //case 'l': |
|
561 // // leaf name |
|
562 // if ("leafName" in extraInfo) { |
|
563 // extraInfo.leafName += ":" + info; |
|
564 // } else { |
|
565 // extraInfo.leafName = info; |
|
566 // } |
|
567 // break; |
|
568 case 'm': |
|
569 // marker |
|
570 if (!("marker" in extraInfo)) { |
|
571 extraInfo.marker = []; |
|
572 } |
|
573 extraInfo.marker.push(info); |
|
574 break; |
|
575 case 's': |
|
576 // sample |
|
577 var sampleName = info; |
|
578 sample = makeSample([indexForSymbol(sampleName)], extraInfo); |
|
579 samples.push(sample); |
|
580 extraInfo = {}; // reset the extra info for future rounds |
|
581 break; |
|
582 case 'c': |
|
583 case 'l': |
|
584 // continue sample |
|
585 if (sample) { // ignore the case where we see a 'c' before an 's' |
|
586 sample.frames.push(indexForSymbol(info)); |
|
587 } |
|
588 break; |
|
589 case 'L': |
|
590 // continue sample; this is an ARM LR record. Stick it before the |
|
591 // PC if it's one of the functions where we know LR is good. |
|
592 if (sample && sample.frames.length > 1) { |
|
593 var pcIndex = sample.frames[sample.frames.length - 1]; |
|
594 if (shouldIncludeARMLRForPC(pcIndex)) { |
|
595 sample.frames.splice(-1, 0, indexForSymbol(info)); |
|
596 } |
|
597 } |
|
598 break; |
|
599 case 't': |
|
600 // time |
|
601 if (sample) { |
|
602 sample.extraInfo["time"] = parseFloat(info); |
|
603 } |
|
604 break; |
|
605 case 'r': |
|
606 // responsiveness |
|
607 if (sample) { |
|
608 sample.extraInfo["responsiveness"] = parseFloat(info); |
|
609 } |
|
610 break; |
|
611 } |
|
612 progressReporter.setProgress((i + 1) / lines.length); |
|
613 } |
|
614 } |
|
615 |
|
616 function parseProfileJSON(profile) { |
|
617 // Thread 0 will always be the main thread of interest |
|
618 // TODO support all the thread in the profile |
|
619 var profileSamples = null; |
|
620 meta = profile.meta || {}; |
|
621 if (params.appendVideoCapture) { |
|
622 meta.videoCapture = { |
|
623 src: params.appendVideoCapture, |
|
624 }; |
|
625 } |
|
626 // Support older format that aren't thread aware |
|
627 if (profile.threads != null) { |
|
628 profileSamples = profile.threads[0].samples; |
|
629 } else { |
|
630 profileSamples = profile; |
|
631 } |
|
632 var rootSymbol = null; |
|
633 var insertCommonRoot = false; |
|
634 var frameStart = {}; |
|
635 meta.frameStart = frameStart; |
|
636 for (var j = 0; j < profileSamples.length; j++) { |
|
637 var sample = profileSamples[j]; |
|
638 var indicedFrames = []; |
|
639 if (!sample) { |
|
640 // This sample was filtered before saving |
|
641 samples.push(null); |
|
642 progressReporter.setProgress((j + 1) / profileSamples.length); |
|
643 continue; |
|
644 } |
|
645 for (var k = 0; sample.frames && k < sample.frames.length; k++) { |
|
646 var frame = sample.frames[k]; |
|
647 var pcIndex; |
|
648 if (frame.location !== undefined) { |
|
649 pcIndex = indexForSymbol(frame.location); |
|
650 } else { |
|
651 pcIndex = indexForSymbol(frame); |
|
652 } |
|
653 |
|
654 if (frame.lr !== undefined && shouldIncludeARMLRForPC(pcIndex)) { |
|
655 indicedFrames.push(indexForSymbol(frame.lr)); |
|
656 } |
|
657 |
|
658 indicedFrames.push(pcIndex); |
|
659 } |
|
660 if (indicedFrames.length >= 1) { |
|
661 if (rootSymbol && rootSymbol != indicedFrames[0]) { |
|
662 insertCommonRoot = true; |
|
663 } |
|
664 rootSymbol = rootSymbol || indicedFrames[0]; |
|
665 } |
|
666 if (sample.extraInfo == null) { |
|
667 sample.extraInfo = {}; |
|
668 } |
|
669 if (sample.responsiveness) { |
|
670 sample.extraInfo["responsiveness"] = sample.responsiveness; |
|
671 } |
|
672 if (sample.marker) { |
|
673 sample.extraInfo["marker"] = sample.marker; |
|
674 } |
|
675 if (sample.time) { |
|
676 sample.extraInfo["time"] = sample.time; |
|
677 } |
|
678 if (sample.frameNumber) { |
|
679 sample.extraInfo["frameNumber"] = sample.frameNumber; |
|
680 //dump("Got frame number: " + sample.frameNumber + "\n"); |
|
681 frameStart[sample.frameNumber] = samples.length; |
|
682 } |
|
683 samples.push(makeSample(indicedFrames, sample.extraInfo)); |
|
684 progressReporter.setProgress((j + 1) / profileSamples.length); |
|
685 } |
|
686 if (insertCommonRoot) { |
|
687 var rootIndex = indexForSymbol("(root)"); |
|
688 for (var i = 0; i < samples.length; i++) { |
|
689 var sample = samples[i]; |
|
690 if (!sample) continue; |
|
691 // If length == 0 then the sample was filtered when saving the profile |
|
692 if (sample.frames.length >= 1 && sample.frames[0] != rootIndex) |
|
693 sample.frames.unshift(rootIndex) |
|
694 } |
|
695 } |
|
696 } |
|
697 |
|
698 progressReporter.finish(); |
|
699 // Don't increment the profile ID now because (1) it's buggy |
|
700 // and (2) for now there's no point in storing each profile |
|
701 // here if we're storing them in the local storage. |
|
702 //var profileID = gNextProfileID++; |
|
703 var profileID = gNextProfileID; |
|
704 gProfiles[profileID] = JSON.parse(JSON.stringify({ |
|
705 meta: meta, |
|
706 symbols: symbols, |
|
707 functions: functions, |
|
708 resources: resources, |
|
709 allSamples: samples |
|
710 })); |
|
711 clearRegExpLastMatch(); |
|
712 sendFinished(requestID, { |
|
713 meta: meta, |
|
714 numSamples: samples.length, |
|
715 profileID: profileID, |
|
716 symbols: symbols, |
|
717 functions: functions, |
|
718 resources: resources |
|
719 }); |
|
720 } |
|
721 |
|
722 function getSerializedProfile(requestID, profileID, complete) { |
|
723 var profile = gProfiles[profileID]; |
|
724 var symbolicationTable = {}; |
|
725 if (complete || !profile.filterSettings.mergeFunctions) { |
|
726 for (var symbolIndex in profile.symbols) { |
|
727 symbolicationTable[symbolIndex] = profile.symbols[symbolIndex].symbolName; |
|
728 } |
|
729 } else { |
|
730 for (var functionIndex in profile.functions) { |
|
731 var f = profile.functions[functionIndex]; |
|
732 symbolicationTable[functionIndex] = f.symbol; |
|
733 } |
|
734 } |
|
735 var serializedProfile = JSON.stringify({ |
|
736 format: "profileJSONWithSymbolicationTable,1", |
|
737 meta: profile.meta, |
|
738 profileJSON: complete ? profile.allSamples : profile.filteredSamples, |
|
739 symbolicationTable: symbolicationTable |
|
740 }); |
|
741 sendFinished(requestID, serializedProfile); |
|
742 } |
|
743 |
|
744 function TreeNode(name, parent, startCount) { |
|
745 this.name = name; |
|
746 this.children = []; |
|
747 this.counter = startCount; |
|
748 this.parent = parent; |
|
749 } |
|
750 TreeNode.prototype.getDepth = function TreeNode__getDepth() { |
|
751 if (this.parent) |
|
752 return this.parent.getDepth() + 1; |
|
753 return 0; |
|
754 }; |
|
755 TreeNode.prototype.findChild = function TreeNode_findChild(name) { |
|
756 for (var i = 0; i < this.children.length; i++) { |
|
757 var child = this.children[i]; |
|
758 if (child.name == name) |
|
759 return child; |
|
760 } |
|
761 return null; |
|
762 } |
|
763 // path is an array of strings which is matched to our nodes' names. |
|
764 // Try to walk path in our own tree and return the last matching node. The |
|
765 // length of the match can be calculated by the caller by comparing the |
|
766 // returned node's depth with the depth of the path's start node. |
|
767 TreeNode.prototype.followPath = function TreeNode_followPath(path) { |
|
768 if (path.length == 0) |
|
769 return this; |
|
770 |
|
771 var matchingChild = this.findChild(path[0]); |
|
772 if (!matchingChild) |
|
773 return this; |
|
774 |
|
775 return matchingChild.followPath(path.slice(1)); |
|
776 }; |
|
777 TreeNode.prototype.incrementCountersInParentChain = function TreeNode_incrementCountersInParentChain() { |
|
778 this.counter++; |
|
779 if (this.parent) |
|
780 this.parent.incrementCountersInParentChain(); |
|
781 }; |
|
782 |
|
783 function convertToCallTree(samples, isReverse) { |
|
784 function areSamplesMultiroot(samples) { |
|
785 var previousRoot; |
|
786 for (var i = 0; i < samples.length; ++i) { |
|
787 if (!previousRoot) { |
|
788 previousRoot = samples[i].frames[0]; |
|
789 continue; |
|
790 } |
|
791 if (previousRoot != samples[i].frames[0]) { |
|
792 return true; |
|
793 } |
|
794 } |
|
795 return false; |
|
796 } |
|
797 samples = samples.filter(function noNullSamples(sample) { |
|
798 return sample != null; |
|
799 }); |
|
800 if (samples.length == 0) |
|
801 return new TreeNode("(empty)", null, 0); |
|
802 var firstRoot = null; |
|
803 for (var i = 0; i < samples.length; ++i) { |
|
804 firstRoot = samples[i].frames[0]; |
|
805 break; |
|
806 } |
|
807 if (firstRoot == null) { |
|
808 return new TreeNode("(all filtered)", null, 0); |
|
809 } |
|
810 var multiRoot = areSamplesMultiroot(samples); |
|
811 var treeRoot = new TreeNode((isReverse || multiRoot) ? "(total)" : firstRoot, null, 0); |
|
812 for (var i = 0; i < samples.length; ++i) { |
|
813 var sample = samples[i]; |
|
814 var callstack = sample.frames.slice(0); |
|
815 callstack.shift(); |
|
816 if (isReverse) |
|
817 callstack.reverse(); |
|
818 var deepestExistingNode = treeRoot.followPath(callstack); |
|
819 var remainingCallstack = callstack.slice(deepestExistingNode.getDepth()); |
|
820 deepestExistingNode.incrementCountersInParentChain(); |
|
821 var node = deepestExistingNode; |
|
822 for (var j = 0; j < remainingCallstack.length; ++j) { |
|
823 var frame = remainingCallstack[j]; |
|
824 var child = new TreeNode(frame, node, 1); |
|
825 node.children.push(child); |
|
826 node = child; |
|
827 } |
|
828 } |
|
829 return treeRoot; |
|
830 } |
|
831 |
|
832 function filterByJank(samples, filterThreshold) { |
|
833 return samples.map(function nullNonJank(sample) { |
|
834 if (!sample || |
|
835 !("responsiveness" in sample.extraInfo) || |
|
836 sample.extraInfo["responsiveness"] < filterThreshold) |
|
837 return null; |
|
838 return sample; |
|
839 }); |
|
840 } |
|
841 |
|
842 function filterBySymbol(samples, symbolOrFunctionIndex) { |
|
843 return samples.map(function filterSample(origSample) { |
|
844 if (!origSample) |
|
845 return null; |
|
846 var sample = cloneSample(origSample); |
|
847 for (var i = 0; i < sample.frames.length; i++) { |
|
848 if (symbolOrFunctionIndex == sample.frames[i]) { |
|
849 sample.frames = sample.frames.slice(i); |
|
850 return sample; |
|
851 } |
|
852 } |
|
853 return null; // no frame matched; filter out complete sample |
|
854 }); |
|
855 } |
|
856 |
|
857 function filterByCallstackPrefix(samples, symbols, functions, callstack, appliesToJS, useFunctions) { |
|
858 var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { |
|
859 return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); |
|
860 } : function isJSSymbolOrRoot(symbolIndex) { |
|
861 return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); |
|
862 }; |
|
863 return samples.map(function filterSample(sample) { |
|
864 if (!sample) |
|
865 return null; |
|
866 if (sample.frames.length < callstack.length) |
|
867 return null; |
|
868 for (var i = 0, j = 0; j < callstack.length; i++) { |
|
869 if (i >= sample.frames.length) |
|
870 return null; |
|
871 if (appliesToJS && !isJSFrameOrRoot(sample.frames[i])) |
|
872 continue; |
|
873 if (sample.frames[i] != callstack[j]) |
|
874 return null; |
|
875 j++; |
|
876 } |
|
877 return makeSample(sample.frames.slice(i - 1), sample.extraInfo); |
|
878 }); |
|
879 } |
|
880 |
|
881 function filterByCallstackPostfix(samples, symbols, functions, callstack, appliesToJS, useFunctions) { |
|
882 var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { |
|
883 return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); |
|
884 } : function isJSSymbolOrRoot(symbolIndex) { |
|
885 return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); |
|
886 }; |
|
887 return samples.map(function filterSample(sample) { |
|
888 if (!sample) |
|
889 return null; |
|
890 if (sample.frames.length < callstack.length) |
|
891 return null; |
|
892 for (var i = 0, j = 0; j < callstack.length; i++) { |
|
893 if (i >= sample.frames.length) |
|
894 return null; |
|
895 if (appliesToJS && !isJSFrameOrRoot(sample.frames[sample.frames.length - i - 1])) |
|
896 continue; |
|
897 if (sample.frames[sample.frames.length - i - 1] != callstack[j]) |
|
898 return null; |
|
899 j++; |
|
900 } |
|
901 var newFrames = sample.frames.slice(0, sample.frames.length - i + 1); |
|
902 return makeSample(newFrames, sample.extraInfo); |
|
903 }); |
|
904 } |
|
905 |
|
906 function chargeNonJSToCallers(samples, symbols, functions, useFunctions) { |
|
907 var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { |
|
908 return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); |
|
909 } : function isJSSymbolOrRoot(symbolIndex) { |
|
910 return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); |
|
911 }; |
|
912 samples = samples.slice(0); |
|
913 for (var i = 0; i < samples.length; ++i) { |
|
914 var sample = samples[i]; |
|
915 if (!sample) |
|
916 continue; |
|
917 var newFrames = sample.frames.filter(isJSFrameOrRoot); |
|
918 if (!newFrames.length) { |
|
919 samples[i] = null; |
|
920 } else { |
|
921 samples[i].frames = newFrames; |
|
922 } |
|
923 } |
|
924 return samples; |
|
925 } |
|
926 |
|
927 function filterByName(samples, symbols, functions, filterName, useFunctions) { |
|
928 function getSymbolOrFunctionName(index, useFunctions) { |
|
929 if (useFunctions) { |
|
930 if (!(index in functions)) |
|
931 return ""; |
|
932 return functions[index].functionName; |
|
933 } |
|
934 if (!(index in symbols)) |
|
935 return ""; |
|
936 return symbols[index].symbolName; |
|
937 } |
|
938 function getLibraryName(index, useFunctions) { |
|
939 if (useFunctions) { |
|
940 if (!(index in functions)) |
|
941 return ""; |
|
942 return functions[index].libraryName; |
|
943 } |
|
944 if (!(index in symbols)) |
|
945 return ""; |
|
946 return symbols[index].libraryName; |
|
947 } |
|
948 samples = samples.slice(0); |
|
949 filterName = filterName.toLowerCase(); |
|
950 calltrace_it: for (var i = 0; i < samples.length; ++i) { |
|
951 var sample = samples[i]; |
|
952 if (!sample) |
|
953 continue; |
|
954 var callstack = sample.frames; |
|
955 for (var j = 0; j < callstack.length; ++j) { |
|
956 var symbolOrFunctionName = getSymbolOrFunctionName(callstack[j], useFunctions); |
|
957 var libraryName = getLibraryName(callstack[j], useFunctions); |
|
958 if (symbolOrFunctionName.toLowerCase().indexOf(filterName) != -1 || |
|
959 libraryName.toLowerCase().indexOf(filterName) != -1) { |
|
960 continue calltrace_it; |
|
961 } |
|
962 } |
|
963 samples[i] = null; |
|
964 } |
|
965 return samples; |
|
966 } |
|
967 |
|
968 function discardLineLevelInformation(samples, symbols, functions) { |
|
969 var data = samples; |
|
970 var filteredData = []; |
|
971 for (var i = 0; i < data.length; i++) { |
|
972 if (!data[i]) { |
|
973 filteredData.push(null); |
|
974 continue; |
|
975 } |
|
976 filteredData.push(cloneSample(data[i])); |
|
977 var frames = filteredData[i].frames; |
|
978 for (var j = 0; j < frames.length; j++) { |
|
979 if (!(frames[j] in symbols)) |
|
980 continue; |
|
981 frames[j] = symbols[frames[j]].functionIndex; |
|
982 } |
|
983 } |
|
984 return filteredData; |
|
985 } |
|
986 |
|
987 function mergeUnbranchedCallPaths(root) { |
|
988 var mergedNames = [root.name]; |
|
989 var node = root; |
|
990 while (node.children.length == 1 && node.counter == node.children[0].counter) { |
|
991 node = node.children[0]; |
|
992 mergedNames.push(node.name); |
|
993 } |
|
994 if (node != root) { |
|
995 // Merge path from root to node into root. |
|
996 root.children = node.children; |
|
997 root.mergedNames = mergedNames; |
|
998 //root.name = clipText(root.name, 50) + " to " + this._clipText(node.name, 50); |
|
999 } |
|
1000 for (var i = 0; i < root.children.length; i++) { |
|
1001 mergeUnbranchedCallPaths(root.children[i]); |
|
1002 } |
|
1003 } |
|
1004 |
|
1005 function FocusedFrameSampleFilter(focusedSymbol) { |
|
1006 this._focusedSymbol = focusedSymbol; |
|
1007 } |
|
1008 FocusedFrameSampleFilter.prototype = { |
|
1009 filter: function FocusedFrameSampleFilter_filter(samples, symbols, functions, useFunctions) { |
|
1010 return filterBySymbol(samples, this._focusedSymbol); |
|
1011 } |
|
1012 }; |
|
1013 |
|
1014 function FocusedCallstackPrefixSampleFilter(focusedCallstack, appliesToJS) { |
|
1015 this._focusedCallstackPrefix = focusedCallstack; |
|
1016 this._appliesToJS = appliesToJS; |
|
1017 } |
|
1018 FocusedCallstackPrefixSampleFilter.prototype = { |
|
1019 filter: function FocusedCallstackPrefixSampleFilter_filter(samples, symbols, functions, useFunctions) { |
|
1020 return filterByCallstackPrefix(samples, symbols, functions, this._focusedCallstackPrefix, this._appliesToJS, useFunctions); |
|
1021 } |
|
1022 }; |
|
1023 |
|
1024 function FocusedCallstackPostfixSampleFilter(focusedCallstack, appliesToJS) { |
|
1025 this._focusedCallstackPostfix = focusedCallstack; |
|
1026 this._appliesToJS = appliesToJS; |
|
1027 } |
|
1028 FocusedCallstackPostfixSampleFilter.prototype = { |
|
1029 filter: function FocusedCallstackPostfixSampleFilter_filter(samples, symbols, functions, useFunctions) { |
|
1030 return filterByCallstackPostfix(samples, symbols, functions, this._focusedCallstackPostfix, this._appliesToJS, useFunctions); |
|
1031 } |
|
1032 }; |
|
1033 |
|
1034 function RangeSampleFilter(start, end) { |
|
1035 this._start = start; |
|
1036 this._end = end; |
|
1037 } |
|
1038 RangeSampleFilter.prototype = { |
|
1039 filter: function RangeSampleFilter_filter(samples, symbols, functions) { |
|
1040 return samples.slice(this._start, this._end); |
|
1041 } |
|
1042 } |
|
1043 |
|
1044 function unserializeSampleFilters(filters) { |
|
1045 return filters.map(function (filter) { |
|
1046 switch (filter.type) { |
|
1047 case "FocusedFrameSampleFilter": |
|
1048 return new FocusedFrameSampleFilter(filter.focusedSymbol); |
|
1049 case "FocusedCallstackPrefixSampleFilter": |
|
1050 return new FocusedCallstackPrefixSampleFilter(filter.focusedCallstack, filter.appliesToJS); |
|
1051 case "FocusedCallstackPostfixSampleFilter": |
|
1052 return new FocusedCallstackPostfixSampleFilter(filter.focusedCallstack, filter.appliesToJS); |
|
1053 case "RangeSampleFilter": |
|
1054 return new RangeSampleFilter(filter.start, filter.end); |
|
1055 case "PluginView": |
|
1056 return null; |
|
1057 default: |
|
1058 throw new Error("Unknown filter"); |
|
1059 } |
|
1060 }) |
|
1061 } |
|
1062 |
|
1063 var gJankThreshold = 50 /* ms */; |
|
1064 |
|
1065 function updateFilters(requestID, profileID, filters) { |
|
1066 var profile = gProfiles[profileID]; |
|
1067 var samples = profile.allSamples; |
|
1068 var symbols = profile.symbols; |
|
1069 var functions = profile.functions; |
|
1070 |
|
1071 if (filters.mergeFunctions) { |
|
1072 samples = discardLineLevelInformation(samples, symbols, functions); |
|
1073 } |
|
1074 if (filters.nameFilter) { |
|
1075 try { |
|
1076 samples = filterByName(samples, symbols, functions, filters.nameFilter, filters.mergeFunctions); |
|
1077 } catch (e) { |
|
1078 dump("Could not filer by name: " + e + "\n"); |
|
1079 } |
|
1080 } |
|
1081 samples = unserializeSampleFilters(filters.sampleFilters).reduce(function (filteredSamples, currentFilter) { |
|
1082 if (currentFilter===null) return filteredSamples; |
|
1083 return currentFilter.filter(filteredSamples, symbols, functions, filters.mergeFunctions); |
|
1084 }, samples); |
|
1085 if (filters.jankOnly) { |
|
1086 samples = filterByJank(samples, gJankThreshold); |
|
1087 } |
|
1088 if (filters.javascriptOnly) { |
|
1089 samples = chargeNonJSToCallers(samples, symbols, functions, filters.mergeFunctions); |
|
1090 } |
|
1091 |
|
1092 gProfiles[profileID].filterSettings = filters; |
|
1093 gProfiles[profileID].filteredSamples = samples; |
|
1094 sendFinishedInChunks(requestID, samples, 40000, |
|
1095 function (sample) { return sample ? sample.frames.length : 1; }); |
|
1096 } |
|
1097 |
|
1098 function updateViewOptions(requestID, profileID, options) { |
|
1099 var profile = gProfiles[profileID]; |
|
1100 var samples = profile.filteredSamples; |
|
1101 var symbols = profile.symbols; |
|
1102 var functions = profile.functions; |
|
1103 |
|
1104 var treeData = convertToCallTree(samples, options.invertCallstack); |
|
1105 if (options.mergeUnbranched) |
|
1106 mergeUnbranchedCallPaths(treeData); |
|
1107 sendFinished(requestID, treeData); |
|
1108 } |
|
1109 |
|
1110 // The responsiveness threshold (in ms) after which the sample shuold become |
|
1111 // completely red in the histogram. |
|
1112 var kDelayUntilWorstResponsiveness = 1000; |
|
1113 |
|
1114 function calculateHistogramData(requestID, profileID) { |
|
1115 |
|
1116 function getStepColor(step) { |
|
1117 if (step.extraInfo && "responsiveness" in step.extraInfo) { |
|
1118 var res = step.extraInfo.responsiveness; |
|
1119 var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness)); |
|
1120 return "rgb(" + redComponent + ",0,0)"; |
|
1121 } |
|
1122 |
|
1123 return "rgb(0,0,0)"; |
|
1124 } |
|
1125 |
|
1126 var profile = gProfiles[profileID]; |
|
1127 var data = profile.filteredSamples; |
|
1128 var histogramData = []; |
|
1129 var maxHeight = 0; |
|
1130 for (var i = 0; i < data.length; ++i) { |
|
1131 if (!data[i]) |
|
1132 continue; |
|
1133 var value = data[i].frames.length; |
|
1134 if (maxHeight < value) |
|
1135 maxHeight = value; |
|
1136 } |
|
1137 maxHeight += 1; |
|
1138 var nextX = 0; |
|
1139 // The number of data items per histogramData rects. |
|
1140 // Except when seperated by a marker. |
|
1141 // This is used to cut down the number of rects, since |
|
1142 // there's no point in having more rects then pixels |
|
1143 var samplesPerStep = Math.max(1, Math.floor(data.length / 2000)); |
|
1144 var frameStart = {}; |
|
1145 for (var i = 0; i < data.length; i++) { |
|
1146 var step = data[i]; |
|
1147 if (!step) { |
|
1148 // Add a gap for the sample that was filtered out. |
|
1149 nextX += 1 / samplesPerStep; |
|
1150 continue; |
|
1151 } |
|
1152 nextX = Math.ceil(nextX); |
|
1153 var value = step.frames.length / maxHeight; |
|
1154 var frames = step.frames; |
|
1155 var currHistogramData = histogramData[histogramData.length-1]; |
|
1156 if (step.extraInfo && "marker" in step.extraInfo) { |
|
1157 // A new marker boundary has been discovered. |
|
1158 histogramData.push({ |
|
1159 frames: "marker", |
|
1160 x: nextX, |
|
1161 width: 2, |
|
1162 value: 1, |
|
1163 marker: step.extraInfo.marker, |
|
1164 color: "fuchsia" |
|
1165 }); |
|
1166 nextX += 2; |
|
1167 histogramData.push({ |
|
1168 frames: [step.frames], |
|
1169 x: nextX, |
|
1170 width: 1, |
|
1171 value: value, |
|
1172 color: getStepColor(step), |
|
1173 }); |
|
1174 nextX += 1; |
|
1175 } else if (currHistogramData != null && |
|
1176 currHistogramData.frames.length < samplesPerStep && |
|
1177 !(step.extraInfo && "frameNumber" in step.extraInfo)) { |
|
1178 currHistogramData.frames.push(step.frames); |
|
1179 // When merging data items take the average: |
|
1180 currHistogramData.value = |
|
1181 (currHistogramData.value * (currHistogramData.frames.length - 1) + value) / |
|
1182 currHistogramData.frames.length; |
|
1183 // Merge the colors? For now we keep the first color set. |
|
1184 } else { |
|
1185 // A new name boundary has been discovered. |
|
1186 currHistogramData = { |
|
1187 frames: [step.frames], |
|
1188 x: nextX, |
|
1189 width: 1, |
|
1190 value: value, |
|
1191 color: getStepColor(step), |
|
1192 }; |
|
1193 if (step.extraInfo && "frameNumber" in step.extraInfo) { |
|
1194 currHistogramData.frameNumber = step.extraInfo.frameNumber; |
|
1195 frameStart[step.extraInfo.frameNumber] = histogramData.length; |
|
1196 } |
|
1197 histogramData.push(currHistogramData); |
|
1198 nextX += 1; |
|
1199 } |
|
1200 } |
|
1201 sendFinished(requestID, { histogramData: histogramData, frameStart: frameStart, widthSum: Math.ceil(nextX) }); |
|
1202 } |
|
1203 |
|
1204 var diagnosticList = [ |
|
1205 // *************** Known bugs first (highest priority) |
|
1206 { |
|
1207 image: "io.png", |
|
1208 title: "Main Thread IO - Bug 765135 - TISCreateInputSourceList", |
|
1209 check: function(frames, symbols, meta) { |
|
1210 |
|
1211 if (!stepContains('TISCreateInputSourceList', frames, symbols)) |
|
1212 return false; |
|
1213 |
|
1214 return stepContains('__getdirentries64', frames, symbols) |
|
1215 || stepContains('__read', frames, symbols) |
|
1216 || stepContains('__open', frames, symbols) |
|
1217 || stepContains('stat$INODE64', frames, symbols) |
|
1218 ; |
|
1219 }, |
|
1220 }, |
|
1221 |
|
1222 { |
|
1223 image: "js.png", |
|
1224 title: "Bug 772916 - Gradients are slow on mobile", |
|
1225 bugNumber: "772916", |
|
1226 check: function(frames, symbols, meta) { |
|
1227 |
|
1228 return stepContains('PaintGradient', frames, symbols) |
|
1229 && stepContains('ClientTiledLayerBuffer::PaintThebesSingleBufferDraw', frames, symbols) |
|
1230 ; |
|
1231 }, |
|
1232 }, |
|
1233 { |
|
1234 image: "cache.png", |
|
1235 title: "Bug 717761 - Main thread can be blocked by IO on the cache thread", |
|
1236 bugNumber: "717761", |
|
1237 check: function(frames, symbols, meta) { |
|
1238 |
|
1239 return stepContains('nsCacheEntryDescriptor::GetStoragePolicy', frames, symbols) |
|
1240 ; |
|
1241 }, |
|
1242 }, |
|
1243 { |
|
1244 image: "js.png", |
|
1245 title: "Web Content Shutdown Notification", |
|
1246 check: function(frames, symbols, meta) { |
|
1247 |
|
1248 return stepContains('nsAppStartup::Quit', frames, symbols) |
|
1249 && stepContains('nsDocShell::FirePageHideNotification', frames, symbols) |
|
1250 ; |
|
1251 }, |
|
1252 }, |
|
1253 { |
|
1254 image: "js.png", |
|
1255 title: "Bug 789193 - AMI_startup() takes 200ms on startup", |
|
1256 bugNumber: "789193", |
|
1257 check: function(frames, symbols, meta) { |
|
1258 |
|
1259 return stepContains('AMI_startup()', frames, symbols) |
|
1260 ; |
|
1261 }, |
|
1262 }, |
|
1263 { |
|
1264 image: "js.png", |
|
1265 title: "Bug 818296 - [Shutdown] js::NukeCrossCompartmentWrappers takes up 300ms on shutdown", |
|
1266 bugNumber: "818296", |
|
1267 check: function(frames, symbols, meta) { |
|
1268 return stepContains('js::NukeCrossCompartmentWrappers', frames, symbols) |
|
1269 && (stepContains('WindowDestroyedEvent', frames, symbols) || stepContains('DoShutdown', frames, symbols)) |
|
1270 ; |
|
1271 }, |
|
1272 }, |
|
1273 { |
|
1274 image: "js.png", |
|
1275 title: "Bug 818274 - [Shutdown] Telemetry takes ~10ms on shutdown", |
|
1276 bugNumber: "818274", |
|
1277 check: function(frames, symbols, meta) { |
|
1278 return stepContains('TelemetryPing.js', frames, symbols) |
|
1279 ; |
|
1280 }, |
|
1281 }, |
|
1282 { |
|
1283 image: "plugin.png", |
|
1284 title: "Bug 818265 - [Shutdown] Plug-in shutdown takes ~90ms on shutdown", |
|
1285 bugNumber: "818265", |
|
1286 check: function(frames, symbols, meta) { |
|
1287 return stepContains('PluginInstanceParent::Destroy', frames, symbols) |
|
1288 ; |
|
1289 }, |
|
1290 }, |
|
1291 { |
|
1292 image: "snapshot.png", |
|
1293 title: "Bug 720575 - Make thumbnailing faster and/or asynchronous", |
|
1294 bugNumber: "720575", |
|
1295 check: function(frames, symbols, meta) { |
|
1296 return stepContains('Thumbnails_capture()', frames, symbols) |
|
1297 ; |
|
1298 }, |
|
1299 }, |
|
1300 |
|
1301 { |
|
1302 image: "js.png", |
|
1303 title: "Bug 789185 - LoginManagerStorage_mozStorage.init() takes 300ms on startup ", |
|
1304 bugNumber: "789185", |
|
1305 check: function(frames, symbols, meta) { |
|
1306 |
|
1307 return stepContains('LoginManagerStorage_mozStorage.prototype.init()', frames, symbols) |
|
1308 ; |
|
1309 }, |
|
1310 }, |
|
1311 |
|
1312 { |
|
1313 image: "js.png", |
|
1314 title: "JS - Bug 767070 - Text selection performance is bad on android", |
|
1315 bugNumber: "767070", |
|
1316 check: function(frames, symbols, meta) { |
|
1317 |
|
1318 if (!stepContains('FlushPendingNotifications', frames, symbols)) |
|
1319 return false; |
|
1320 |
|
1321 return stepContains('sh_', frames, symbols) |
|
1322 && stepContains('browser.js', frames, symbols) |
|
1323 ; |
|
1324 }, |
|
1325 }, |
|
1326 |
|
1327 { |
|
1328 image: "js.png", |
|
1329 title: "JS - Bug 765930 - Reader Mode: Optimize readability check", |
|
1330 bugNumber: "765930", |
|
1331 check: function(frames, symbols, meta) { |
|
1332 |
|
1333 return stepContains('Readability.js', frames, symbols) |
|
1334 ; |
|
1335 }, |
|
1336 }, |
|
1337 |
|
1338 // **************** General issues |
|
1339 { |
|
1340 image: "js.png", |
|
1341 title: "JS is triggering a sync reflow", |
|
1342 check: function(frames, symbols, meta) { |
|
1343 return symbolSequence(['js::RunScript','layout::DoReflow'], frames, symbols) || |
|
1344 symbolSequence(['js::RunScript','layout::Flush'], frames, symbols) |
|
1345 ; |
|
1346 }, |
|
1347 }, |
|
1348 |
|
1349 { |
|
1350 image: "gc.png", |
|
1351 title: "Garbage Collection Slice", |
|
1352 canMergeWithGC: false, |
|
1353 check: function(frames, symbols, meta, step) { |
|
1354 var slice = findGCSlice(frames, symbols, meta, step); |
|
1355 |
|
1356 if (slice) { |
|
1357 var gcEvent = findGCEvent(frames, symbols, meta, step); |
|
1358 //dump("found event matching diagnostic\n"); |
|
1359 //dump(JSON.stringify(gcEvent) + "\n"); |
|
1360 return true; |
|
1361 } |
|
1362 return false; |
|
1363 }, |
|
1364 details: function(frames, symbols, meta, step) { |
|
1365 var slice = findGCSlice(frames, symbols, meta, step); |
|
1366 if (slice) { |
|
1367 return "" + |
|
1368 "Reason: " + slice.reason + "\n" + |
|
1369 "Slice: " + slice.slice + "\n" + |
|
1370 "Pause: " + slice.pause + " ms"; |
|
1371 } |
|
1372 return null; |
|
1373 }, |
|
1374 onclickDetails: function(frames, symbols, meta, step) { |
|
1375 var gcEvent = findGCEvent(frames, symbols, meta, step); |
|
1376 if (gcEvent) { |
|
1377 return JSON.stringify(gcEvent); |
|
1378 } else { |
|
1379 return null; |
|
1380 } |
|
1381 }, |
|
1382 }, |
|
1383 { |
|
1384 image: "cc.png", |
|
1385 title: "Cycle Collect", |
|
1386 check: function(frames, symbols, meta, step) { |
|
1387 var ccEvent = findCCEvent(frames, symbols, meta, step); |
|
1388 |
|
1389 if (ccEvent) { |
|
1390 return true; |
|
1391 } |
|
1392 return false; |
|
1393 }, |
|
1394 details: function(frames, symbols, meta, step) { |
|
1395 var ccEvent = findCCEvent(frames, symbols, meta, step); |
|
1396 if (ccEvent) { |
|
1397 return "" + |
|
1398 "Duration: " + ccEvent.duration + " ms\n" + |
|
1399 "Suspected: " + ccEvent.suspected; |
|
1400 } |
|
1401 return null; |
|
1402 }, |
|
1403 onclickDetails: function(frames, symbols, meta, step) { |
|
1404 var ccEvent = findCCEvent(frames, symbols, meta, step); |
|
1405 if (ccEvent) { |
|
1406 return JSON.stringify(ccEvent); |
|
1407 } else { |
|
1408 return null; |
|
1409 } |
|
1410 }, |
|
1411 }, |
|
1412 { |
|
1413 image: "gc.png", |
|
1414 title: "Garbage Collection", |
|
1415 canMergeWithGC: false, |
|
1416 check: function(frames, symbols, meta) { |
|
1417 return stepContainsRegEx(/.*Collect.*Runtime.*Invocation.*/, frames, symbols) |
|
1418 || stepContains('GarbageCollectNow', frames, symbols) // Label |
|
1419 || stepContains('JS_GC(', frames, symbols) // Label |
|
1420 || stepContains('CycleCollect__', frames, symbols) // Label |
|
1421 ; |
|
1422 }, |
|
1423 }, |
|
1424 { |
|
1425 image: "cc.png", |
|
1426 title: "Cycle Collect", |
|
1427 check: function(frames, symbols, meta) { |
|
1428 return stepContains('nsCycleCollector::Collect', frames, symbols) |
|
1429 || stepContains('CycleCollect__', frames, symbols) // Label |
|
1430 || stepContains('nsCycleCollectorRunner::Collect', frames, symbols) // Label |
|
1431 ; |
|
1432 }, |
|
1433 }, |
|
1434 { |
|
1435 image: "plugin.png", |
|
1436 title: "Sync Plugin Constructor", |
|
1437 check: function(frames, symbols, meta) { |
|
1438 return stepContains('CallPPluginInstanceConstructor', frames, symbols) |
|
1439 || stepContains('CallPCrashReporterConstructor', frames, symbols) |
|
1440 || stepContains('PPluginModuleParent::CallNP_Initialize', frames, symbols) |
|
1441 || stepContains('GeckoChildProcessHost::SyncLaunch', frames, symbols) |
|
1442 ; |
|
1443 }, |
|
1444 }, |
|
1445 { |
|
1446 image: "text.png", |
|
1447 title: "Font Loading", |
|
1448 check: function(frames, symbols, meta) { |
|
1449 return stepContains('gfxFontGroup::BuildFontList', frames, symbols); |
|
1450 }, |
|
1451 }, |
|
1452 { |
|
1453 image: "io.png", |
|
1454 title: "Main Thread IO!", |
|
1455 check: function(frames, symbols, meta) { |
|
1456 return stepContains('__getdirentries64', frames, symbols) |
|
1457 || stepContains('__open', frames, symbols) |
|
1458 || stepContains('NtFlushBuffersFile', frames, symbols) |
|
1459 || stepContains('storage:::Statement::ExecuteStep', frames, symbols) |
|
1460 || stepContains('__unlink', frames, symbols) |
|
1461 || stepContains('fsync', frames, symbols) |
|
1462 || stepContains('stat$INODE64', frames, symbols) |
|
1463 ; |
|
1464 }, |
|
1465 }, |
|
1466 ]; |
|
1467 |
|
1468 function hasJSFrame(frames, symbols) { |
|
1469 for (var i = 0; i < frames.length; i++) { |
|
1470 if (symbols[frames[i]].isJSFrame === true) { |
|
1471 return true; |
|
1472 } |
|
1473 } |
|
1474 return false; |
|
1475 } |
|
1476 function findCCEvent(frames, symbols, meta, step) { |
|
1477 if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats) |
|
1478 return null; |
|
1479 |
|
1480 var time = step.extraInfo.time; |
|
1481 |
|
1482 for (var i = 0; i < meta.gcStats.ccEvents.length; i++) { |
|
1483 var ccEvent = meta.gcStats.ccEvents[i]; |
|
1484 if (ccEvent.start_timestamp <= time && ccEvent.end_timestamp >= time) { |
|
1485 //dump("JSON: " + js_beautify(JSON.stringify(ccEvent)) + "\n"); |
|
1486 return ccEvent; |
|
1487 } |
|
1488 } |
|
1489 |
|
1490 return null; |
|
1491 } |
|
1492 function findGCEvent(frames, symbols, meta, step) { |
|
1493 if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats) |
|
1494 return null; |
|
1495 |
|
1496 var time = step.extraInfo.time; |
|
1497 |
|
1498 for (var i = 0; i < meta.gcStats.gcEvents.length; i++) { |
|
1499 var gcEvent = meta.gcStats.gcEvents[i]; |
|
1500 if (!gcEvent.slices) |
|
1501 continue; |
|
1502 for (var j = 0; j < gcEvent.slices.length; j++) { |
|
1503 var slice = gcEvent.slices[j]; |
|
1504 if (slice.start_timestamp <= time && slice.end_timestamp >= time) { |
|
1505 return gcEvent; |
|
1506 } |
|
1507 } |
|
1508 } |
|
1509 |
|
1510 return null; |
|
1511 } |
|
1512 function findGCSlice(frames, symbols, meta, step) { |
|
1513 if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats) |
|
1514 return null; |
|
1515 |
|
1516 var time = step.extraInfo.time; |
|
1517 |
|
1518 for (var i = 0; i < meta.gcStats.gcEvents.length; i++) { |
|
1519 var gcEvent = meta.gcStats.gcEvents[i]; |
|
1520 if (!gcEvent.slices) |
|
1521 continue; |
|
1522 for (var j = 0; j < gcEvent.slices.length; j++) { |
|
1523 var slice = gcEvent.slices[j]; |
|
1524 if (slice.start_timestamp <= time && slice.end_timestamp >= time) { |
|
1525 return slice; |
|
1526 } |
|
1527 } |
|
1528 } |
|
1529 |
|
1530 return null; |
|
1531 } |
|
1532 function stepContains(substring, frames, symbols) { |
|
1533 for (var i = 0; frames && i < frames.length; i++) { |
|
1534 if (!(frames[i] in symbols)) |
|
1535 continue; |
|
1536 var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; |
|
1537 if (frameSym.indexOf(substring) != -1) { |
|
1538 return true; |
|
1539 } |
|
1540 } |
|
1541 return false; |
|
1542 } |
|
1543 function stepContainsRegEx(regex, frames, symbols) { |
|
1544 for (var i = 0; frames && i < frames.length; i++) { |
|
1545 if (!(frames[i] in symbols)) |
|
1546 continue; |
|
1547 var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; |
|
1548 if (regex.exec(frameSym)) { |
|
1549 return true; |
|
1550 } |
|
1551 } |
|
1552 return false; |
|
1553 } |
|
1554 function symbolSequence(symbolsOrder, frames, symbols) { |
|
1555 var symbolIndex = 0; |
|
1556 for (var i = 0; frames && i < frames.length; i++) { |
|
1557 if (!(frames[i] in symbols)) |
|
1558 continue; |
|
1559 var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; |
|
1560 var substring = symbolsOrder[symbolIndex]; |
|
1561 if (frameSym.indexOf(substring) != -1) { |
|
1562 symbolIndex++; |
|
1563 if (symbolIndex == symbolsOrder.length) { |
|
1564 return true; |
|
1565 } |
|
1566 } |
|
1567 } |
|
1568 return false; |
|
1569 } |
|
1570 function firstMatch(array, matchFunction) { |
|
1571 for (var i = 0; i < array.length; i++) { |
|
1572 if (matchFunction(array[i])) |
|
1573 return array[i]; |
|
1574 } |
|
1575 return undefined; |
|
1576 } |
|
1577 |
|
1578 function calculateDiagnosticItems(requestID, profileID, meta) { |
|
1579 /* |
|
1580 if (!histogramData || histogramData.length < 1) { |
|
1581 sendFinished(requestID, []); |
|
1582 return; |
|
1583 }*/ |
|
1584 |
|
1585 var profile = gProfiles[profileID]; |
|
1586 //var symbols = profile.symbols; |
|
1587 var symbols = profile.functions; |
|
1588 var data = profile.filteredSamples; |
|
1589 |
|
1590 var lastStep = data[data.length-1]; |
|
1591 var widthSum = data.length; |
|
1592 var pendingDiagnosticInfo = null; |
|
1593 |
|
1594 var diagnosticItems = []; |
|
1595 |
|
1596 function finishPendingDiagnostic(endX) { |
|
1597 if (!pendingDiagnosticInfo) |
|
1598 return; |
|
1599 |
|
1600 var diagnostic = pendingDiagnosticInfo.diagnostic; |
|
1601 var currDiagnostic = { |
|
1602 x: pendingDiagnosticInfo.x / widthSum, |
|
1603 width: (endX - pendingDiagnosticInfo.x) / widthSum, |
|
1604 imageFile: pendingDiagnosticInfo.diagnostic.image, |
|
1605 title: pendingDiagnosticInfo.diagnostic.title, |
|
1606 details: pendingDiagnosticInfo.details, |
|
1607 onclickDetails: pendingDiagnosticInfo.onclickDetails |
|
1608 }; |
|
1609 |
|
1610 if (!currDiagnostic.onclickDetails && diagnostic.bugNumber) { |
|
1611 currDiagnostic.onclickDetails = "bug " + diagnostic.bugNumber; |
|
1612 } |
|
1613 |
|
1614 diagnosticItems.push(currDiagnostic); |
|
1615 |
|
1616 pendingDiagnosticInfo = null; |
|
1617 } |
|
1618 |
|
1619 /* |
|
1620 dump("meta: " + meta.gcStats + "\n"); |
|
1621 if (meta && meta.gcStats) { |
|
1622 dump("GC Stats: " + JSON.stringify(meta.gcStats) + "\n"); |
|
1623 } |
|
1624 */ |
|
1625 |
|
1626 data.forEach(function diagnoseStep(step, x) { |
|
1627 if (step) { |
|
1628 var frames = step.frames; |
|
1629 |
|
1630 var diagnostic = firstMatch(diagnosticList, function (diagnostic) { |
|
1631 return diagnostic.check(frames, symbols, meta, step); |
|
1632 }); |
|
1633 } |
|
1634 |
|
1635 if (!diagnostic) { |
|
1636 finishPendingDiagnostic(x); |
|
1637 return; |
|
1638 } |
|
1639 |
|
1640 var details = diagnostic.details ? diagnostic.details(frames, symbols, meta, step) : null; |
|
1641 |
|
1642 if (pendingDiagnosticInfo) { |
|
1643 // We're already inside a diagnostic range. |
|
1644 if (diagnostic == pendingDiagnosticInfo.diagnostic && pendingDiagnosticInfo.details == details) { |
|
1645 // We're still inside the same diagnostic. |
|
1646 return; |
|
1647 } |
|
1648 |
|
1649 // We have left the old diagnostic and found a new one. Finish the old one. |
|
1650 finishPendingDiagnostic(x); |
|
1651 } |
|
1652 |
|
1653 pendingDiagnosticInfo = { |
|
1654 diagnostic: diagnostic, |
|
1655 x: x, |
|
1656 details: details, |
|
1657 onclickDetails: diagnostic.onclickDetails ? diagnostic.onclickDetails(frames, symbols, meta, step) : null |
|
1658 }; |
|
1659 }); |
|
1660 if (pendingDiagnosticInfo) |
|
1661 finishPendingDiagnostic(data.length); |
|
1662 |
|
1663 sendFinished(requestID, diagnosticItems); |
|
1664 } |