diff -r 000000000000 -r 6474c204b198 browser/devtools/profiler/cleopatra/js/parserWorker.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/devtools/profiler/cleopatra/js/parserWorker.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1664 @@ +/* -*- 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/. */ + +"use strict"; + +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 || null) { + 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" + }); + } + + if (protocol.startsWith("file")) { + return ensureResource("file_" + host, { + type: "file", + name: host + }); + } + + 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('ClientTiledLayerBuffer::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); +}