From e15b9dcc561f085279a28c4573c7b1c92881cf7d Mon Sep 17 00:00:00 2001 From: Anton Kovalyov Date: Wed, 16 Jan 2013 11:44:35 -0800 Subject: [PATCH] Bug 828052 - Update Cleopatra and fix the issue with gJavaScriptOnly flag not working; r=past --- browser/devtools/profiler/ProfilerPanel.jsm | 2 +- .../profiler/cleopatra/js/devtools.js | 125 +++- .../profiler/cleopatra/js/parserWorker.js | 536 ++++++++++------ .../devtools/profiler/cleopatra/js/tree.js | 126 +++- browser/devtools/profiler/cleopatra/js/ui.js | 600 ++++++++++++++---- 5 files changed, 1022 insertions(+), 367 deletions(-) diff --git a/browser/devtools/profiler/ProfilerPanel.jsm b/browser/devtools/profiler/ProfilerPanel.jsm index 046e9025e3a..7308bc3283d 100644 --- a/browser/devtools/profiler/ProfilerPanel.jsm +++ b/browser/devtools/profiler/ProfilerPanel.jsm @@ -277,7 +277,7 @@ ProfilerPanel.prototype = { * If the instance is already loaded, onLoad will be * called synchronously. */ - switchToProfile: function PP_switchToProfile(profile, onLoad) { + switchToProfile: function PP_switchToProfile(profile, onLoad=function() {}) { let doc = this.document; if (this.activeProfile) { diff --git a/browser/devtools/profiler/cleopatra/js/devtools.js b/browser/devtools/profiler/cleopatra/js/devtools.js index cefe9938983..e90a7795bfc 100644 --- a/browser/devtools/profiler/cleopatra/js/devtools.js +++ b/browser/devtools/profiler/cleopatra/js/devtools.js @@ -47,14 +47,19 @@ function onParentMessage(event) { var stop = document.getElementById("stopWrapper"); var msg = JSON.parse(event.data); - if (msg.task === "onStarted") { - start.style.display = "none"; - start.querySelector("button").removeAttribute("disabled"); - stop.style.display = "inline"; - } else if (msg.task === "onStopped") { - stop.style.display = "none"; - stop.querySelector("button").removeAttribute("disabled"); - start.style.display = "inline"; + switch (msg.task) { + case "onStarted": + start.style.display = "none"; + start.querySelector("button").removeAttribute("disabled"); + stop.style.display = "inline"; + break; + case "onStopped": + stop.style.display = "none"; + stop.querySelector("button").removeAttribute("disabled"); + start.style.display = "inline"; + break; + case "receiveProfileData": + loadProfile(JSON.stringify(msg.rawProfile)); } } @@ -66,7 +71,9 @@ window.addEventListener("message", onParentMessage); */ function initUI() { gLightMode = true; - gJavaScriptOnly = true; + + gFileList = { profileParsingFinished: function () {} }; + gInfoBar = { display: function () {} }; var container = document.createElement("div"); container.id = "ui"; @@ -101,4 +108,104 @@ function initUI() { controlPane.querySelector("#stopWrapper > span.btn").appendChild(stopButton); gMainArea.appendChild(controlPane); +} + +/** + * Modified copy of Cleopatra's enterFinishedProfileUI. + * By overriding the function we don't need to modify ui.js which helps + * with updating from upstream. + */ +function enterFinishedProfileUI() { + var cover = document.createElement("div"); + cover.className = "finishedProfilePaneBackgroundCover"; + + var pane = document.createElement("table"); + var rowIndex = 0; + var currRow; + + pane.style.width = "100%"; + pane.style.height = "100%"; + pane.border = "0"; + pane.cellPadding = "0"; + pane.cellSpacing = "0"; + pane.borderCollapse = "collapse"; + pane.className = "finishedProfilePane"; + + gBreadcrumbTrail = new BreadcrumbTrail(); + currRow = pane.insertRow(rowIndex++); + currRow.insertCell(0).appendChild(gBreadcrumbTrail.getContainer()); + + gHistogramView = new HistogramView(); + currRow = pane.insertRow(rowIndex++); + currRow.insertCell(0).appendChild(gHistogramView.getContainer()); + + if (gMeta && gMeta.videoCapture) { + gVideoPane = new VideoPane(gMeta.videoCapture); + gVideoPane.onTimeChange(videoPaneTimeChange); + currRow = pane.insertRow(rowIndex++); + currRow.insertCell(0).appendChild(gVideoPane.getContainer()); + } + + var tree = document.createElement("div"); + tree.className = "treeContainer"; + tree.style.width = "100%"; + tree.style.height = "100%"; + + gTreeManager = new ProfileTreeManager(); + gTreeManager.treeView.setColumns([ + { name: "sampleCount", title: "Running time" }, + { name: "selfSampleCount", title: "Self" }, + { name: "resource", title: "" }, + ]); + + currRow = pane.insertRow(rowIndex++); + currRow.style.height = "100%"; + + var cell = currRow.insertCell(0); + cell.appendChild(tree); + tree.appendChild(gTreeManager.getContainer()); + + gPluginView = new PluginView(); + tree.appendChild(gPluginView.getContainer()); + + gMainArea.appendChild(cover); + gMainArea.appendChild(pane); + + var currentBreadcrumb = gSampleFilters; + gBreadcrumbTrail.add({ + title: "Complete Profile", + enterCallback: function () { + gSampleFilters = []; + filtersChanged(); + } + }); + + if (currentBreadcrumb == null || currentBreadcrumb.length == 0) { + gTreeManager.restoreSerializedSelectionSnapshot(gRestoreSelection); + viewOptionsChanged(); + } + + for (var i = 0; i < currentBreadcrumb.length; i++) { + var filter = currentBreadcrumb[i]; + var forceSelection = null; + if (gRestoreSelection != null && i == currentBreadcrumb.length - 1) { + forceSelection = gRestoreSelection; + } + switch (filter.type) { + case "FocusedFrameSampleFilter": + focusOnSymbol(filter.name, filter.symbolName); + gBreadcrumbTrail.enterLastItem(forceSelection); + case "FocusedCallstackPrefixSampleFilter": + focusOnCallstack(filter.focusedCallstack, filter.name, false); + gBreadcrumbTrail.enterLastItem(forceSelection); + case "FocusedCallstackPostfixSampleFilter": + focusOnCallstack(filter.focusedCallstack, filter.name, true); + gBreadcrumbTrail.enterLastItem(forceSelection); + case "RangeSampleFilter": + gHistogramView.selectRange(filter.start, filter.end); + gBreadcrumbTrail.enterLastItem(forceSelection); + } + } + + toggleJavascriptOnly(); } \ No newline at end of file diff --git a/browser/devtools/profiler/cleopatra/js/parserWorker.js b/browser/devtools/profiler/cleopatra/js/parserWorker.js index cacea8ba083..e7335af61c1 100755 --- a/browser/devtools/profiler/cleopatra/js/parserWorker.js +++ b/browser/devtools/profiler/cleopatra/js/parserWorker.js @@ -239,17 +239,26 @@ function parseRawProfile(requestID, params, rawProfile) { var symbolicationTable = {}; var symbols = []; var symbolIndices = {}; + var resources = {}; var functions = []; var functionIndices = {}; var samples = []; var meta = {}; var armIncludePCIndex = {}; + if (rawProfile == null) { + throw "rawProfile is null"; + } + if (typeof rawProfile == "string" && rawProfile[0] == "{") { // rawProfile is a JSON string. rawProfile = JSON.parse(rawProfile); + if (rawProfile === null) { + throw "rawProfile couldn't not successfully be parsed using JSON.parse. Make sure that the profile is a valid JSON encoding."; + } } + if (rawProfile.profileJSON && !rawProfile.profileJSON.meta && rawProfile.meta) { rawProfile.profileJSON.meta = rawProfile.meta; } @@ -271,149 +280,217 @@ function parseRawProfile(requestID, params, rawProfile) { parseProfileString(rawProfile); } + if (params.profileId) { + meta.profileId = params.profileId; + } + function cleanFunctionName(functionName) { var ignoredPrefix = "non-virtual thunk to "; - if (functionName.substr(0, ignoredPrefix.length) == ignoredPrefix) + if (functionName.startsWith(ignoredPrefix)) return functionName.substr(ignoredPrefix.length); return functionName; } - function resourceNameForAddon(addonID) { - for (var i in meta.addons) { - var addon = meta.addons[i]; - if (addon.id.toLowerCase() == addonID.toLowerCase()) { - var iconHTML = ""; - if (addon.iconURL) - iconHTML = " " - return iconHTML + " " + (/@jetpack$/.exec(addonID) ? "Jetpack: " : "") + addon.name; - } - } - return ""; - } - - function parseResourceName(url) { - if (!url) { - return "No URL"; - } - if (url.startsWith("resource:///")) { - // Take the last URL from a chained list of URLs. - var urls = url.split(" -> "); - url = urls[urls.length - 1]; - } - - // TODO Fix me, this certainly doesn't handle all URLs formats - var match = /^.*:\/\/(.*?)\/.*$/.exec(url); - - if (!match) - return url; - - var host = match[1]; - - if (meta && meta.addons) { - if (url.startsWith("resource:") && endsWith(host, "-at-jetpack")) { - // Assume this is a jetpack url - var jetpackID = host.substring(0, host.length - 11) + "@jetpack"; - var resName = resourceNameForAddon(jetpackID); - if (resName) - return resName; - } - if (url.startsWith("file:///") && url.indexOf("/extensions/") != -1) { - var unpackedAddonNameMatch = /\/extensions\/(.*?)\//.exec(url); - if (unpackedAddonNameMatch) { - var resName = resourceNameForAddon(decodeURIComponent(unpackedAddonNameMatch[1])); - if (resName) - return resName; - } - } - if (url.startsWith("jar:file:///") && url.indexOf("/extensions/") != -1) { - var packedAddonNameMatch = /\/extensions\/(.*?).xpi/.exec(url); - if (packedAddonNameMatch) { - var resName = resourceNameForAddon(decodeURIComponent(packedAddonNameMatch[1])); - if (resName) - return resName; - } - } - } + function resourceNameForAddon(addon) { + if (!addon) + return ""; var iconHTML = ""; - if (url.indexOf("http://") == 0) { - iconHTML = " "; - } else if (url.indexOf("https://") == 0) { - iconHTML = " "; + if (addon.iconURL) + iconHTML = " " + return iconHTML + " " + (/@jetpack$/.exec(addon.id) ? "Jetpack: " : "") + addon.name; + } + + function addonWithID(addonID) { + return firstMatch(meta.addons, function addonHasID(addon) { + return addon.id.toLowerCase() == addonID.toLowerCase(); + }) + } + + function resourceNameForAddonWithID(addonID) { + return resourceNameForAddon(addonWithID(addonID)); + } + + function findAddonForChromeURIHost(host) { + return firstMatch(meta.addons, function addonUsesChromeURIHost(addon) { + return addon.chromeURIHosts && addon.chromeURIHosts.indexOf(host) != -1; + }); + } + + function ensureResource(name, resourceDescription) { + if (!(name in resources)) { + resources[name] = resourceDescription; } - return iconHTML + host; + return name; + } + + function resourceNameFromLibrary(library) { + return ensureResource("lib_" + library, { + type: "library", + name: library + }); + } + + function getAddonForScriptURI(url, host) { + if (!meta || !meta.addons) + return null; + + if (url.startsWith("resource:") && endsWith(host, "-at-jetpack")) { + // Assume this is a jetpack url + var jetpackID = host.substring(0, host.length - 11) + "@jetpack"; + return addonWithID(jetpackID); + } + + if (url.startsWith("file:///") && url.indexOf("/extensions/") != -1) { + var unpackedAddonNameMatch = /\/extensions\/(.*?)\//.exec(url); + if (unpackedAddonNameMatch) + return addonWithID(decodeURIComponent(unpackedAddonNameMatch[1])); + return null; + } + + if (url.startsWith("jar:file:///") && url.indexOf("/extensions/") != -1) { + var packedAddonNameMatch = /\/extensions\/(.*?).xpi/.exec(url); + if (packedAddonNameMatch) + return addonWithID(decodeURIComponent(packedAddonNameMatch[1])); + return null; + } + + if (url.startsWith("chrome://")) { + var chromeURIMatch = /chrome\:\/\/(.*?)\//.exec(url); + if (chromeURIMatch) + return findAddonForChromeURIHost(chromeURIMatch[1]); + return null; + } + + return null; + } + + function resourceNameFromURI(url) { + if (!url) + return ensureResource("unknown", {type: "unknown", name: ""}); + + var match = /^(.*):\/\/(.*?)\//.exec(url); + + if (!match) { + // Can this happen? If so, we should change the regular expression above. + return ensureResource("url_" + url, {type: "url", name: url}); + } + + var urlRoot = match[0]; + var protocol = match[1]; + var host = match[2]; + + var addon = getAddonForScriptURI(url, host); + if (addon) { + return ensureResource("addon_" + addon.id, { + type: "addon", + name: addon.name, + addonID: addon.id, + icon: addon.iconURL + }); + } + + if (protocol.startsWith("http")) { + return ensureResource("webhost_" + host, { + type: "webhost", + name: host, + icon: urlRoot + "favicon.ico" + }); + } + + return ensureResource("otherhost_" + host, { + type: "otherhost", + name: host + }); } function parseScriptFile(url) { - // TODO Fix me, this certainly doesn't handle all URLs formats - var match = /^.*\/(.*)\.js$/.exec(url); + var match = /([^\/]*)$/.exec(url); + if (match && match[1]) + return match[1]; - if (!match) - return url; - - return match[1] + ".js"; + return url; } - function parseScriptURI(url) { + // JS File information sometimes comes with multiple URIs which are chained + // with " -> ". We only want the last URI in this list. + function getRealScriptURI(url) { if (url) { - var urlTokens = url.split(" "); - url = urlTokens[urlTokens.length-1]; + var urls = url.split(" -> "); + return urls[urls.length - 1]; } return url; } function getFunctionInfo(fullName) { - var isJSFrame = false; - var match = - /^(.*) \(in ([^\)]*)\) (\+ [0-9]+)$/.exec(fullName) || - /^(.*) \(in ([^\)]*)\) (\(.*:.*\))$/.exec(fullName) || - /^(.*) \(in ([^\)]*)\)$/.exec(fullName); - // Try to parse a JS frame - var scriptLocation = null; - var jsMatch1 = match || - /^(.*) \((.*):([0-9]+)\)$/.exec(fullName); - if (!match && jsMatch1) { - scriptLocation = { - scriptURI: parseScriptURI(jsMatch1[2]), - lineInformation: jsMatch1[3] + + function getCPPFunctionInfo(fullName) { + var match = + /^(.*) \(in ([^\)]*)\) (\+ [0-9]+)$/.exec(fullName) || + /^(.*) \(in ([^\)]*)\) (\(.*:.*\))$/.exec(fullName) || + /^(.*) \(in ([^\)]*)\)$/.exec(fullName); + + if (!match) + return null; + + return { + functionName: cleanFunctionName(match[1]), + libraryName: resourceNameFromLibrary(match[2]), + lineInformation: match[3] || "", + isRoot: false, + isJSFrame: false }; - match = [0, jsMatch1[1]+"() @ "+parseScriptFile(jsMatch1[2]) + ":" + jsMatch1[3], parseResourceName(jsMatch1[2]), ""]; - isJSFrame = true; } - var jsMatch2 = match || - /^(.*):([0-9]+)$/.exec(fullName); - if (!match && jsMatch2) { - scriptLocation = { - scriptURI: parseScriptURI(jsMatch2[1]), - lineInformation: jsMatch2[2] + + function getJSFunctionInfo(fullName) { + var jsMatch = + /^(.*) \((.*):([0-9]+)\)$/.exec(fullName) || + /^()(.*):([0-9]+)$/.exec(fullName); + + if (!jsMatch) + return null; + + var functionName = jsMatch[1] || ""; + var scriptURI = getRealScriptURI(jsMatch[2]); + var lineNumber = jsMatch[3]; + var scriptFile = parseScriptFile(scriptURI); + var resourceName = resourceNameFromURI(scriptURI); + + return { + functionName: functionName + "() @ " + scriptFile + ":" + lineNumber, + libraryName: resourceName, + lineInformation: "", + isRoot: false, + isJSFrame: true, + scriptLocation: { + scriptURI: scriptURI, + lineInformation: lineNumber + } }; - match = [0, " @ "+parseScriptFile(jsMatch2[1]) + ":" + jsMatch2[2], parseResourceName(jsMatch2[1]), ""]; - isJSFrame = true; } - if (!match) { - match = [fullName, fullName]; + + function getFallbackFunctionInfo(fullName) { + return { + functionName: cleanFunctionName(fullName), + libraryName: "", + lineInformation: "", + isRoot: fullName == "(root)", + isJSFrame: false + }; } - return { - functionName: cleanFunctionName(match[1]), - libraryName: match[2] || "", - lineInformation: match[3] || "", - isJSFrame: isJSFrame, - scriptLocation: scriptLocation - }; + + return getCPPFunctionInfo(fullName) || + getJSFunctionInfo(fullName) || + getFallbackFunctionInfo(fullName); } - function indexForFunction(symbol, functionName, libraryName, isJSFrame, scriptLocation) { - var resolve = functionName+"_LIBNAME_"+libraryName; + function indexForFunction(symbol, info) { + var resolve = info.functionName + "__" + info.libraryName; if (resolve in functionIndices) return functionIndices[resolve]; var newIndex = functions.length; - functions[newIndex] = { - symbol: symbol, - functionName: functionName, - libraryName: libraryName, - isJSFrame: isJSFrame, - scriptLocation: scriptLocation - }; + info.symbol = symbol; + functions[newIndex] = info; functionIndices[resolve] = newIndex; return newIndex; } @@ -424,8 +501,9 @@ function parseRawProfile(requestID, params, rawProfile) { return { symbolName: symbol, functionName: info.functionName, - functionIndex: indexForFunction(symbol, info.functionName, info.libraryName, info.isJSFrame, info.scriptLocation), + functionIndex: indexForFunction(symbol, info), lineInformation: info.lineInformation, + isRoot: info.isRoot, isJSFrame: info.isJSFrame, scriptLocation: info.scriptLocation }; @@ -582,6 +660,9 @@ function parseRawProfile(requestID, params, rawProfile) { if (sample.responsiveness) { sample.extraInfo["responsiveness"] = sample.responsiveness; } + if (sample.marker) { + sample.extraInfo["marker"] = sample.marker; + } if (sample.time) { sample.extraInfo["time"] = sample.time; } @@ -600,17 +681,22 @@ function parseRawProfile(requestID, params, rawProfile) { if (!sample) continue; // If length == 0 then the sample was filtered when saving the profile if (sample.frames.length >= 1 && sample.frames[0] != rootIndex) - sample.frames.splice(0, 0, rootIndex) + sample.frames.unshift(rootIndex) } } } progressReporter.finish(); - var profileID = gNextProfileID++; + // Don't increment the profile ID now because (1) it's buggy + // and (2) for now there's no point in storing each profile + // here if we're storing them in the local storage. + //var profileID = gNextProfileID++; + var profileID = gNextProfileID; gProfiles[profileID] = JSON.parse(JSON.stringify({ meta: meta, symbols: symbols, functions: functions, + resources: resources, allSamples: samples })); clearRegExpLastMatch(); @@ -619,7 +705,8 @@ function parseRawProfile(requestID, params, rawProfile) { numSamples: samples.length, profileID: profileID, symbols: symbols, - functions: functions + functions: functions, + resources: resources }); } @@ -688,7 +775,6 @@ function convertToCallTree(samples, isReverse) { function areSamplesMultiroot(samples) { var previousRoot; for (var i = 0; i < samples.length; ++i) { - if (!samples[i].frames) continue; if (!previousRoot) { previousRoot = samples[i].frames[0]; continue; @@ -706,8 +792,6 @@ function convertToCallTree(samples, isReverse) { return new TreeNode("(empty)", null, 0); var firstRoot = null; for (var i = 0; i < samples.length; ++i) { - if (!samples[i].frames) continue; - sendError(null, "got root: " + samples[i].frames[0]); firstRoot = samples[i].frames[0]; break; } @@ -718,9 +802,6 @@ function convertToCallTree(samples, isReverse) { var treeRoot = new TreeNode((isReverse || multiRoot) ? "(total)" : firstRoot, null, 0); for (var i = 0; i < samples.length; ++i) { var sample = samples[i]; - if (!sample.frames) { - continue; - } var callstack = sample.frames.slice(0); callstack.shift(); if (isReverse) @@ -764,68 +845,72 @@ function filterBySymbol(samples, symbolOrFunctionIndex) { }); } -function filterByCallstackPrefix(samples, callstack) { - return samples.map(function filterSample(origSample) { - if (!origSample) +function filterByCallstackPrefix(samples, symbols, functions, callstack, appliesToJS, useFunctions) { + var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { + return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); + } : function isJSSymbolOrRoot(symbolIndex) { + return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); + }; + return samples.map(function filterSample(sample) { + if (!sample) return null; - if (origSample.frames.length < callstack.length) + if (sample.frames.length < callstack.length) return null; - var sample = cloneSample(origSample); - for (var i = 0; i < callstack.length; i++) { - if (sample.frames[i] != callstack[i]) + for (var i = 0, j = 0; j < callstack.length; i++) { + if (i >= sample.frames.length) return null; + if (appliesToJS && !isJSFrameOrRoot(sample.frames[i])) + continue; + if (sample.frames[i] != callstack[j]) + return null; + j++; } - sample.frames = sample.frames.slice(callstack.length - 1); - return sample; + return makeSample(sample.frames.slice(i - 1), sample.extraInfo); }); } -function filterByCallstackPostfix(samples, callstack) { - return samples.map(function filterSample(origSample) { - if (!origSample) +function filterByCallstackPostfix(samples, symbols, functions, callstack, appliesToJS, useFunctions) { + var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { + return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); + } : function isJSSymbolOrRoot(symbolIndex) { + return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); + }; + return samples.map(function filterSample(sample) { + if (!sample) return null; - if (origSample.frames.length < callstack.length) + if (sample.frames.length < callstack.length) return null; - var sample = cloneSample(origSample); - for (var i = 0; i < callstack.length; i++) { - if (sample.frames[sample.frames.length - i - 1] != callstack[i]) + for (var i = 0, j = 0; j < callstack.length; i++) { + if (i >= sample.frames.length) return null; + if (appliesToJS && !isJSFrameOrRoot(sample.frames[sample.frames.length - i - 1])) + continue; + if (sample.frames[sample.frames.length - i - 1] != callstack[j]) + return null; + j++; } - sample.frames = sample.frames.slice(0, sample.frames.length - callstack.length + 1); - return sample; + var newFrames = sample.frames.slice(0, sample.frames.length - i + 1); + return makeSample(newFrames, sample.extraInfo); }); } function chargeNonJSToCallers(samples, symbols, functions, useFunctions) { - function isJSFrame(index, useFunction) { - if (useFunctions) { - if (!(index in functions)) - return ""; - return functions[index].isJSFrame; - } - if (!(index in symbols)) - return ""; - return symbols[index].isJSFrame; - } + var isJSFrameOrRoot = useFunctions ? function isJSFunctionOrRoot(functionIndex) { + return (functionIndex in functions) && (functions[functionIndex].isJSFrame || functions[functionIndex].isRoot); + } : function isJSSymbolOrRoot(symbolIndex) { + return (symbolIndex in symbols) && (symbols[symbolIndex].isJSFrame || symbols[symbolIndex].isRoot); + }; samples = samples.slice(0); for (var i = 0; i < samples.length; ++i) { var sample = samples[i]; if (!sample) continue; - var callstack = sample.frames; - var newFrames = []; - for (var j = 0; j < callstack.length; ++j) { - if (isJSFrame(callstack[j], useFunctions)) { - // Record Javascript frames - newFrames.push(callstack[j]); - } - } + var newFrames = sample.frames.filter(isJSFrameOrRoot); if (!newFrames.length) { - newFrames = null; + samples[i] = null; } else { - newFrames.splice(0, 0, "(total)"); + samples[i].frames = newFrames; } - samples[i].frames = newFrames; } return samples; } @@ -912,26 +997,28 @@ function FocusedFrameSampleFilter(focusedSymbol) { this._focusedSymbol = focusedSymbol; } FocusedFrameSampleFilter.prototype = { - filter: function FocusedFrameSampleFilter_filter(samples, symbols, functions) { + filter: function FocusedFrameSampleFilter_filter(samples, symbols, functions, useFunctions) { return filterBySymbol(samples, this._focusedSymbol); } }; -function FocusedCallstackPrefixSampleFilter(focusedCallstack) { +function FocusedCallstackPrefixSampleFilter(focusedCallstack, appliesToJS) { this._focusedCallstackPrefix = focusedCallstack; + this._appliesToJS = appliesToJS; } FocusedCallstackPrefixSampleFilter.prototype = { - filter: function FocusedCallstackPrefixSampleFilter_filter(samples, symbols, functions) { - return filterByCallstackPrefix(samples, this._focusedCallstackPrefix); + filter: function FocusedCallstackPrefixSampleFilter_filter(samples, symbols, functions, useFunctions) { + return filterByCallstackPrefix(samples, symbols, functions, this._focusedCallstackPrefix, this._appliesToJS, useFunctions); } }; -function FocusedCallstackPostfixSampleFilter(focusedCallstack) { +function FocusedCallstackPostfixSampleFilter(focusedCallstack, appliesToJS) { this._focusedCallstackPostfix = focusedCallstack; + this._appliesToJS = appliesToJS; } FocusedCallstackPostfixSampleFilter.prototype = { - filter: function FocusedCallstackPostfixSampleFilter_filter(samples, symbols, functions) { - return filterByCallstackPostfix(samples, this._focusedCallstackPostfix); + filter: function FocusedCallstackPostfixSampleFilter_filter(samples, symbols, functions, useFunctions) { + return filterByCallstackPostfix(samples, symbols, functions, this._focusedCallstackPostfix, this._appliesToJS, useFunctions); } }; @@ -951,9 +1038,9 @@ function unserializeSampleFilters(filters) { case "FocusedFrameSampleFilter": return new FocusedFrameSampleFilter(filter.focusedSymbol); case "FocusedCallstackPrefixSampleFilter": - return new FocusedCallstackPrefixSampleFilter(filter.focusedCallstack); + return new FocusedCallstackPrefixSampleFilter(filter.focusedCallstack, filter.appliesToJS); case "FocusedCallstackPostfixSampleFilter": - return new FocusedCallstackPostfixSampleFilter(filter.focusedCallstack); + return new FocusedCallstackPostfixSampleFilter(filter.focusedCallstack, filter.appliesToJS); case "RangeSampleFilter": return new RangeSampleFilter(filter.start, filter.end); case "PluginView": @@ -975,14 +1062,6 @@ function updateFilters(requestID, profileID, filters) { if (filters.mergeFunctions) { samples = discardLineLevelInformation(samples, symbols, functions); } - if (filters.javascriptOnly) { - try { - //samples = filterByName(samples, symbols, functions, "runScript", filters.mergeFunctions); - samples = chargeNonJSToCallers(samples, symbols, functions, filters.mergeFunctions); - } catch (e) { - dump("Could not filer by javascript: " + e + "\n"); - } - } if (filters.nameFilter) { try { samples = filterByName(samples, symbols, functions, filters.nameFilter, filters.mergeFunctions); @@ -992,16 +1071,19 @@ function updateFilters(requestID, profileID, filters) { } samples = unserializeSampleFilters(filters.sampleFilters).reduce(function (filteredSamples, currentFilter) { if (currentFilter===null) return filteredSamples; - return currentFilter.filter(filteredSamples, symbols, functions); + return currentFilter.filter(filteredSamples, symbols, functions, filters.mergeFunctions); }, samples); if (filters.jankOnly) { samples = filterByJank(samples, gJankThreshold); } + if (filters.javascriptOnly) { + samples = chargeNonJSToCallers(samples, symbols, functions, filters.mergeFunctions); + } gProfiles[profileID].filterSettings = filters; gProfiles[profileID].filteredSamples = samples; sendFinishedInChunks(requestID, samples, 40000, - function (sample) { return (sample && sample.frames) ? sample.frames.length : 1; }); + function (sample) { return sample ? sample.frames.length : 1; }); } function updateViewOptions(requestID, profileID, options) { @@ -1039,7 +1121,7 @@ function calculateHistogramData(requestID, profileID) { for (var i = 0; i < data.length; ++i) { if (!data[i]) continue; - var value = data[i].frames ? data[i].frames.length : 0; + var value = data[i].frames.length; if (maxHeight < value) maxHeight = value; } @@ -1053,7 +1135,7 @@ function calculateHistogramData(requestID, profileID) { var frameStart = {}; for (var i = 0; i < data.length; i++) { var step = data[i]; - if (!step || !step.frames) { + if (!step) { // Add a gap for the sample that was filtered out. nextX += 1 / samplesPerStep; continue; @@ -1139,6 +1221,26 @@ var diagnosticList = [ ; }, }, + { + image: "cache.png", + title: "Bug 717761 - Main thread can be blocked by IO on the cache thread", + bugNumber: "717761", + check: function(frames, symbols, meta) { + + return stepContains('nsCacheEntryDescriptor::GetStoragePolicy', frames, symbols) + ; + }, + }, + { + image: "js.png", + title: "Web Content Shutdown Notification", + check: function(frames, symbols, meta) { + + return stepContains('nsAppStartup::Quit', frames, symbols) + && stepContains('nsDocShell::FirePageHideNotification', frames, symbols) + ; + }, + }, { image: "js.png", title: "Bug 789193 - AMI_startup() takes 200ms on startup", @@ -1149,6 +1251,44 @@ var diagnosticList = [ ; }, }, + { + image: "js.png", + title: "Bug 818296 - [Shutdown] js::NukeCrossCompartmentWrappers takes up 300ms on shutdown", + bugNumber: "818296", + check: function(frames, symbols, meta) { + return stepContains('js::NukeCrossCompartmentWrappers', frames, symbols) + && (stepContains('WindowDestroyedEvent', frames, symbols) || stepContains('DoShutdown', frames, symbols)) + ; + }, + }, + { + image: "js.png", + title: "Bug 818274 - [Shutdown] Telemetry takes ~10ms on shutdown", + bugNumber: "818274", + check: function(frames, symbols, meta) { + return stepContains('TelemetryPing.js', frames, symbols) + ; + }, + }, + { + image: "plugin.png", + title: "Bug 818265 - [Shutdown] Plug-in shutdown takes ~90ms on shutdown", + bugNumber: "818265", + check: function(frames, symbols, meta) { + return stepContains('PluginInstanceParent::Destroy', frames, symbols) + ; + }, + }, + { + image: "snapshot.png", + title: "Bug 720575 - Make thumbnailing faster and/or asynchronous", + bugNumber: "720575", + check: function(frames, symbols, meta) { + return stepContains('Thumbnails_capture()', frames, symbols) + ; + }, + }, + { image: "js.png", title: "Bug 789185 - LoginManagerStorage_mozStorage.init() takes 300ms on startup ", @@ -1238,7 +1378,6 @@ var diagnosticList = [ var ccEvent = findCCEvent(frames, symbols, meta, step); if (ccEvent) { - dump("Found\n"); return true; } return false; @@ -1268,10 +1407,21 @@ var diagnosticList = [ check: function(frames, symbols, meta) { return stepContainsRegEx(/.*Collect.*Runtime.*Invocation.*/, frames, symbols) || stepContains('GarbageCollectNow', frames, symbols) // Label + || stepContains('JS_GC(', frames, symbols) // Label || stepContains('CycleCollect__', frames, symbols) // Label ; }, }, + { + image: "cc.png", + title: "Cycle Collect", + check: function(frames, symbols, meta) { + return stepContains('nsCycleCollector::Collect', frames, symbols) + || stepContains('CycleCollect__', frames, symbols) // Label + || stepContains('nsCycleCollectorRunner::Collect', frames, symbols) // Label + ; + }, + }, { image: "plugin.png", title: "Sync Plugin Constructor", @@ -1296,6 +1446,7 @@ var diagnosticList = [ check: function(frames, symbols, meta) { return stepContains('__getdirentries64', frames, symbols) || stepContains('__open', frames, symbols) + || stepContains('NtFlushBuffersFile', frames, symbols) || stepContains('storage:::Statement::ExecuteStep', frames, symbols) || stepContains('__unlink', frames, symbols) || stepContains('fsync', frames, symbols) @@ -1371,6 +1522,8 @@ function findGCSlice(frames, symbols, meta, step) { } function stepContains(substring, frames, symbols) { for (var i = 0; frames && i < frames.length; i++) { + if (!(frames[i] in symbols)) + continue; var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; if (frameSym.indexOf(substring) != -1) { return true; @@ -1380,6 +1533,8 @@ function stepContains(substring, frames, symbols) { } function stepContainsRegEx(regex, frames, symbols) { for (var i = 0; frames && i < frames.length; i++) { + if (!(frames[i] in symbols)) + continue; var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; if (regex.exec(frameSym)) { return true; @@ -1390,6 +1545,8 @@ function stepContainsRegEx(regex, frames, symbols) { function symbolSequence(symbolsOrder, frames, symbols) { var symbolIndex = 0; for (var i = 0; frames && i < frames.length; i++) { + if (!(frames[i] in symbols)) + continue; var frameSym = symbols[frames[i]].functionName || symbols[frames[i]].symbolName; var substring = symbolsOrder[symbolIndex]; if (frameSym.indexOf(substring) != -1) { @@ -1458,14 +1615,13 @@ function calculateDiagnosticItems(requestID, profileID, meta) { */ data.forEach(function diagnoseStep(step, x) { - if (!step) - return; + if (step) { + var frames = step.frames; - var frames = step.frames; - - var diagnostic = firstMatch(diagnosticList, function (diagnostic) { - return diagnostic.check(frames, symbols, meta, step); - }); + var diagnostic = firstMatch(diagnosticList, function (diagnostic) { + return diagnostic.check(frames, symbols, meta, step); + }); + } if (!diagnostic) { finishPendingDiagnostic(x); diff --git a/browser/devtools/profiler/cleopatra/js/tree.js b/browser/devtools/profiler/cleopatra/js/tree.js index dbd94d7c52d..07d7a9f9a5f 100755 --- a/browser/devtools/profiler/cleopatra/js/tree.js +++ b/browser/devtools/profiler/cleopatra/js/tree.js @@ -20,24 +20,20 @@ RegExp.escape = function(text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } -var requestAnimationFrame_timeout = null; var requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback, element) { - window.setTimeout(callback, 1000 / 60); + return window.setTimeout(callback, 1000 / 60); }; var cancelAnimationFrame = window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || window.msCancelAnimationFrame || - function(callback, element) { - if (requestAnimationFrame_timeout) { - window.clearTimeout(requestAnimationFrame_timeout); - requestAnimationFrame_timeout = null; - } + function(req) { + window.clearTimeout(req); }; function TreeView() { @@ -65,6 +61,10 @@ function TreeView() { this._horizontalScrollbox.className = "treeViewHorizontalScrollbox"; this._verticalScrollbox.appendChild(this._horizontalScrollbox); + this._styleElement = document.createElement("style"); + this._styleElement.setAttribute("type", "text/css"); + this._container.appendChild(this._styleElement); + this._contextMenu = document.createElement("menu"); this._contextMenu.setAttribute("type", "context"); this._contextMenu.id = "contextMenuForTreeView" + TreeView.instanceCounter++; @@ -74,11 +74,17 @@ function TreeView() { this._busyCover.className = "busyCover"; this._container.appendChild(this._busyCover); this._abortToggleAll = false; + this.initSelection = true; var self = this; this._container.onkeydown = function (e) { self._onkeypress(e); }; + this._container.onkeypress = function (e) { + // on key down gives us '8' and mapping shift+8='*' may not be portable. + if (String.fromCharCode(e.charCode) == '*') + self._onkeypress(e); + }; this._container.onclick = function (e) { self._onclick(e); }; @@ -106,9 +112,11 @@ TreeView.prototype = { dataIsOutdated: function TreeView_dataIsOutdated() { this._busyCover.classList.add("busy"); }, - display: function TreeView_display(data, filterByName) { + display: function TreeView_display(data, resources, filterByName) { this._busyCover.classList.remove("busy"); this._filterByName = filterByName; + this._resources = resources; + this._addResourceIconStyles(); this._filterByNameReg = null; // lazy init if (this._filterByName === "") this._filterByName = null; @@ -126,9 +134,12 @@ TreeView.prototype = { data: data[0].getData() }); this._processPendingActionsChunk(); - this._select(this._horizontalScrollbox.firstChild); - this._toggle(this._horizontalScrollbox.firstChild); - this._container.focus(); + if (this._initSelection === true) { + this._initSelection = false; + this._select(this._horizontalScrollbox.firstChild); + this._toggle(this._horizontalScrollbox.firstChild); + } + changeFocus(this._container); }, // Provide a snapshot of the reverse selection to restore with 'invert callback' getReverseSelectionSnapshot: function TreeView__getReverseSelectionSnapshot(isJavascriptOnly) { @@ -142,8 +153,8 @@ TreeView.prototype = { snapshot.push(curr.name); //dump(JSON.stringify(curr.name) + "\n"); } - if (curr.children && curr.children.length >= 1) { - curr = curr.children[0].getData(); + if (curr.treeChildren && curr.treeChildren.length >= 1) { + curr = curr.treeChildren[0].getData(); } else { break; } @@ -171,6 +182,7 @@ TreeView.prototype = { }, // Take a selection snapshot and restore the selection restoreSelectionSnapshot: function TreeView_restoreSelectionSnapshot(snapshot, allowNonContigious) { + //console.log("restore selection: " + JSON.stringify(snapshot)); var currNode = this._horizontalScrollbox.firstChild; if (currNode.data.name == snapshot[0] || snapshot[0] == "(total)") { snapshot.shift(); @@ -181,7 +193,7 @@ TreeView.prototype = { this._syncProcessPendingActionProcessing(); for (var i = 0; i < currNode.treeChildren.length; i++) { if (currNode.treeChildren[i].data.name == snapshot[0]) { - //dump("Found: " + currNode.treeChildren[i].data.name + "\n"); + //console.log("Found: " + currNode.treeChildren[i].data.name + "\n"); snapshot.shift(); this._toggle(currNode, false, true); currNode = currNode.treeChildren[i]; @@ -193,11 +205,11 @@ TreeView.prototype = { var pendingSearch = [currNode.data]; while (pendingSearch.length > 0) { var node = pendingSearch.shift(); - //dump("searching: " + node.name + " for: " + snapshot[0] + "\n"); - if (!node.children) + //console.log("searching: " + node.name + " for: " + snapshot[0] + "\n"); + if (!node.treeChildren) continue; - for (var i = 0; i < node.children.length; i++) { - var childNode = node.children[i].getData(); + for (var i = 0; i < node.treeChildren.length; i++) { + var childNode = node.treeChildren[i].getData(); if (childNode.name == snapshot[0]) { //dump("found: " + childNode.name + "\n"); snapshot.shift(); @@ -317,7 +329,13 @@ TreeView.prototype = { }; }, _scrollHeightChanged: function TreeView__scrollHeightChanged() { - this._leftColumnBackground.style.height = this._horizontalScrollbox.getBoundingClientRect().height + 'px'; + if (!this._pendingScrollHeightChanged) { + var self = this; + this._pendingScrollHeightChanged = setTimeout(function() { + self._leftColumnBackground.style.height = self._horizontalScrollbox.getBoundingClientRect().height + 'px'; + self._pendingScrollHeightChanged = null; + }, 0); + } }, _createTree: function TreeView__createTree(parentElement, parentNode, data) { var div = document.createElement("div"); @@ -328,27 +346,49 @@ TreeView.prototype = { var treeLine = document.createElement("div"); treeLine.className = "treeLine"; treeLine.innerHTML = this._HTMLForFunction(data); + div.depth = parentNode ? parentNode.depth + 1 : 0; + div.style.marginLeft = div.depth + "em"; // When this item is toggled we will expand its children div.pendingExpand = []; div.treeLine = treeLine; div.data = data; + // Useful for debugging + //this.uniqueID = this.uniqueID || 0; + //div.id = "Node" + this.uniqueID++; div.appendChild(treeLine); div.treeChildren = []; div.treeParent = parentNode; if (hasChildren) { - var parent = document.createElement("div"); - parent.className = "treeViewNodeList"; for (var i = 0; i < data.children.length; ++i) { - div.pendingExpand.push({parentElement: parent, parentNode: div, data: data.children[i].getData() }); + div.pendingExpand.push({parentElement: this._horizontalScrollbox, parentNode: div, data: data.children[i].getData() }); } - div.appendChild(parent); } if (parentNode) { parentNode.treeChildren.push(div); } - parentElement.appendChild(div); + if (parentNode != null) { + var nextTo; + if (parentNode.treeChildren.length > 1) { + nextTo = parentNode.treeChildren[parentNode.treeChildren.length-2].nextSibling; + } else { + nextTo = parentNode.nextSibling; + } + parentElement.insertBefore(div, nextTo); + } else { + parentElement.appendChild(div); + } return div; }, + _addResourceIconStyles: function TreeView__addResourceIconStyles() { + var styles = []; + for (var resourceName in this._resources) { + var resource = this._resources[resourceName]; + if (resource.icon) { + styles.push('.resourceIcon[data-resource="' + resourceName + '"] { background-image: url("' + resource.icon + '"); }'); + } + } + this._styleElement.textContent = styles.join("\n"); + }, _populateContextMenu: function TreeView__populateContextMenu(event) { this._verticalScrollbox.setAttribute("contextmenu", ""); @@ -385,9 +425,9 @@ TreeView.prototype = { _contextMenuForFunction: function TreeView__contextMenuForFunction(node) { // TODO move me outside tree.js var menu = []; - if (node.library != null && ( - node.library.toLowerCase() == "xul" || - node.library.toLowerCase() == "xul.dll" + if (node.library && ( + node.library.toLowerCase() == "lib_xul" || + node.library.toLowerCase() == "lib_xul.dll" )) { menu.push("View Source"); } @@ -403,7 +443,8 @@ TreeView.prototype = { }, _HTMLForFunction: function TreeView__HTMLForFunction(node) { var nodeName = escapeHTML(node.name); - var libName = node.library; + var resource = this._resources[node.library] || {}; + var libName = escapeHTML(resource.name || ""); if (this._filterByName) { if (!this._filterByNameReg) { this._filterByName = RegExp.escape(this._filterByName); @@ -422,6 +463,7 @@ TreeView.prototype = { '' + node.counter + ' ' + '' + samplePercentage + ' ' + '' + node.selfCounter + ' ' + + ' ' + '' + nodeName + '' + '' + libName + '' + ''; @@ -434,15 +476,26 @@ TreeView.prototype = { this._schedulePendingActionProcessing(); } }, + _showChild: function TreeView__showChild(div, isVisible) { + for (var i = 0; i < div.treeChildren.length; i++) { + div.treeChildren[i].style.display = isVisible?"":"none"; + if (!isVisible) { + div.treeChildren[i].classList.add("collapsed"); + this._showChild(div.treeChildren[i], isVisible); + } + } + }, _toggle: function TreeView__toggle(div, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) { var currentCollapsedValue = this._isCollapsed(div); if (newCollapsedValue === undefined) newCollapsedValue = !currentCollapsedValue; if (newCollapsedValue) { div.classList.add("collapsed"); + this._showChild(div, false); } else { this._resolveChildren(div, true); div.classList.remove("collapsed"); + this._showChild(div, true); } if (!suppressScrollHeightNotification) this._scrollHeightChanged(); @@ -500,6 +553,19 @@ TreeView.prototype = { return this._getNextSib(div.treeParent); return div.treeParent.treeChildren[nodeIndex+1]; }, + _scheduleScrollIntoView: function TreeView__scheduleScrollIntoView(element, maxImportantWidth) { + // Schedule this on the animation frame otherwise we may run this more then once per frames + // causing more work then needed. + var self = this; + if (self._pendingAnimationFrame != null) { + return; + } + self._pendingAnimationFrame = requestAnimationFrame(function anim_frame() { + cancelAnimationFrame(self._pendingAnimationFrame); + self._pendingAnimationFrame = null; + self._scrollIntoView(element, maxImportantWidth); + }); + }, _scrollIntoView: function TreeView__scrollIntoView(element, maxImportantWidth) { // Make sure that element is inside the visible part of our scrollbox by // adjusting the scroll positions. If element is wider or @@ -541,9 +607,10 @@ TreeView.prototype = { li.treeLine.classList.add("selected"); this._selectedNode = li; var functionName = li.treeLine.querySelector(".functionName"); - this._scrollIntoView(functionName, 400); + this._scheduleScrollIntoView(functionName, 400); this._fireEvent("select", li.data); } + updateDocumentURL(); }, _isCollapsed: function TreeView__isCollapsed(div) { return div.classList.contains("collapsed"); @@ -615,6 +682,7 @@ TreeView.prototype = { var isCollapsed = this._isCollapsed(selected); if (isCollapsed) { this._toggle(selected); + this._syncProcessPendingActionProcessing(); } else { // Do KEY_DOWN var nextSib = this._getNextSib(selected); diff --git a/browser/devtools/profiler/cleopatra/js/ui.js b/browser/devtools/profiler/cleopatra/js/ui.js index f611f2121a8..f34d1d07688 100755 --- a/browser/devtools/profiler/cleopatra/js/ui.js +++ b/browser/devtools/profiler/cleopatra/js/ui.js @@ -59,26 +59,45 @@ FileList.prototype = { return this._container; }, + clearFiles: function FileList_clearFiles() { + this.fileItemList = []; + this._selectedFileItem = null; + this._container.innerHTML = ""; + }, + loadProfileListFromLocalStorage: function FileList_loadProfileListFromLocalStorage() { var self = this; gLocalStorage.getProfileList(function(profileList) { - for (var i = 0; i < profileList.length; i++) { + for (var i = profileList.length - 1; i >= 0; i--) { (function closure() { // This only carries info about the profile and the access key to retrieve it. var profileInfo = profileList[i]; //PROFILERTRACE("Profile list from local storage: " + JSON.stringify(profileInfo)); - var fileEntry = self.addFile(profileInfo.profileKey, "local storage", function fileEntryClick() { + var dateObj = new Date(profileInfo.date); + var fileEntry = self.addFile(profileInfo, dateObj.toLocaleString(), function fileEntryClick() { + PROFILERLOG("open: " + profileInfo.profileKey + "\n"); loadLocalStorageProfile(profileInfo.profileKey); }); })(); } }); + gLocalStorage.onProfileListChange(function(profileList) { + self.clearFiles(); + self.loadProfileListFromLocalStorage(); + }); }, - addFile: function FileList_addFile(fileName, description, onselect) { + addFile: function FileList_addFile(profileInfo, description, onselect) { var li = document.createElement("li"); - li.fileName = fileName || "New Profile"; + var fileName; + if (profileInfo.profileKey && profileInfo.profileKey.indexOf("http://profile-store.commondatastorage.googleapis.com/") >= 0) { + fileName = profileInfo.profileKey.substring(54); + fileName = fileName.substring(0, 8) + "..." + fileName.substring(28); + } else { + fileName = profileInfo.name; + } + li.fileName = fileName || "(New Profile)"; li.description = description || "(empty)"; li.className = "fileListItem"; @@ -87,10 +106,11 @@ FileList.prototype = { this._selectedFileItem = li; } - li.onselect = onselect; var self = this; li.onclick = function() { self.setSelection(li); + if (onselect) + onselect(); } var fileListItemTitleSpan = document.createElement("span"); @@ -121,8 +141,8 @@ FileList.prototype = { }, profileParsingFinished: function FileList_profileParsingFinished() { - this._container.querySelector(".fileListItemTitle").textContent = "Current Profile"; - this._container.querySelector(".fileListItemDescription").textContent = gNumSamples + " Samples"; + //this._container.querySelector(".fileListItemTitle").textContent = "Current Profile"; + //this._container.querySelector(".fileListItemDescription").textContent = gNumSamples + " Samples"; } } @@ -135,11 +155,15 @@ function ProfileTreeManager() { this.treeView.setColumns([ { name: "sampleCount", title: "Running time" }, { name: "selfSampleCount", title: "Self" }, - { name: "symbolName", title: "Symbol Name"}, + { name: "resource", title: "" }, + { name: "symbolName", title: "Symbol Name"} ]); var self = this; this.treeView.addEventListener("select", function (frameData) { self.highlightFrame(frameData); + if (window.comparator_setSelection) { + window.comparator_setSelection(gTreeManager.serializeCurrentSelectionSnapshot(), frameData); + } }); this.treeView.addEventListener("contextMenuClick", function (e) { self._onContextMenuClick(e); @@ -165,12 +189,21 @@ ProfileTreeManager.prototype = { dataIsOutdated: function ProfileTreeManager_dataIsOutdated() { this.treeView.dataIsOutdated(); }, - saveSelectionSnapshot: function ProfileTreeManager_getSelectionSnapshot(isJavascriptOnly) { + saveSelectionSnapshot: function ProfileTreeManager_saveSelectionSnapshot(isJavascriptOnly) { this._savedSnapshot = this.treeView.getSelectionSnapshot(isJavascriptOnly); }, - saveReverseSelectionSnapshot: function ProfileTreeManager_getReverseSelectionSnapshot(isJavascriptOnly) { + saveReverseSelectionSnapshot: function ProfileTreeManager_saveReverseSelectionSnapshot(isJavascriptOnly) { this._savedSnapshot = this.treeView.getReverseSelectionSnapshot(isJavascriptOnly); }, + hasNonTrivialSelection: function ProfileTreeManager_hasNonTrivialSelection() { + return this.treeView.getSelectionSnapshot().length > 1; + }, + serializeCurrentSelectionSnapshot: function ProfileTreeManager_serializeCurrentSelectionSnapshot() { + return JSON.stringify(this.treeView.getSelectionSnapshot()); + }, + restoreSerializedSelectionSnapshot: function ProfileTreeManager_restoreSerializedSelectionSnapshot(selection) { + this._savedSnapshot = JSON.parse(selection); + }, _restoreSelectionSnapshot: function ProfileTreeManager__restoreSelectionSnapshot(snapshot, allowNonContigous) { return this.treeView.restoreSelectionSnapshot(snapshot, allowNonContigous); }, @@ -231,11 +264,12 @@ ProfileTreeManager.prototype = { setAllowNonContigous: function ProfileTreeManager_setAllowNonContigous() { this._allowNonContigous = true; }, - display: function ProfileTreeManager_display(tree, symbols, functions, useFunctions, filterByName) { - this.treeView.display(this.convertToJSTreeData(tree, symbols, functions, useFunctions), filterByName); + display: function ProfileTreeManager_display(tree, symbols, functions, resources, useFunctions, filterByName) { + this.treeView.display(this.convertToJSTreeData(tree, symbols, functions, useFunctions), resources, filterByName); if (this._savedSnapshot) { + var old = this._savedSnapshot.clone(); this._restoreSelectionSnapshot(this._savedSnapshot, this._allowNonContigous); - this._savedSnapshot = null; + this._savedSnapshot = old; this._allowNonContigous = false; } }, @@ -252,7 +286,7 @@ ProfileTreeManager.prototype = { curObj.selfCounter = selfCounter; curObj.ratio = node.counter / totalSamples; curObj.fullFrameNamesAsInSample = node.mergedNames ? node.mergedNames : [node.name]; - if (useFunctions ? !(node.name in functions) : !(node.name in symbols)) { + if (!(node.name in (useFunctions ? functions : symbols))) { curObj.name = node.name; curObj.library = ""; } else { @@ -261,7 +295,7 @@ ProfileTreeManager.prototype = { functionName: functionObj.functionName, libraryName: functionObj.libraryName, lineInformation: useFunctions ? "" : symbols[node.name].lineInformation - }; + }; curObj.name = (info.functionName + " " + info.lineInformation).trim(); curObj.library = info.libraryName; curObj.isJSFrame = functionObj.isJSFrame; @@ -281,7 +315,7 @@ ProfileTreeManager.prototype = { return { getData: function () { if (!createdNode) { - createdNode = createTreeViewNode(child, parent); + createdNode = createTreeViewNode(child, parent); } return createdNode; } @@ -302,7 +336,7 @@ function SampleBar() { this._header.alt = "This shows the heaviest leaf of the selected sample. Use this to get a quick glimpse of where the selection is spending most of its time."; this._container.appendChild(this._header); - this._text = document.createElement("span"); + this._text = document.createElement("ul"); this._text.style.whiteSpace = "pre"; this._text.innerHTML = "Sample text"; this._container.appendChild(this._text); @@ -322,11 +356,12 @@ SampleBar.prototype = { var functionObj = gMergeFunctions ? gFunctions[sample[i]] : gFunctions[symbols[sample[i]].functionIndex]; if (!functionObj) continue; + var functionItem = document.createElement("li"); var functionLink = document.createElement("a"); - functionLink.textContent = "- " + functionObj.functionName; + functionLink.textContent = functionLink.title = functionObj.functionName; functionLink.href = "#"; - this._text.appendChild(functionLink); - this._text.appendChild(document.createElement("br")); + functionItem.appendChild(functionLink); + this._text.appendChild(functionItem); list.push(functionObj.functionName); functionLink.selectIndex = i; functionLink.onclick = function() { @@ -378,11 +413,9 @@ PluginView.prototype = { this._iframe.src = "js/plugins/" + pluginName + "/index.html"; var self = this; this._iframe.onload = function() { - console.log("Pluginview '" + pluginName + " iframe onload"); self._iframe.contentWindow.initCleopatraPlugin(data, param, gSymbols); } this.show(); - //console.log(gSymbols); }, } @@ -417,6 +450,9 @@ HistogramView.prototype = { getContainer: function HistogramView_getContainer() { return this._container; }, + selectRange: function HistogramView_selectRange(start, end) { + this._rangeSelector._finishSelection(start, end); + }, showVideoFramePosition: function HistogramView_showVideoFramePosition(frame) { if (!this._frameStart || !this._frameStart[frame]) return; @@ -451,17 +487,14 @@ HistogramView.prototype = { return Math.ceil(minWidth / this._widthSum); }, histogramClick: function HistogramView_histogramClick(index) { - var sample = this._histogramData[index]; + var sample = this._histogramData[index]; var frames = sample.frames; - if (gSampleBar) { - var list = gSampleBar.setSample(frames[0]); - gTreeManager.setSelection(list); - setHighlightedCallstack(frames[0], frames[0]); - } + var list = gSampleBar.setSample(frames[0]); + gTreeManager.setSelection(list); + setHighlightedCallstack(frames[0], frames[0]); }, display: function HistogramView_display(histogramData, frameStart, widthSum, highlightedCallstack) { this._histogramData = histogramData; - PROFILERTRACE("FRAME START: " + frameStart + "\n"); this._frameStart = frameStart; this._widthSum = widthSum; this._widthMultiplier = this._calculateWidthMultiplier(); @@ -469,13 +502,26 @@ HistogramView.prototype = { this._render(highlightedCallstack); this._busyCover.classList.remove("busy"); }, + _scheduleRender: function HistogramView__scheduleRender(highlightedCallstack) { + var self = this; + if (self._pendingAnimationFrame != null) { + return; + } + self._pendingAnimationFrame = requestAnimationFrame(function anim_frame() { + cancelAnimationFrame(self._pendingAnimationFrame); + self._pendingAnimationFrame = null; + self._render(highlightedCallstack); + }); + }, _render: function HistogramView__render(highlightedCallstack) { var ctx = this._canvas.getContext("2d"); var height = this._canvas.height; ctx.setTransform(this._widthMultiplier, 0, 0, 1, 0, 0); + ctx.font = "20px Georgia"; ctx.clearRect(0, 0, this._widthSum, height); var self = this; + var markerCount = 0; for (var i = 0; i < this._histogramData.length; i++) { var step = this._histogramData[i]; var isSelected = self._isStepSelected(step, highlightedCallstack); @@ -489,12 +535,22 @@ HistogramView.prototype = { } var roundedHeight = Math.round(step.value * height); ctx.fillRect(step.x, height - roundedHeight, step.width, roundedHeight); + if (step.marker) { + var x = step.x + step.width + 2; + var endPoint = x + ctx.measureText(step.marker).width; + var lastDataPoint = this._histogramData[this._histogramData.length-1]; + if (endPoint >= lastDataPoint.x + lastDataPoint.width) { + x -= endPoint - (lastDataPoint.x + lastDataPoint.width) - 1; + } + ctx.fillText(step.marker, x, 15 + ((markerCount % 2) == 0 ? 0 : 20)); + markerCount++; + } } this._finishedRendering = true; }, highlightedCallstackChanged: function HistogramView_highlightedCallstackChanged(highlightedCallstack) { - this._render(highlightedCallstack); + this._scheduleRender(highlightedCallstack); }, _isInRangeSelector: function HistogramView_isInRangeSelector(index) { return false; @@ -502,29 +558,33 @@ HistogramView.prototype = { _isStepSelected: function HistogramView__isStepSelected(step, highlightedCallstack) { if ("marker" in step) return false; - return step.frames.some(function isCallstackSelected(frames) { + + search_frames: for (var i = 0; i < step.frames.length; i++) { + var frames = step.frames[i]; + if (frames.length < highlightedCallstack.length || highlightedCallstack.length <= (gInvertCallstack ? 0 : 1)) - return false; + continue; var compareFrames = frames; if (gInvertCallstack) { for (var j = 0; j < highlightedCallstack.length; j++) { var compareFrameIndex = compareFrames.length - 1 - j; if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) { - return false; + continue search_frames; } } } else { for (var j = 0; j < highlightedCallstack.length; j++) { var compareFrameIndex = j; if (highlightedCallstack[j] != compareFrames[compareFrameIndex]) { - return false; + continue search_frames; } } } return true; - }); + }; + return false; }, getHistogramData: function HistogramView__getHistogramData() { return this._histogramData; @@ -606,6 +666,9 @@ RangeSelector.prototype = { var isDrawingRectangle = false; var origX, origY; var self = this; + // Compute this on the mouse down rather then forcing a sync reflow + // every frame. + var boundingRect = null; function histogramClick(clickX, clickY) { clickX = Math.min(clickX, graph.parentNode.getBoundingClientRect().right); clickX = clickX - graph.parentNode.getBoundingClientRect().left; @@ -613,8 +676,8 @@ RangeSelector.prototype = { self._histogram.histogramClick(index); } function updateHiliteRectangle(newX, newY) { - newX = Math.min(newX, graph.parentNode.getBoundingClientRect().right); - var startX = Math.min(newX, origX) - graph.parentNode.getBoundingClientRect().left; + newX = Math.min(newX, boundingRect.right); + var startX = Math.min(newX, origX) - boundingRect.left; var startY = 0; var width = Math.abs(newX - origX); var height = graph.parentNode.clientHeight; @@ -637,6 +700,7 @@ RangeSelector.prototype = { self.beginHistogramSelection(); origX = e.pageX; origY = e.pageY; + boundingRect = graph.parentNode.getBoundingClientRect(); if (this.setCapture) this.setCapture(); // Reset the highlight rectangle @@ -659,14 +723,12 @@ RangeSelector.prototype = { var index = self._sampleIndexFromPoint(e.pageX - graph.parentNode.getBoundingClientRect().left); // TODO Select this sample in the tree view var sample = gCurrentlyShownSampleData[index]; - console.log("Should select: " + sample); } } }, false); graph.addEventListener("mousemove", function(e) { this._movedDuringClick = true; if (isDrawingRectangle) { - console.log(e.pageX); updateMouseMarker(-1); // Clear updateHiliteRectangle(e.pageX, e.pageY); } else { @@ -739,11 +801,12 @@ function videoPaneTimeChange(video) { //var frameStart = gMeta.frameStart[frame]; //var frameEnd = gMeta.frameStart[frame+1]; // If we don't have a frameEnd assume the end of the profile - gHistogramView.showVideoFramePosition(frame); + gHistogramView.showVideoFramePosition(frame); } window.onpopstate = function(ev) { + return; // Conflicts with document url if (!gBreadcrumbTrail) return; console.log("pop: " + JSON.stringify(ev.state)); @@ -820,10 +883,17 @@ BreadcrumbTrail.prototype = { if (this._breadcrumbs.length-2 >= 0) this._enter(this._breadcrumbs.length-2); }, - _enter: function BreadcrumbTrail__select(index) { + enterLastItem: function BreadcrumbTrail_enterLastItem(forceSelection) { + this._enter(this._breadcrumbs.length-1, forceSelection); + }, + _enter: function BreadcrumbTrail__select(index, forceSelection) { if (index == this._selectedBreadcrumbIndex) return; - gTreeManager.saveSelectionSnapshot(); + if (forceSelection) { + gTreeManager.restoreSerializedSelectionSnapshot(forceSelection); + } else { + gTreeManager.saveSelectionSnapshot(); + } var prevSelected = this._breadcrumbs[this._selectedBreadcrumbIndex]; if (prevSelected) prevSelected.classList.remove("selected"); @@ -935,7 +1005,7 @@ function copyProfile() { function saveProfileToLocalStorage() { Parser.getSerializedProfile(true, function (serializedProfile) { - gLocalStorage.storeLocalProfile(serializedProfile, function profileSaved() { + gLocalStorage.storeLocalProfile(serializedProfile, gMeta.profileId, function profileSaved() { }); }); @@ -947,36 +1017,114 @@ function downloadProfile() { }); } +function promptUploadProfile(selected) { + var overlay = document.createElement("div"); + overlay.style.position = "absolute"; + overlay.style.top = 0; + overlay.style.left = 0; + overlay.style.width = "100%"; + overlay.style.height = "100%"; + overlay.style.backgroundColor = "transparent"; + + var bg = document.createElement("div"); + bg.style.position = "absolute"; + bg.style.top = 0; + bg.style.left = 0; + bg.style.width = "100%"; + bg.style.height = "100%"; + bg.style.opacity = "0.6"; + bg.style.backgroundColor = "#aaaaaa"; + overlay.appendChild(bg); + + var contentDiv = document.createElement("div"); + contentDiv.className = "sideBar"; + contentDiv.style.position = "absolute"; + contentDiv.style.top = "50%"; + contentDiv.style.left = "50%"; + contentDiv.style.width = "40em"; + contentDiv.style.height = "20em"; + contentDiv.style.marginLeft = "-20em"; + contentDiv.style.marginTop = "-10em"; + contentDiv.style.padding = "10px"; + contentDiv.style.border = "2px solid black"; + contentDiv.style.backgroundColor = "rgb(219, 223, 231)"; + overlay.appendChild(contentDiv); + + var noticeHTML = ""; + noticeHTML += "

Upload Profile - Privacy Notice

"; + noticeHTML += "You're about to upload your profile publicly where anyone will be able to access it. "; + noticeHTML += "To better diagnose performance problems profiles include the following information:"; + noticeHTML += "
    "; + noticeHTML += "
  • The URLs and scripts of the tabs that were executing.
  • "; + noticeHTML += "
  • The metadata of all your Add-ons to identify slow Add-ons.
  • "; + noticeHTML += "
  • Firefox build and runtime configuration.
  • "; + noticeHTML += "

"; + noticeHTML += "To view all the information you can download the full profile to a file and open the json structure with a text editor.

"; + contentDiv.innerHTML = noticeHTML; + + var cancelButton = document.createElement("input"); + cancelButton.style.position = "absolute"; + cancelButton.style.bottom = "10px"; + cancelButton.type = "button"; + cancelButton.value = "Cancel"; + cancelButton.onclick = function() { + document.body.removeChild(overlay); + } + contentDiv.appendChild(cancelButton); + + var uploadButton = document.createElement("input"); + uploadButton.style.position = "absolute"; + uploadButton.style.right = "10px"; + uploadButton.style.bottom = "10px"; + uploadButton.type = "button"; + uploadButton.value = "Upload"; + uploadButton.onclick = function() { + document.body.removeChild(overlay); + uploadProfile(selected); + } + contentDiv.appendChild(uploadButton); + + document.body.appendChild(overlay); +} + function uploadProfile(selected) { Parser.getSerializedProfile(!selected, function (dataToUpload) { var oXHR = new XMLHttpRequest(); - oXHR.open("POST", "http://profile-store.appspot.com/store", true); oXHR.onload = function (oEvent) { - if (oXHR.status == 200) { - document.getElementById("upload_status").innerHTML = "Success! Use this link"; - } else { + if (oXHR.status == 200) { + gReportID = oXHR.responseText; + updateDocumentURL(); + document.getElementById("upload_status").innerHTML = "Success! Use this link"; + document.getElementById("linkElem").href = document.URL; + } else { document.getElementById("upload_status").innerHTML = "Error " + oXHR.status + " occurred uploading your file."; - } + } }; oXHR.onerror = function (oEvent) { document.getElementById("upload_status").innerHTML = "Error " + oXHR.status + " occurred uploading your file."; } - oXHR.onprogress = function (oEvent) { + oXHR.upload.onprogress = function(oEvent) { if (oEvent.lengthComputable) { - document.getElementById("upload_status").innerHTML = "Uploading: " + ((oEvent.loaded / oEvent.total)*100) + "%"; + var progress = Math.round((oEvent.loaded / oEvent.total)*100); + if (progress == 100) { + document.getElementById("upload_status").innerHTML = "Uploading: Waiting for server side compression"; + } else { + document.getElementById("upload_status").innerHTML = "Uploading: " + Math.round((oEvent.loaded / oEvent.total)*100) + "%"; + } } - } + }; var dataSize; if (dataToUpload.length > 1024*1024) { - dataSize = (dataToUpload.length/1024/1024) + " MB(s)"; + dataSize = (dataToUpload.length/1024/1024).toFixed(1) + " MB(s)"; } else { - dataSize = (dataToUpload.length/1024) + " KB(s)"; + dataSize = (dataToUpload.length/1024).toFixed(1) + " KB(s)"; } var formData = new FormData(); formData.append("file", dataToUpload); document.getElementById("upload_status").innerHTML = "Uploading Profile (" + dataSize + ")"; + oXHR.open("POST", "http://profile-store.appspot.com/store", true); oXHR.send(formData); }); } @@ -990,7 +1138,7 @@ function populate_skip_symbol() { elOptNew.value = gSkipSymbols[i]; elSel.add(elOptNew); } - + } function delete_skip_symbol() { @@ -998,7 +1146,7 @@ function delete_skip_symbol() { } function add_skip_symbol() { - + } var gFilterChangeCallback = null; @@ -1014,12 +1162,12 @@ function filterOnChange() { function filterUpdate() { gFilterChangeCallback = null; - filtersChanged(); + filtersChanged(); var filterNameInput = document.getElementById("filterName"); if (filterNameInput != null) { - filterNameInput.focus(); - } + changeFocus(filterNameInput); + } } // Maps document id to a tooltip description @@ -1037,7 +1185,7 @@ var tooltip = { function addTooltips() { for (var elemId in tooltip) { - var elem = document.getElementById(elemId); + var elem = document.getElementById(elemId); if (!elem) continue; if (elem.parentNode.nodeName.toLowerCase() == "label") @@ -1083,15 +1231,15 @@ InfoBar.prototype = { infoText += "

Pre Filtering

\n"; // Disable for now since it's buggy and not useful //infoText += "
\n"; - infoText += "
\n"; var filterNameInputOld = document.getElementById("filterName"); - infoText += "\n"; + infoText += "Filter:\n"; + infoText += "\n"; infoText += "

Post Filtering

\n"; infoText += "\n"; infoText += "

View Options

\n"; + infoText += "
\n"; infoText += "
\n"; infoText += "
\n"; @@ -1101,12 +1249,15 @@ InfoBar.prototype = { infoText += "
\n"; infoText += "\n"; + infoText += "

Compare

\n"; + infoText += "\n"; + //infoText += "
\n"; //infoText += "Skip functions:
\n"; //infoText += "
" //infoText += "
\n"; //infoText += "
\n"; - + infobar.innerHTML = infoText; addTooltips(); @@ -1114,13 +1265,19 @@ InfoBar.prototype = { if (filterNameInputOld != null && filterNameInputNew != null) { filterNameInputNew.parentNode.replaceChild(filterNameInputOld, filterNameInputNew); //filterNameInputNew.value = filterNameInputOld.value; + } else if (gQueryParamFilterName != null) { + filterNameInputNew.value = gQueryParamFilterName; + gQueryParamFilterName = null; + } + document.getElementById('compare').onclick = function() { + openProfileCompare(); } document.getElementById('upload').onclick = function() { - uploadProfile(false); + promptUploadProfile(false); }; document.getElementById('download').onclick = downloadProfile; document.getElementById('upload_select').onclick = function() { - uploadProfile(true); + promptUploadProfile(true); }; //document.getElementById('delete_skipsymbol').onclick = delete_skip_symbol; //document.getElementById('add_skipsymbol').onclick = add_skip_symbol; @@ -1129,13 +1286,13 @@ InfoBar.prototype = { } } -// in light mode we simplify the UI by default -var gLightMode = false; var gNumSamples = 0; var gMeta = null; var gSymbols = {}; var gFunctions = {}; +var gResources = {}; var gHighlightedCallstack = []; +var gFrameView = null; var gTreeManager = null; var gSampleBar = null; var gBreadcrumbTrail = null; @@ -1149,6 +1306,9 @@ var gMainArea = null; var gCurrentlyShownSampleData = null; var gSkipSymbols = ["test2", "test1"]; var gAppendVideoCapture = null; +var gQueryParamFilterName = null; +var gRestoreSelection = null; +var gReportID = null; function getTextData() { var data = []; @@ -1190,7 +1350,7 @@ function loadLocalStorageProfile(profileKey) { gLocalStorage.getProfile(profileKey, function(profile) { subreporters.fileLoading.finish(); - loadRawProfile(subreporters.parsing, JSON.stringify(profile)); + loadRawProfile(subreporters.parsing, profile, profileKey); }); subreporters.fileLoading.begin("Reading local storage..."); } @@ -1254,14 +1414,18 @@ function loadProfileURL(url) { xhr.open("GET", url, true); xhr.responseType = "text"; xhr.onreadystatechange = function (e) { - if (xhr.readyState === 4 && xhr.status === 200) { + if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 0)) { subreporters.fileLoading.finish(); PROFILERLOG("Got profile from '" + url + "'."); - loadRawProfile(subreporters.parsing, xhr.responseText); + if (xhr.responseText == null || xhr.responseText === "") { + subreporters.fileLoading.begin("Profile '" + url + "' is empty. Did you set the CORS headers?"); + return; + } + loadRawProfile(subreporters.parsing, xhr.responseText, url); } }; - xhr.onerror = function (e) { - subreporters.fileLoading.begin("Error fetching profile :(. URL: " + url); + xhr.onerror = function (e) { + subreporters.fileLoading.begin("Error fetching profile :(. URL: '" + url + "'. Did you set the CORS headers?"); } xhr.onprogress = function (e) { if (e.lengthComputable && (e.loaded <= e.total)) { @@ -1281,12 +1445,17 @@ function loadProfile(rawProfile) { loadRawProfile(reporter, rawProfile); } -function loadRawProfile(reporter, rawProfile) { +function loadRawProfile(reporter, rawProfile, profileId) { PROFILERLOG("Parse raw profile: ~" + rawProfile.length + " bytes"); reporter.begin("Parsing..."); + if (rawProfile == null || rawProfile.length === 0) { + reporter.begin("Profile is null or empty"); + return; + } var startTime = Date.now(); var parseRequest = Parser.parse(rawProfile, { - appendVideoCapture : gAppendVideoCapture, + appendVideoCapture : gAppendVideoCapture, + profileId: profileId, }); gVideoCapture = null; parseRequest.addEventListener("progress", function (progress, action) { @@ -1301,9 +1470,9 @@ function loadRawProfile(reporter, rawProfile) { gNumSamples = result.numSamples; gSymbols = result.symbols; gFunctions = result.functions; + gResources = result.resources; enterFinishedProfileUI(); - if (gFileList) - gFileList.profileParsingFinished(); + gFileList.profileParsingFinished(); }); } @@ -1330,16 +1499,9 @@ window.addEventListener("message", function messageFromAddon(msg) { case "importFromAddonFinish": importFromAddonFinish(o.rawProfile); break; - case "receiveProfileData": - receiveProfileData(o.rawProfile); - break; } }); -function receiveProfileData(data) { - loadProfile(JSON.stringify(data)); -} - function importFromAddonFinish(rawProfile) { gImportFromAddonSubreporters.import.finish(); loadRawProfile(gImportFromAddonSubreporters.parsing, rawProfile); @@ -1357,13 +1519,13 @@ function toggleInvertCallStack() { var gMergeUnbranched = false; function toggleMergeUnbranched() { gMergeUnbranched = !gMergeUnbranched; - viewOptionsChanged(); + viewOptionsChanged(); } var gMergeFunctions = true; function toggleMergeFunctions() { gMergeFunctions = !gMergeFunctions; - filtersChanged(); + filtersChanged(); } var gJankOnly = false; @@ -1393,7 +1555,7 @@ function toggleJavascriptOnly() { var gSampleFilters = []; function focusOnSymbol(focusSymbol, name) { - var newFilterChain = gSampleFilters.concat([{type: "FocusedFrameSampleFilter", focusedSymbol: focusSymbol}]); + var newFilterChain = gSampleFilters.concat([{type: "FocusedFrameSampleFilter", name: name, focusedSymbol: focusSymbol}]); gBreadcrumbTrail.addAndEnter({ title: name, enterCallback: function () { @@ -1403,10 +1565,16 @@ function focusOnSymbol(focusSymbol, name) { }); } -function focusOnCallstack(focusedCallstack, name) { +function focusOnCallstack(focusedCallstack, name, overwriteCallstack) { + var invertCallback = gInvertCallstack; + if (overwriteCallstack != null) { + invertCallstack = overwriteCallstack; + } var filter = { - type: gInvertCallstack ? "FocusedCallstackPostfixSampleFilter" : "FocusedCallstackPrefixSampleFilter", - focusedCallstack: focusedCallstack + type: !invertCallstack ? "FocusedCallstackPostfixSampleFilter" : "FocusedCallstackPrefixSampleFilter", + name: name, + focusedCallstack: focusedCallstack, + appliesToJS: gJavascriptOnly }; var newFilterChain = gSampleFilters.concat([filter]); gBreadcrumbTrail.addAndEnter({ @@ -1450,30 +1618,24 @@ function setHighlightedCallstack(samples, heaviestSample) { // Always show heavy heaviestSample = heaviestSample.clone().reverse(); } - if (gSampleBar) + + if (gSampleBar) { gSampleBar.setSample(heaviestSample); + } } -function enterMainUI(isLightMode) { - if (isLightMode !== undefined) { - gLightMode = isLightMode; - if (gLightMode) { - gJavascriptOnly = true; - } - } - +function enterMainUI() { var uiContainer = document.createElement("div"); uiContainer.id = "ui"; - //gFileList.loadProfileListFromLocalStorage(); - if (!gLightMode) { - gFileList = new FileList(); - uiContainer.appendChild(gFileList.getContainer()); + gFileList = new FileList(); + uiContainer.appendChild(gFileList.getContainer()); - gFileList.addFile(); - gInfoBar = new InfoBar(); - uiContainer.appendChild(gInfoBar.getContainer()); - } + //gFileList.addFile(); + gFileList.loadProfileListFromLocalStorage(); + + gInfoBar = new InfoBar(); + uiContainer.appendChild(gInfoBar.getContainer()); gMainArea = document.createElement("div"); gMainArea.id = "mainarea"; @@ -1522,9 +1684,7 @@ function enterProgressUI() { } function enterFinishedProfileUI() { - //dump("prepare to save\n"); - //saveProfileToLocalStorage(); - //dump("prepare to saved\n"); + saveProfileToLocalStorage(); var finishedProfilePaneBackgroundCover = document.createElement("div"); finishedProfilePaneBackgroundCover.className = "finishedProfilePaneBackgroundCover"; @@ -1544,7 +1704,7 @@ function enterFinishedProfileUI() { // until some actions happen such as focusing this box var filterNameInput = document.getElementById("filterName"); if (filterNameInput != null) { - filterNameInput.focus(); + changeFocus(filterNameInput); } }, 100); @@ -1556,27 +1716,32 @@ function enterFinishedProfileUI() { currRow = finishedProfilePane.insertRow(rowIndex++); currRow.insertCell(0).appendChild(gHistogramView.getContainer()); - if (typeof DiagnosticBar !== "undefined") { - gDiagnosticBar = new DiagnosticBar(); - gDiagnosticBar.setDetailsListener(function(details) { - if (details.indexOf("bug ") == 0) { - window.open('https://bugzilla.mozilla.org/show_bug.cgi?id=' + details.substring(4)); - } else { - var sourceView = new SourceView(); - sourceView.setText("Diagnostic", js_beautify(details)); - gMainArea.appendChild(sourceView.getContainer()); - } - }); + if (false && gLocation.indexOf("file:") == 0) { + // Local testing for frameView + gFrameView = new FrameView(); currRow = finishedProfilePane.insertRow(rowIndex++); - currRow.insertCell(0).appendChild(gDiagnosticBar.getContainer()); + currRow.insertCell(0).appendChild(gFrameView.getContainer()); } + gDiagnosticBar = new DiagnosticBar(); + gDiagnosticBar.setDetailsListener(function(details) { + if (details.indexOf("bug ") == 0) { + window.open('https://bugzilla.mozilla.org/show_bug.cgi?id=' + details.substring(4)); + } else { + var sourceView = new SourceView(); + sourceView.setText("Diagnostic", js_beautify(details)); + gMainArea.appendChild(sourceView.getContainer()); + } + }); + currRow = finishedProfilePane.insertRow(rowIndex++); + currRow.insertCell(0).appendChild(gDiagnosticBar.getContainer()); + // For testing: //gMeta.videoCapture = { // src: "http://videos-cdn.mozilla.net/brand/Mozilla_Firefox_Manifesto_v0.2_640.webm", //}; - if (!gLightMode && gMeta && gMeta.videoCapture) { + if (gMeta && gMeta.videoCapture) { gVideoPane = new VideoPane(gMeta.videoCapture); gVideoPane.onTimeChange(videoPaneTimeChange); currRow = finishedProfilePane.insertRow(rowIndex++); @@ -1595,22 +1760,19 @@ function enterFinishedProfileUI() { cell.appendChild(treeContainerDiv); treeContainerDiv.appendChild(gTreeManager.getContainer()); - if (!gLightMode) { - gSampleBar = new SampleBar(); - treeContainerDiv.appendChild(gSampleBar.getContainer()); - } + gSampleBar = new SampleBar(); + treeContainerDiv.appendChild(gSampleBar.getContainer()); // sampleBar - if (!gLightMode) { - gPluginView = new PluginView(); - //currRow = finishedProfilePane.insertRow(4); - treeContainerDiv.appendChild(gPluginView.getContainer()); - } + gPluginView = new PluginView(); + //currRow = finishedProfilePane.insertRow(4); + treeContainerDiv.appendChild(gPluginView.getContainer()); gMainArea.appendChild(finishedProfilePaneBackgroundCover); gMainArea.appendChild(finishedProfilePane); + var currentBreadcrumb = gSampleFilters; gBreadcrumbTrail.add({ title: "Complete Profile", enterCallback: function () { @@ -1618,16 +1780,66 @@ function enterFinishedProfileUI() { filtersChanged(); } }); + if (currentBreadcrumb == null || currentBreadcrumb.length == 0) { + gTreeManager.restoreSerializedSelectionSnapshot(gRestoreSelection); + viewOptionsChanged(); + } + for (var i = 0; i < currentBreadcrumb.length; i++) { + var filter = currentBreadcrumb[i]; + var forceSelection = null; + if (gRestoreSelection != null && i == currentBreadcrumb.length - 1) { + forceSelection = gRestoreSelection; + } + switch (filter.type) { + case "FocusedFrameSampleFilter": + focusOnSymbol(filter.name, filter.symbolName); + gBreadcrumbTrail.enterLastItem(forceSelection); + case "FocusedCallstackPrefixSampleFilter": + focusOnCallstack(filter.focusedCallstack, filter.name, false); + gBreadcrumbTrail.enterLastItem(forceSelection); + case "FocusedCallstackPostfixSampleFilter": + focusOnCallstack(filter.focusedCallstack, filter.name, true); + gBreadcrumbTrail.enterLastItem(forceSelection); + case "RangeSampleFilter": + gHistogramView.selectRange(filter.start, filter.end); + gBreadcrumbTrail.enterLastItem(forceSelection); + } + } +} + +// Make all focus change events go through this function. +// This function will mediate the focus changes in case +// that we're in a compare view. In a compare view an inactive +// instance of cleopatra should not steal focus from the active +// cleopatra instance. +function changeFocus(elem) { + if (window.comparator_changeFocus) { + window.comparator_changeFocus(elem); + } else { + PROFILERLOG("FOCUS\n\n\n\n\n\n\n\n\n"); + elem.focus(); + } +} + +function comparator_receiveSelection(snapshot, frameData) { + gTreeManager.restoreSerializedSelectionSnapshot(snapshot); + if (frameData) + gTreeManager.highlightFrame(frameData); + viewOptionsChanged(); } function filtersChanged() { + if (window.comparator_setSelection) { + // window.comparator_setSelection(gTreeManager.serializeCurrentSelectionSnapshot(), null); + } + updateDocumentURL(); var data = { symbols: {}, functions: {}, samples: [] }; gHistogramView.dataIsOutdated(); var filterNameInput = document.getElementById("filterName"); var updateRequest = Parser.updateFilters({ mergeFunctions: gMergeFunctions, - nameFilter: (filterNameInput && filterNameInput.value) || "", + nameFilter: (filterNameInput && filterNameInput.value) || gQueryParamFilterName || "", sampleFilters: gSampleFilters, jankOnly: gJankOnly, javascriptOnly: gJavascriptOnly @@ -1636,15 +1848,14 @@ function filtersChanged() { updateRequest.addEventListener("finished", function (filteredSamples) { console.log("profile filtering (in worker): " + (Date.now() - start) + "ms."); gCurrentlyShownSampleData = filteredSamples; - if (gInfoBar) - gInfoBar.display(); + gInfoBar.display(); - if (gPluginView && gSampleFilters.length > 0 && gSampleFilters[gSampleFilters.length-1].type === "PluginView") { + if (gSampleFilters.length > 0 && gSampleFilters[gSampleFilters.length-1].type === "PluginView") { start = Date.now(); gPluginView.display(gSampleFilters[gSampleFilters.length-1].pluginName, gSampleFilters[gSampleFilters.length-1].param, gCurrentlyShownSampleData, gHighlightedCallstack); console.log("plugin displaying: " + (Date.now() - start) + "ms."); - } else if (gPluginView) { + } else { gPluginView.hide(); } }); @@ -1653,6 +1864,8 @@ function filtersChanged() { histogramRequest.addEventListener("finished", function (data) { start = Date.now(); gHistogramView.display(data.histogramData, data.frameStart, data.widthSum, gHighlightedCallstack); + if (gFrameView) + gFrameView.display(data.histogramData, data.frameStart, data.widthSum, gHighlightedCallstack); console.log("histogram displaying: " + (Date.now() - start) + "ms."); }); @@ -1677,7 +1890,118 @@ function viewOptionsChanged() { }); updateViewOptionsRequest.addEventListener("finished", function (calltree) { var start = Date.now(); - gTreeManager.display(calltree, gSymbols, gFunctions, gMergeFunctions, filterNameInput && filterNameInput.value); + gTreeManager.display(calltree, gSymbols, gFunctions, gResources, gMergeFunctions, filterNameInput && filterNameInput.value); console.log("tree displaying: " + (Date.now() - start) + "ms."); }); } + +function loadQueryData(queryData) { + var isFiltersChanged = false; + var queryDataOriginal = queryData; + var queryData = {}; + for (var i in queryDataOriginal) { + queryData[i] = unQueryEscape(queryDataOriginal[i]); + } + if (queryData.search) { + gQueryParamFilterName = queryData.search; + isFiltersChanged = true; + } + if (queryData.jankOnly) { + gJankOnly = queryData.jankOnly; + isFiltersChanged = true; + } + if (queryData.javascriptOnly) { + gJavascriptOnly = queryData.javascriptOnly; + isFiltersChanged = true; + } + if (queryData.mergeUnbranched) { + gMergeUnbranched = queryData.mergeUnbranched; + isFiltersChanged = true; + } + if (queryData.invertCallback) { + gInvertCallstack = queryData.invertCallback; + isFiltersChanged = true; + } + if (queryData.report) { + gReportID = queryData.report; + } + if (queryData.filter) { + var filterChain = JSON.parse(queryData.filter); + gSampleFilters = filterChain; + } + if (queryData.selection) { + var selection = queryData.selection; + gRestoreSelection = selection; + } + + if (isFiltersChanged) { + //filtersChanged(); + } +} + +function unQueryEscape(str) { + return decodeURIComponent(str); +} + +function queryEscape(str) { + return encodeURIComponent(encodeURIComponent(str)); +} + +function updateDocumentURL() { + location.hash = getDocumentHashString(); + return document.location; +} + +function getDocumentHashString() { + var query = ""; + if (gReportID) { + if (query != "") + query += "&"; + query += "report=" + queryEscape(gReportID); + } + if (document.getElementById("filterName") != null && + document.getElementById("filterName").value != null && + document.getElementById("filterName").value != "") { + if (query != "") + query += "&"; + query += "search=" + queryEscape(document.getElementById("filterName").value); + } + // For now don't restore the view rest + return query; + if (gJankOnly) { + if (query != "") + query += "&"; + query += "jankOnly=" + queryEscape(gJankOnly); + } + if (gJavascriptOnly) { + if (query != "") + query += "&"; + query += "javascriptOnly=" + queryEscape(gJavascriptOnly); + } + if (gMergeUnbranched) { + if (query != "") + query += "&"; + query += "mergeUnbranched=" + queryEscape(gMergeUnbranched); + } + if (gInvertCallstack) { + if (query != "") + query += "&"; + query += "invertCallback=" + queryEscape(gInvertCallstack); + } + if (gSampleFilters && gSampleFilters.length != 0) { + if (query != "") + query += "&"; + query += "filter=" + queryEscape(JSON.stringify(gSampleFilters)); + } + if (gTreeManager.hasNonTrivialSelection()) { + if (query != "") + query += "&"; + query += "selection=" + queryEscape(gTreeManager.serializeCurrentSelectionSnapshot()); + } + if (!gReportID) { + query = "uploadProfileFirst!"; + } + + return query; +} +