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: }