/* -*- Mode: js2; indent-tabs-mode: nil; js2-basic-offset: 2; -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ importScripts("ProgressReporter.js"); var gProfiles = []; var partialTaskData = {}; var gNextProfileID = 0; var gLogLines = []; var gDebugLog = false; var gDebugTrace = false; // Use for verbose tracing, otherwise use log function PROFILDERTRACE(msg) { if (gDebugTrace) PROFILERLOG(msg); } function PROFILERLOG(msg) { if (gDebugLog) { msg = "Cleo: " + msg; //if (window.dump) // window.dump(msg + "\n"); } } function PROFILERERROR(msg) { msg = "Cleo: " + msg; //if (window.dump) // window.dump(msg + "\n"); } // http://stackoverflow.com/a/2548133 function endsWith(str, suffix) { return str.indexOf(suffix, this.length - suffix.length) !== -1; }; // https://bugzilla.mozilla.org/show_bug.cgi?id=728780 if (!String.prototype.startsWith) { String.prototype.startsWith = function(s) { return this.lastIndexOf(s, 0) === 0; } } // functions for which lr is unconditionally valid. These are // largely going to be atomics and other similar functions // that don't touch lr. This is currently populated with // some functions from bionic, largely via manual inspection // of the assembly in e.g. // http://androidxref.com/source/xref/bionic/libc/arch-arm/syscalls/ var sARMFunctionsWithValidLR = [ "__atomic_dec", "__atomic_inc", "__atomic_cmpxchg", "__atomic_swap", "__atomic_dec", "__atomic_inc", "__atomic_cmpxchg", "__atomic_swap", "__futex_syscall3", "__futex_wait", "__futex_wake", "__futex_syscall3", "__futex_wait", "__futex_wake", "__futex_syscall4", "__ioctl", "__brk", "__wait4", "epoll_wait", "fsync", "futex", "nanosleep", "pause", "sched_yield", "syscall" ]; function log() { var z = []; for (var i = 0; i < arguments.length; ++i) z.push(arguments[i]); gLogLines.push(z.join(" ")); } self.onmessage = function (msg) { try { var requestID = msg.data.requestID; var task = msg.data.task; var taskData = msg.data.taskData; if (!taskData && (["chunkedStart", "chunkedChunk", "chunkedEnd"].indexOf(task) == -1)) { taskData = partialTaskData[requestID]; delete partialTaskData[requestID]; } PROFILERLOG("Start task: " + task); gLogLines = []; switch (task) { case "initWorker": gDebugLog = taskData.debugLog; gDebugTrace = taskData.debugTrace; PROFILERLOG("Init logging in parserWorker"); return; case "chunkedStart": partialTaskData[requestID] = null; break; case "chunkedChunk": if (partialTaskData[requestID] === null) partialTaskData[requestID] = msg.data.chunk; else partialTaskData[requestID] = partialTaskData[requestID].concat(msg.data.chunk); break; case "chunkedEnd": break; case "parseRawProfile": parseRawProfile(requestID, msg.data.params, taskData); break; case "updateFilters": updateFilters(requestID, taskData.profileID, taskData.filters); break; case "updateViewOptions": updateViewOptions(requestID, taskData.profileID, taskData.options); break; case "getSerializedProfile": getSerializedProfile(requestID, taskData.profileID, taskData.complete); break; case "calculateHistogramData": calculateHistogramData(requestID, taskData.profileID); break; case "calculateDiagnosticItems": calculateDiagnosticItems(requestID, taskData.profileID, taskData.meta); break; default: sendError(requestID, "Unknown task " + task); break; } PROFILERLOG("Complete task: " + task); } catch (e) { PROFILERERROR("Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")"); sendError(requestID, "Exception: " + e + " (" + e.fileName + ":" + e.lineNumber + ")"); } } function sendError(requestID, error) { // support sendError(msg) if (error == null) { error = requestID; requestID = null; } self.postMessage({ requestID: requestID, type: "error", error: error, log: gLogLines }); } function sendProgress(requestID, progress) { self.postMessage({ requestID: requestID, type: "progress", progress: progress }); } function sendFinished(requestID, result) { self.postMessage({ requestID: requestID, type: "finished", result: result, log: gLogLines }); } function bucketsBySplittingArray(array, maxCostPerBucket, costOfElementCallback) { var buckets = []; var currentBucket = []; var currentBucketCost = 0; for (var i = 0; i < array.length; i++) { var element = array[i]; var costOfCurrentElement = costOfElementCallback ? costOfElementCallback(element) : 1; if (currentBucketCost + costOfCurrentElement > maxCostPerBucket) { buckets.push(currentBucket); currentBucket = []; currentBucketCost = 0; } currentBucket.push(element); currentBucketCost += costOfCurrentElement; } buckets.push(currentBucket); return buckets; } function sendFinishedInChunks(requestID, result, maxChunkCost, costOfElementCallback) { if (result.length === undefined || result.slice === undefined) throw new Error("Can't slice result into chunks"); self.postMessage({ requestID: requestID, type: "finishedStart" }); var chunks = bucketsBySplittingArray(result, maxChunkCost, costOfElementCallback); for (var i = 0; i < chunks.length; i++) { self.postMessage({ requestID: requestID, type: "finishedChunk", chunk: chunks[i] }); } self.postMessage({ requestID: requestID, type: "finishedEnd", log: gLogLines }); } function makeSample(frames, extraInfo) { return { frames: frames, extraInfo: extraInfo }; } function cloneSample(sample) { return makeSample(sample.frames.slice(0), sample.extraInfo); } function parseRawProfile(requestID, params, rawProfile) { var progressReporter = new ProgressReporter(); progressReporter.addListener(function (r) { sendProgress(requestID, r.getProgress()); }); progressReporter.begin("Parsing..."); var symbolicationTable = {}; var symbols = []; var symbolIndices = {}; var resources = {}; var functions = []; var functionIndices = {}; var samples = []; var meta = {}; var armIncludePCIndex = {}; if (rawProfile == null) { throw "rawProfile is null"; } if (typeof rawProfile == "string" && rawProfile[0] == "{") { // rawProfile is a JSON string. rawProfile = JSON.parse(rawProfile); if (rawProfile === null) { throw "rawProfile couldn't not successfully be parsed using JSON.parse. Make sure that the profile is a valid JSON encoding."; } } if (rawProfile.profileJSON && !rawProfile.profileJSON.meta && rawProfile.meta) { rawProfile.profileJSON.meta = rawProfile.meta; } if (typeof rawProfile == "object") { switch (rawProfile.format) { case "profileStringWithSymbolicationTable,1": symbolicationTable = rawProfile.symbolicationTable; parseProfileString(rawProfile.profileString); break; case "profileJSONWithSymbolicationTable,1": symbolicationTable = rawProfile.symbolicationTable; parseProfileJSON(rawProfile.profileJSON); break; default: parseProfileJSON(rawProfile); } } else { parseProfileString(rawProfile); } if (params.profileId) { meta.profileId = params.profileId; } function cleanFunctionName(functionName) { var ignoredPrefix = "non-virtual thunk to "; if (functionName.startsWith(ignoredPrefix)) return functionName.substr(ignoredPrefix.length); return functionName; } function resourceNameForAddon(addon) { if (!addon) return ""; var iconHTML = ""; if (addon.iconURL) iconHTML = " " return iconHTML + " " + (/@jetpack$/.exec(addon.id) ? "Jetpack: " : "") + addon.name; } function addonWithID(addonID) { return firstMatch(meta.addons, function addonHasID(addon) { return addon.id.toLowerCase() == addonID.toLowerCase(); }) } function resourceNameForAddonWithID(addonID) { return resourceNameForAddon(addonWithID(addonID)); } function findAddonForChromeURIHost(host) { return firstMatch(meta.addons, function addonUsesChromeURIHost(addon) { return addon.chromeURIHosts && addon.chromeURIHosts.indexOf(host) != -1; }); } function ensureResource(name, resourceDescription) { if (!(name in resources)) { resources[name] = resourceDescription; } return name; } function resourceNameFromLibrary(library) { return ensureResource("lib_" + library, { type: "library", name: library }); } function getAddonForScriptURI(url, host) { if (!meta || !meta.addons) return null; if (url.startsWith("resource:") && endsWith(host, "-at-jetpack")) { // Assume this is a jetpack url var jetpackID = host.substring(0, host.length - 11) + "@jetpack"; return addonWithID(jetpackID); } if (url.startsWith("file:///") && url.indexOf("/extensions/") != -1) { var unpackedAddonNameMatch = /\/extensions\/(.*?)\//.exec(url); if (unpackedAddonNameMatch) return addonWithID(decodeURIComponent(unpackedAddonNameMatch[1])); return null; } if (url.startsWith("jar:file:///") && url.indexOf("/extensions/") != -1) { var packedAddonNameMatch = /\/extensions\/(.*?).xpi/.exec(url); if (packedAddonNameMatch) return addonWithID(decodeURIComponent(packedAddonNameMatch[1])); return null; } if (url.startsWith("chrome://")) { var chromeURIMatch = /chrome\:\/\/(.*?)\//.exec(url); if (chromeURIMatch) return findAddonForChromeURIHost(chromeURIMatch[1]); return null; } return null; } function resourceNameFromURI(url) { if (!url) return ensureResource("unknown", {type: "unknown", name: ""}); var match = /^(.*):\/\/(.*?)\//.exec(url); if (!match) { // Can this happen? If so, we should change the regular expression above. return ensureResource("url_" + url, {type: "url", name: url}); } var urlRoot = match[0]; var protocol = match[1]; var host = match[2]; var addon = getAddonForScriptURI(url, host); if (addon) { return ensureResource("addon_" + addon.id, { type: "addon", name: addon.name, addonID: addon.id, icon: addon.iconURL }); } if (protocol.startsWith("http")) { return ensureResource("webhost_" + host, { type: "webhost", name: host, icon: urlRoot + "favicon.ico" }); } return ensureResource("otherhost_" + host, { type: "otherhost", name: host }); } function parseScriptFile(url) { var match = /([^\/]*)$/.exec(url); if (match && match[1]) return match[1]; return url; } // JS File information sometimes comes with multiple URIs which are chained // with " -> ". We only want the last URI in this list. function getRealScriptURI(url) { if (url) { var urls = url.split(" -> "); return urls[urls.length - 1]; } return url; } function getFunctionInfo(fullName) { function getCPPFunctionInfo(fullName) { var match = /^(.*) \(in ([^\)]*)\) (\+ [0-9]+)$/.exec(fullName) || /^(.*) \(in ([^\)]*)\) (\(.*:.*\))$/.exec(fullName) || /^(.*) \(in ([^\)]*)\)$/.exec(fullName); if (!match) return null; return { functionName: cleanFunctionName(match[1]), libraryName: resourceNameFromLibrary(match[2]), lineInformation: match[3] || "", isRoot: false, isJSFrame: false }; } function getJSFunctionInfo(fullName) { var jsMatch = /^(.*) \((.*):([0-9]+)\)$/.exec(fullName) || /^()(.*):([0-9]+)$/.exec(fullName); if (!jsMatch) return null; var functionName = jsMatch[1] || ""; var scriptURI = getRealScriptURI(jsMatch[2]); var lineNumber = jsMatch[3]; var scriptFile = parseScriptFile(scriptURI); var resourceName = resourceNameFromURI(scriptURI); return { functionName: functionName + "() @ " + scriptFile + ":" + lineNumber, libraryName: resourceName, lineInformation: "", isRoot: false, isJSFrame: true, scriptLocation: { scriptURI: scriptURI, lineInformation: lineNumber } }; } function getFallbackFunctionInfo(fullName) { return { functionName: cleanFunctionName(fullName), libraryName: "", lineInformation: "", isRoot: fullName == "(root)", isJSFrame: false }; } return getCPPFunctionInfo(fullName) || getJSFunctionInfo(fullName) || getFallbackFunctionInfo(fullName); } function indexForFunction(symbol, info) { var resolve = info.functionName + "__" + info.libraryName; if (resolve in functionIndices) return functionIndices[resolve]; var newIndex = functions.length; info.symbol = symbol; functions[newIndex] = info; functionIndices[resolve] = newIndex; return newIndex; } function parseSymbol(symbol) { var info = getFunctionInfo(symbol); //dump("Parse symbol: " + symbol + "\n"); return { symbolName: symbol, functionName: info.functionName, functionIndex: indexForFunction(symbol, info), lineInformation: info.lineInformation, isRoot: info.isRoot, isJSFrame: info.isJSFrame, scriptLocation: info.scriptLocation }; } function translatedSymbol(symbol) { return symbolicationTable[symbol] || symbol; } function indexForSymbol(symbol) { if (symbol in symbolIndices) return symbolIndices[symbol]; var newIndex = symbols.length; symbols[newIndex] = parseSymbol(translatedSymbol(symbol)); symbolIndices[symbol] = newIndex; return newIndex; } function clearRegExpLastMatch() { /./.exec(" "); } function shouldIncludeARMLRForPC(pcIndex) { if (pcIndex in armIncludePCIndex) return armIncludePCIndex[pcIndex]; var pcName = symbols[pcIndex].functionName; var include = sARMFunctionsWithValidLR.indexOf(pcName) != -1; armIncludePCIndex[pcIndex] = include; return include; } function parseProfileString(data) { var extraInfo = {}; var lines = data.split("\n"); var sample = null; for (var i = 0; i < lines.length; ++i) { var line = lines[i]; if (line.length < 2 || line[1] != '-') { // invalid line, ignore it continue; } var info = line.substring(2); switch (line[0]) { //case 'l': // // leaf name // if ("leafName" in extraInfo) { // extraInfo.leafName += ":" + info; // } else { // extraInfo.leafName = info; // } // break; case 'm': // marker if (!("marker" in extraInfo)) { extraInfo.marker = []; } extraInfo.marker.push(info); break; case 's': // sample var sampleName = info; sample = makeSample([indexForSymbol(sampleName)], extraInfo); samples.push(sample); extraInfo = {}; // reset the extra info for future rounds break; case 'c': case 'l': // continue sample if (sample) { // ignore the case where we see a 'c' before an 's' sample.frames.push(indexForSymbol(info)); } break; case 'L': // continue sample; this is an ARM LR record. Stick it before the // PC if it's one of the functions where we know LR is good. if (sample && sample.frames.length > 1) { var pcIndex = sample.frames[sample.frames.length - 1]; if (shouldIncludeARMLRForPC(pcIndex)) { sample.frames.splice(-1, 0, indexForSymbol(info)); } } break; case 't': // time if (sample) { sample.extraInfo["time"] = parseFloat(info); } break; case 'r': // responsiveness if (sample) { sample.extraInfo["responsiveness"] = parseFloat(info); } break; } progressReporter.setProgress((i + 1) / lines.length); } } function parseProfileJSON(profile) { // Thread 0 will always be the main thread of interest // TODO support all the thread in the profile var profileSamples = null; meta = profile.meta || {}; if (params.appendVideoCapture) { meta.videoCapture = { src: params.appendVideoCapture, }; } // Support older format that aren't thread aware if (profile.threads != null) { profileSamples = profile.threads[0].samples; } else { profileSamples = profile; } var rootSymbol = null; var insertCommonRoot = false; var frameStart = {}; meta.frameStart = frameStart; for (var j = 0; j < profileSamples.length; j++) { var sample = profileSamples[j]; var indicedFrames = []; if (!sample) { // This sample was filtered before saving samples.push(null); progressReporter.setProgress((j + 1) / profileSamples.length); continue; } for (var k = 0; sample.frames && k < sample.frames.length; k++) { var frame = sample.frames[k]; var pcIndex; if (frame.location !== undefined) { pcIndex = indexForSymbol(frame.location); } else { pcIndex = indexForSymbol(frame); } if (frame.lr !== undefined && shouldIncludeARMLRForPC(pcIndex)) { indicedFrames.push(indexForSymbol(frame.lr)); } indicedFrames.push(pcIndex); } if (indicedFrames.length >= 1) { if (rootSymbol && rootSymbol != indicedFrames[0]) { insertCommonRoot = true; } rootSymbol = rootSymbol || indicedFrames[0]; } if (sample.extraInfo == null) { sample.extraInfo = {}; } if (sample.responsiveness) { sample.extraInfo["responsiveness"] = sample.responsiveness; } if (sample.marker) { sample.extraInfo["marker"] = sample.marker; } if (sample.time) { sample.extraInfo["time"] = sample.time; } if (sample.frameNumber) { sample.extraInfo["frameNumber"] = sample.frameNumber; //dump("Got frame number: " + sample.frameNumber + "\n"); frameStart[sample.frameNumber] = samples.length; } samples.push(makeSample(indicedFrames, sample.extraInfo)); progressReporter.setProgress((j + 1) / profileSamples.length); } if (insertCommonRoot) { var rootIndex = indexForSymbol("(root)"); for (var i = 0; i < samples.length; i++) { var sample = samples[i]; if (!sample) continue; // If length == 0 then the sample was filtered when saving the profile if (sample.frames.length >= 1 && sample.frames[0] != rootIndex) sample.frames.unshift(rootIndex) } } } progressReporter.finish(); // Don't increment the profile ID now because (1) it's buggy // and (2) for now there's no point in storing each profile // here if we're storing them in the local storage. //var profileID = gNextProfileID++; var profileID = gNextProfileID; gProfiles[profileID] = JSON.parse(JSON.stringify({ meta: meta, symbols: symbols, functions: functions, resources: resources, allSamples: samples })); clearRegExpLastMatch(); sendFinished(requestID, { meta: meta, numSamples: samples.length, profileID: profileID, symbols: symbols, functions: functions, resources: resources }); } function getSerializedProfile(requestID, profileID, complete) { var profile = gProfiles[profileID]; var symbolicationTable = {}; if (complete || !profile.filterSettings.mergeFunctions) { for (var symbolIndex in profile.symbols) { symbolicationTable[symbolIndex] = profile.symbols[symbolIndex].symbolName; } } else { for (var functionIndex in profile.functions) { var f = profile.functions[functionIndex]; symbolicationTable[functionIndex] = f.symbol; } } var serializedProfile = JSON.stringify({ format: "profileJSONWithSymbolicationTable,1", meta: profile.meta, profileJSON: complete ? profile.allSamples : profile.filteredSamples, symbolicationTable: symbolicationTable }); sendFinished(requestID, serializedProfile); } function TreeNode(name, parent, startCount) { this.name = name; this.children = []; this.counter = startCount; this.parent = parent; } TreeNode.prototype.getDepth = function TreeNode__getDepth() { if (this.parent) return this.parent.getDepth() + 1; return 0; }; TreeNode.prototype.findChild = function TreeNode_findChild(name) { for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child.name == name) return child; } return null; } // path is an array of strings which is matched to our nodes' names. // Try to walk path in our own tree and return the last matching node. The // length of the match can be calculated by the caller by comparing the // returned node's depth with the depth of the path's start node. TreeNode.prototype.followPath = function TreeNode_followPath(path) { if (path.length == 0) return this; var matchingChild = this.findChild(path[0]); if (!matchingChild) return this; return matchingChild.followPath(path.slice(1)); }; TreeNode.prototype.incrementCountersInParentChain = function TreeNode_incrementCountersInParentChain() { this.counter++; if (this.parent) this.parent.incrementCountersInParentChain(); }; function convertToCallTree(samples, isReverse) { function areSamplesMultiroot(samples) { var previousRoot; for (var i = 0; i < samples.length; ++i) { if (!previousRoot) { previousRoot = samples[i].frames[0]; continue; } if (previousRoot != samples[i].frames[0]) { return true; } } return false; } samples = samples.filter(function noNullSamples(sample) { return sample != null; }); if (samples.length == 0) return new TreeNode("(empty)", null, 0); var firstRoot = null; for (var i = 0; i < samples.length; ++i) { firstRoot = samples[i].frames[0]; break; } if (firstRoot == null) { return new TreeNode("(all filtered)", null, 0); } var multiRoot = areSamplesMultiroot(samples); var treeRoot = new TreeNode((isReverse || multiRoot) ? "(total)" : firstRoot, null, 0); for (var i = 0; i < samples.length; ++i) { var sample = samples[i]; var callstack = sample.frames.slice(0); callstack.shift(); if (isReverse) callstack.reverse(); var deepestExistingNode = treeRoot.followPath(callstack); var remainingCallstack = callstack.slice(deepestExistingNode.getDepth()); deepestExistingNode.incrementCountersInParentChain(); var node = deepestExistingNode; for (var j = 0; j < remainingCallstack.length; ++j) { var frame = remainingCallstack[j]; var child = new TreeNode(frame, node, 1); node.children.push(child); node = child; } } return treeRoot; } function filterByJank(samples, filterThreshold) { return samples.map(function nullNonJank(sample) { if (!sample || !("responsiveness" in sample.extraInfo) || sample.extraInfo["responsiveness"] < filterThreshold) return null; return sample; }); } function filterBySymbol(samples, symbolOrFunctionIndex) { return samples.map(function filterSample(origSample) { if (!origSample) return null; var sample = cloneSample(origSample); for (var i = 0; i < sample.frames.length; i++) { if (symbolOrFunctionIndex == sample.frames[i]) { sample.frames = sample.frames.slice(i); return sample; } } return null; // no frame matched; filter out complete sample }); } function filterByCallstackPrefix(samples, symbols, functions, callstack, appliesToJS, useFunctions) { var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); } : function isJSSymbolOrRoot(symbolIndex) { return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); }; return samples.map(function filterSample(sample) { if (!sample) return null; if (sample.frames.length < callstack.length) return null; for (var i = 0, j = 0; j < callstack.length; i++) { if (i >= sample.frames.length) return null; if (appliesToJS && !isJSFrameOrRoot(sample.frames[i])) continue; if (sample.frames[i] != callstack[j]) return null; j++; } return makeSample(sample.frames.slice(i - 1), sample.extraInfo); }); } function filterByCallstackPostfix(samples, symbols, functions, callstack, appliesToJS, useFunctions) { var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); } : function isJSSymbolOrRoot(symbolIndex) { return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); }; return samples.map(function filterSample(sample) { if (!sample) return null; if (sample.frames.length < callstack.length) return null; for (var i = 0, j = 0; j < callstack.length; i++) { if (i >= sample.frames.length) return null; if (appliesToJS && !isJSFrameOrRoot(sample.frames[sample.frames.length - i - 1])) continue; if (sample.frames[sample.frames.length - i - 1] != callstack[j]) return null; j++; } var newFrames = sample.frames.slice(0, sample.frames.length - i + 1); return makeSample(newFrames, sample.extraInfo); }); } function chargeNonJSToCallers(samples, symbols, functions, useFunctions) { var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); } : function isJSSymbolOrRoot(symbolIndex) { return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); }; samples = samples.slice(0); for (var i = 0; i < samples.length; ++i) { var sample = samples[i]; if (!sample) continue; var newFrames = sample.frames.filter(isJSFrameOrRoot); if (!newFrames.length) { samples[i] = null; } else { samples[i].frames = newFrames; } } return samples; } function filterByName(samples, symbols, functions, filterName, useFunctions) { function getSymbolOrFunctionName(index, useFunctions) { if (useFunctions) { if (!(index in functions)) return ""; return functions[index].functionName; } if (!(index in symbols)) return ""; return symbols[index].symbolName; } function getLibraryName(index, useFunctions) { if (useFunctions) { if (!(index in functions)) return ""; return functions[index].libraryName; } if (!(index in symbols)) return ""; return symbols[index].libraryName; } samples = samples.slice(0); filterName = filterName.toLowerCase(); calltrace_it: for (var i = 0; i < samples.length; ++i) { var sample = samples[i]; if (!sample) continue; var callstack = sample.frames; for (var j = 0; j < callstack.length; ++j) { var symbolOrFunctionName = getSymbolOrFunctionName(callstack[j], useFunctions); var libraryName = getLibraryName(callstack[j], useFunctions); if (symbolOrFunctionName.toLowerCase().indexOf(filterName) != -1 || libraryName.toLowerCase().indexOf(filterName) != -1) { continue calltrace_it; } } samples[i] = null; } return samples; } function discardLineLevelInformation(samples, symbols, functions) { var data = samples; var filteredData = []; for (var i = 0; i < data.length; i++) { if (!data[i]) { filteredData.push(null); continue; } filteredData.push(cloneSample(data[i])); var frames = filteredData[i].frames; for (var j = 0; j < frames.length; j++) { if (!(frames[j] in symbols)) continue; frames[j] = symbols[frames[j]].functionIndex; } } return filteredData; } function mergeUnbranchedCallPaths(root) { var mergedNames = [root.name]; var node = root; while (node.children.length == 1 && node.counter == node.children[0].counter) { node = node.children[0]; mergedNames.push(node.name); } if (node != root) { // Merge path from root to node into root. root.children = node.children; root.mergedNames = mergedNames; //root.name = clipText(root.name, 50) + " to " + this._clipText(node.name, 50); } for (var i = 0; i < root.children.length; i++) { mergeUnbranchedCallPaths(root.children[i]); } } function FocusedFrameSampleFilter(focusedSymbol) { this._focusedSymbol = focusedSymbol; } FocusedFrameSampleFilter.prototype = { filter: function FocusedFrameSampleFilter_filter(samples, symbols, functions, useFunctions) { return filterBySymbol(samples, this._focusedSymbol); } }; function FocusedCallstackPrefixSampleFilter(focusedCallstack, appliesToJS) { this._focusedCallstackPrefix = focusedCallstack; this._appliesToJS = appliesToJS; } FocusedCallstackPrefixSampleFilter.prototype = { filter: function FocusedCallstackPrefixSampleFilter_filter(samples, symbols, functions, useFunctions) { return filterByCallstackPrefix(samples, symbols, functions, this._focusedCallstackPrefix, this._appliesToJS, useFunctions); } }; function FocusedCallstackPostfixSampleFilter(focusedCallstack, appliesToJS) { this._focusedCallstackPostfix = focusedCallstack; this._appliesToJS = appliesToJS; } FocusedCallstackPostfixSampleFilter.prototype = { filter: function FocusedCallstackPostfixSampleFilter_filter(samples, symbols, functions, useFunctions) { return filterByCallstackPostfix(samples, symbols, functions, this._focusedCallstackPostfix, this._appliesToJS, useFunctions); } }; function RangeSampleFilter(start, end) { this._start = start; this._end = end; } RangeSampleFilter.prototype = { filter: function RangeSampleFilter_filter(samples, symbols, functions) { return samples.slice(this._start, this._end); } } function unserializeSampleFilters(filters) { return filters.map(function (filter) { switch (filter.type) { case "FocusedFrameSampleFilter": return new FocusedFrameSampleFilter(filter.focusedSymbol); case "FocusedCallstackPrefixSampleFilter": return new FocusedCallstackPrefixSampleFilter(filter.focusedCallstack, filter.appliesToJS); case "FocusedCallstackPostfixSampleFilter": return new FocusedCallstackPostfixSampleFilter(filter.focusedCallstack, filter.appliesToJS); case "RangeSampleFilter": return new RangeSampleFilter(filter.start, filter.end); case "PluginView": return null; default: throw new Error("Unknown filter"); } }) } var gJankThreshold = 50 /* ms */; function updateFilters(requestID, profileID, filters) { var profile = gProfiles[profileID]; var samples = profile.allSamples; var symbols = profile.symbols; var functions = profile.functions; if (filters.mergeFunctions) { samples = discardLineLevelInformation(samples, symbols, functions); } if (filters.nameFilter) { try { samples = filterByName(samples, symbols, functions, filters.nameFilter, filters.mergeFunctions); } catch (e) { dump("Could not filer by name: " + e + "\n"); } } samples = unserializeSampleFilters(filters.sampleFilters).reduce(function (filteredSamples, currentFilter) { if (currentFilter===null) return filteredSamples; return currentFilter.filter(filteredSamples, symbols, functions, filters.mergeFunctions); }, samples); if (filters.jankOnly) { samples = filterByJank(samples, gJankThreshold); } if (filters.javascriptOnly) { samples = chargeNonJSToCallers(samples, symbols, functions, filters.mergeFunctions); } gProfiles[profileID].filterSettings = filters; gProfiles[profileID].filteredSamples = samples; sendFinishedInChunks(requestID, samples, 40000, function (sample) { return sample ? sample.frames.length : 1; }); } function updateViewOptions(requestID, profileID, options) { var profile = gProfiles[profileID]; var samples = profile.filteredSamples; var symbols = profile.symbols; var functions = profile.functions; var treeData = convertToCallTree(samples, options.invertCallstack); if (options.mergeUnbranched) mergeUnbranchedCallPaths(treeData); sendFinished(requestID, treeData); } // The responsiveness threshold (in ms) after which the sample shuold become // completely red in the histogram. var kDelayUntilWorstResponsiveness = 1000; function calculateHistogramData(requestID, profileID) { function getStepColor(step) { if (step.extraInfo && "responsiveness" in step.extraInfo) { var res = step.extraInfo.responsiveness; var redComponent = Math.round(255 * Math.min(1, res / kDelayUntilWorstResponsiveness)); return "rgb(" + redComponent + ",0,0)"; } return "rgb(0,0,0)"; } var profile = gProfiles[profileID]; var data = profile.filteredSamples; var histogramData = []; var maxHeight = 0; for (var i = 0; i < data.length; ++i) { if (!data[i]) continue; var value = data[i].frames.length; if (maxHeight < value) maxHeight = value; } maxHeight += 1; var nextX = 0; // The number of data items per histogramData rects. // Except when seperated by a marker. // This is used to cut down the number of rects, since // there's no point in having more rects then pixels var samplesPerStep = Math.max(1, Math.floor(data.length / 2000)); var frameStart = {}; for (var i = 0; i < data.length; i++) { var step = data[i]; if (!step) { // Add a gap for the sample that was filtered out. nextX += 1 / samplesPerStep; continue; } nextX = Math.ceil(nextX); var value = step.frames.length / maxHeight; var frames = step.frames; var currHistogramData = histogramData[histogramData.length-1]; if (step.extraInfo && "marker" in step.extraInfo) { // A new marker boundary has been discovered. histogramData.push({ frames: "marker", x: nextX, width: 2, value: 1, marker: step.extraInfo.marker, color: "fuchsia" }); nextX += 2; histogramData.push({ frames: [step.frames], x: nextX, width: 1, value: value, color: getStepColor(step), }); nextX += 1; } else if (currHistogramData != null && currHistogramData.frames.length < samplesPerStep && !(step.extraInfo && "frameNumber" in step.extraInfo)) { currHistogramData.frames.push(step.frames); // When merging data items take the average: currHistogramData.value = (currHistogramData.value * (currHistogramData.frames.length - 1) + value) / currHistogramData.frames.length; // Merge the colors? For now we keep the first color set. } else { // A new name boundary has been discovered. currHistogramData = { frames: [step.frames], x: nextX, width: 1, value: value, color: getStepColor(step), }; if (step.extraInfo && "frameNumber" in step.extraInfo) { currHistogramData.frameNumber = step.extraInfo.frameNumber; frameStart[step.extraInfo.frameNumber] = histogramData.length; } histogramData.push(currHistogramData); nextX += 1; } } sendFinished(requestID, { histogramData: histogramData, frameStart: frameStart, widthSum: Math.ceil(nextX) }); } var diagnosticList = [ // *************** Known bugs first (highest priority) { image: "io.png", title: "Main Thread IO - Bug 765135 - TISCreateInputSourceList", check: function(frames, symbols, meta) { if (!stepContains('TISCreateInputSourceList', frames, symbols)) return false; return stepContains('__getdirentries64', frames, symbols) || stepContains('__read', frames, symbols) || stepContains('__open', frames, symbols) || stepContains('stat$INODE64', frames, symbols) ; }, }, { image: "js.png", title: "Bug 772916 - Gradients are slow on mobile", bugNumber: "772916", check: function(frames, symbols, meta) { return stepContains('PaintGradient', frames, symbols) && stepContains('BasicTiledLayerBuffer::PaintThebesSingleBufferDraw', frames, symbols) ; }, }, { image: "cache.png", title: "Bug 717761 - Main thread can be blocked by IO on the cache thread", bugNumber: "717761", check: function(frames, symbols, meta) { return stepContains('nsCacheEntryDescriptor::GetStoragePolicy', frames, symbols) ; }, }, { image: "js.png", title: "Web Content Shutdown Notification", check: function(frames, symbols, meta) { return stepContains('nsAppStartup::Quit', frames, symbols) && stepContains('nsDocShell::FirePageHideNotification', frames, symbols) ; }, }, { image: "js.png", title: "Bug 789193 - AMI_startup() takes 200ms on startup", bugNumber: "789193", check: function(frames, symbols, meta) { return stepContains('AMI_startup()', frames, symbols) ; }, }, { image: "js.png", title: "Bug 818296 - [Shutdown] js::NukeCrossCompartmentWrappers takes up 300ms on shutdown", bugNumber: "818296", check: function(frames, symbols, meta) { return stepContains('js::NukeCrossCompartmentWrappers', frames, symbols) && (stepContains('WindowDestroyedEvent', frames, symbols) || stepContains('DoShutdown', frames, symbols)) ; }, }, { image: "js.png", title: "Bug 818274 - [Shutdown] Telemetry takes ~10ms on shutdown", bugNumber: "818274", check: function(frames, symbols, meta) { return stepContains('TelemetryPing.js', frames, symbols) ; }, }, { image: "plugin.png", title: "Bug 818265 - [Shutdown] Plug-in shutdown takes ~90ms on shutdown", bugNumber: "818265", check: function(frames, symbols, meta) { return stepContains('PluginInstanceParent::Destroy', frames, symbols) ; }, }, { image: "snapshot.png", title: "Bug 720575 - Make thumbnailing faster and/or asynchronous", bugNumber: "720575", check: function(frames, symbols, meta) { return stepContains('Thumbnails_capture()', frames, symbols) ; }, }, { image: "js.png", title: "Bug 789185 - LoginManagerStorage_mozStorage.init() takes 300ms on startup ", bugNumber: "789185", check: function(frames, symbols, meta) { return stepContains('LoginManagerStorage_mozStorage.prototype.init()', frames, symbols) ; }, }, { image: "js.png", title: "JS - Bug 767070 - Text selection performance is bad on android", bugNumber: "767070", check: function(frames, symbols, meta) { if (!stepContains('FlushPendingNotifications', frames, symbols)) return false; return stepContains('sh_', frames, symbols) && stepContains('browser.js', frames, symbols) ; }, }, { image: "js.png", title: "JS - Bug 765930 - Reader Mode: Optimize readability check", bugNumber: "765930", check: function(frames, symbols, meta) { return stepContains('Readability.js', frames, symbols) ; }, }, // **************** General issues { image: "js.png", title: "JS is triggering a sync reflow", check: function(frames, symbols, meta) { return symbolSequence(['js::RunScript','layout::DoReflow'], frames, symbols) || symbolSequence(['js::RunScript','layout::Flush'], frames, symbols) ; }, }, { image: "gc.png", title: "Garbage Collection Slice", canMergeWithGC: false, check: function(frames, symbols, meta, step) { var slice = findGCSlice(frames, symbols, meta, step); if (slice) { var gcEvent = findGCEvent(frames, symbols, meta, step); //dump("found event matching diagnostic\n"); //dump(JSON.stringify(gcEvent) + "\n"); return true; } return false; }, details: function(frames, symbols, meta, step) { var slice = findGCSlice(frames, symbols, meta, step); if (slice) { return "" + "Reason: " + slice.reason + "\n" + "Slice: " + slice.slice + "\n" + "Pause: " + slice.pause + " ms"; } return null; }, onclickDetails: function(frames, symbols, meta, step) { var gcEvent = findGCEvent(frames, symbols, meta, step); if (gcEvent) { return JSON.stringify(gcEvent); } else { return null; } }, }, { image: "cc.png", title: "Cycle Collect", check: function(frames, symbols, meta, step) { var ccEvent = findCCEvent(frames, symbols, meta, step); if (ccEvent) { return true; } return false; }, details: function(frames, symbols, meta, step) { var ccEvent = findCCEvent(frames, symbols, meta, step); if (ccEvent) { return "" + "Duration: " + ccEvent.duration + " ms\n" + "Suspected: " + ccEvent.suspected; } return null; }, onclickDetails: function(frames, symbols, meta, step) { var ccEvent = findCCEvent(frames, symbols, meta, step); if (ccEvent) { return JSON.stringify(ccEvent); } else { return null; } }, }, { image: "gc.png", title: "Garbage Collection", canMergeWithGC: false, check: function(frames, symbols, meta) { return stepContainsRegEx(/.*Collect.*Runtime.*Invocation.*/, frames, symbols) || stepContains('GarbageCollectNow', frames, symbols) // Label || stepContains('JS_GC(', frames, symbols) // Label || stepContains('CycleCollect__', frames, symbols) // Label ; }, }, { image: "cc.png", title: "Cycle Collect", check: function(frames, symbols, meta) { return stepContains('nsCycleCollector::Collect', frames, symbols) || stepContains('CycleCollect__', frames, symbols) // Label || stepContains('nsCycleCollectorRunner::Collect', frames, symbols) // Label ; }, }, { image: "plugin.png", title: "Sync Plugin Constructor", check: function(frames, symbols, meta) { return stepContains('CallPPluginInstanceConstructor', frames, symbols) || stepContains('CallPCrashReporterConstructor', frames, symbols) || stepContains('PPluginModuleParent::CallNP_Initialize', frames, symbols) || stepContains('GeckoChildProcessHost::SyncLaunch', frames, symbols) ; }, }, { image: "text.png", title: "Font Loading", check: function(frames, symbols, meta) { return stepContains('gfxFontGroup::BuildFontList', frames, symbols); }, }, { image: "io.png", title: "Main Thread IO!", check: function(frames, symbols, meta) { return stepContains('__getdirentries64', frames, symbols) || stepContains('__open', frames, symbols) || stepContains('NtFlushBuffersFile', frames, symbols) || stepContains('storage:::Statement::ExecuteStep', frames, symbols) || stepContains('__unlink', frames, symbols) || stepContains('fsync', frames, symbols) || stepContains('stat$INODE64', frames, symbols) ; }, }, ]; function hasJSFrame(frames, symbols) { for (var i = 0; i < frames.length; i++) { if (symbols[frames[i]].isJSFrame === true) { return true; } } return false; } function findCCEvent(frames, symbols, meta, step) { if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats) return null; var time = step.extraInfo.time; for (var i = 0; i < meta.gcStats.ccEvents.length; i++) { var ccEvent = meta.gcStats.ccEvents[i]; if (ccEvent.start_timestamp <= time && ccEvent.end_timestamp >= time) { //dump("JSON: " + js_beautify(JSON.stringify(ccEvent)) + "\n"); return ccEvent; } } return null; } function findGCEvent(frames, symbols, meta, step) { if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats) return null; var time = step.extraInfo.time; for (var i = 0; i < meta.gcStats.gcEvents.length; i++) { var gcEvent = meta.gcStats.gcEvents[i]; if (!gcEvent.slices) continue; for (var j = 0; j < gcEvent.slices.length; j++) { var slice = gcEvent.slices[j]; if (slice.start_timestamp <= time && slice.end_timestamp >= time) { return gcEvent; } } } return null; } function findGCSlice(frames, symbols, meta, step) { if (!step || !step.extraInfo || !step.extraInfo.time || !meta || !meta.gcStats) return null; var time = step.extraInfo.time; for (var i = 0; i < meta.gcStats.gcEvents.length; i++) { var gcEvent = meta.gcStats.gcEvents[i]; if (!gcEvent.slices) continue; for (var j = 0; j < gcEvent.slices.length; j++) { var slice = gcEvent.slices[j]; if (slice.start_timestamp <= time && slice.end_timestamp >= time) { return slice; } } } return null; } function stepContains(substring, frames, symbols) { for (var i = 0; frames && i < frames.length; i++) { if (!(frames[i] in symbols)) continue; var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; if (frameSym.indexOf(substring) != -1) { return true; } } return false; } function stepContainsRegEx(regex, frames, symbols) { for (var i = 0; frames && i < frames.length; i++) { if (!(frames[i] in symbols)) continue; var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; if (regex.exec(frameSym)) { return true; } } return false; } function symbolSequence(symbolsOrder, frames, symbols) { var symbolIndex = 0; for (var i = 0; frames && i < frames.length; i++) { if (!(frames[i] in symbols)) continue; var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; var substring = symbolsOrder[symbolIndex]; if (frameSym.indexOf(substring) != -1) { symbolIndex++; if (symbolIndex == symbolsOrder.length) { return true; } } } return false; } function firstMatch(array, matchFunction) { for (var i = 0; i < array.length; i++) { if (matchFunction(array[i])) return array[i]; } return undefined; } function calculateDiagnosticItems(requestID, profileID, meta) { /* if (!histogramData || histogramData.length < 1) { sendFinished(requestID, []); return; }*/ var profile = gProfiles[profileID]; //var symbols = profile.symbols; var symbols = profile.functions; var data = profile.filteredSamples; var lastStep = data[data.length-1]; var widthSum = data.length; var pendingDiagnosticInfo = null; var diagnosticItems = []; function finishPendingDiagnostic(endX) { if (!pendingDiagnosticInfo) return; var diagnostic = pendingDiagnosticInfo.diagnostic; var currDiagnostic = { x: pendingDiagnosticInfo.x / widthSum, width: (endX - pendingDiagnosticInfo.x) / widthSum, imageFile: pendingDiagnosticInfo.diagnostic.image, title: pendingDiagnosticInfo.diagnostic.title, details: pendingDiagnosticInfo.details, onclickDetails: pendingDiagnosticInfo.onclickDetails }; if (!currDiagnostic.onclickDetails && diagnostic.bugNumber) { currDiagnostic.onclickDetails = "bug " + diagnostic.bugNumber; } diagnosticItems.push(currDiagnostic); pendingDiagnosticInfo = null; } /* dump("meta: " + meta.gcStats + "\n"); if (meta && meta.gcStats) { dump("GC Stats: " + JSON.stringify(meta.gcStats) + "\n"); } */ data.forEach(function diagnoseStep(step, x) { if (step) { var frames = step.frames; var diagnostic = firstMatch(diagnosticList, function (diagnostic) { return diagnostic.check(frames, symbols, meta, step); }); } if (!diagnostic) { finishPendingDiagnostic(x); return; } var details = diagnostic.details ? diagnostic.details(frames, symbols, meta, step) : null; if (pendingDiagnosticInfo) { // We're already inside a diagnostic range. if (diagnostic == pendingDiagnosticInfo.diagnostic && pendingDiagnosticInfo.details == details) { // We're still inside the same diagnostic. return; } // We have left the old diagnostic and found a new one. Finish the old one. finishPendingDiagnostic(x); } pendingDiagnosticInfo = { diagnostic: diagnostic, x: x, details: details, onclickDetails: diagnostic.onclickDetails ? diagnostic.onclickDetails(frames, symbols, meta, step) : null }; }); if (pendingDiagnosticInfo) finishPendingDiagnostic(data.length); sendFinished(requestID, diagnosticItems); }