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