Bug 795268 - Integrate SPS Profiler; r=rcampbell

This commit is contained in:
Anton Kovalyov 2012-12-13 17:17:00 -05:00
parent e619ad7330
commit d6d2a60246
32 changed files with 6132 additions and 0 deletions

View File

@ -1047,6 +1047,9 @@ pref("devtools.debugger.ui.variables-sorting-enabled", true);
pref("devtools.debugger.ui.variables-only-enum-visible", false);
pref("devtools.debugger.ui.variables-searchbox-visible", false);
// Enable the Profiler
pref("devtools.profiler.enabled", true);
// Enable the Tilt inspector
pref("devtools.tilt.enabled", true);
pref("devtools.tilt.intro_transition", true);

View File

@ -27,6 +27,7 @@ DIRS = \
shared \
responsivedesign \
framework \
profiler \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -18,6 +18,7 @@ const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
@ -39,6 +40,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "StyleEditorPanel",
XPCOMUtils.defineLazyModuleGetter(this, "InspectorPanel",
"resource:///modules/devtools/InspectorPanel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ProfilerPanel",
"resource:///modules/devtools/ProfilerPanel.jsm");
// Strings
XPCOMUtils.defineLazyGetter(this, "webConsoleStrings",
function() Services.strings.createBundle(webConsoleProps));
@ -52,6 +56,9 @@ XPCOMUtils.defineLazyGetter(this, "styleEditorStrings",
XPCOMUtils.defineLazyGetter(this, "inspectorStrings",
function() Services.strings.createBundle(inspectorProps));
XPCOMUtils.defineLazyGetter(this, "profilerStrings",
function() Services.strings.createBundle(profilerProps));
// Definitions
let webConsoleDefinition = {
id: "webconsole",
@ -131,6 +138,28 @@ let styleEditorDefinition = {
}
};
let profilerDefinition = {
id: "jsprofiler",
killswitch: "devtools.profiler.enabled",
icon: "chrome://browser/skin/devtools/tools-icons-small.png",
url: "chrome://browser/content/profiler.xul",
label: l10n("profiler.label", profilerStrings),
isTargetSupported: function (target) {
if (target.isRemote || target.isChrome) {
return false;
}
return true;
},
build: function (frame, target) {
let panel = new ProfilerPanel(frame, target);
return panel.open();
}
};
this.defaultTools = [
styleEditorDefinition,
webConsoleDefinition,
@ -138,6 +167,10 @@ this.defaultTools = [
inspectorDefinition,
];
if (Services.prefs.getBoolPref("devtools.profiler.enabled")) {
defaultTools.push(profilerDefinition);
}
/**
* Lookup l10n string from a string bundle.
*

View File

@ -27,6 +27,24 @@ browser.jar:
content/browser/debugger-view.js (debugger/debugger-view.js)
content/browser/debugger-toolbar.js (debugger/debugger-toolbar.js)
content/browser/debugger-panes.js (debugger/debugger-panes.js)
* content/browser/profiler.xul (profiler/profiler.xul)
content/browser/profiler.css (profiler/profiler.css)
content/browser/devtools/cleopatra.html (profiler/cleopatra/cleopatra.html)
content/browser/devtools/profiler/cleopatra/css/ui.css (profiler/cleopatra/css/ui.css)
content/browser/devtools/profiler/cleopatra/css/tree.css (profiler/cleopatra/css/tree.css)
content/browser/devtools/profiler/cleopatra/css/devtools.css (profiler/cleopatra/css/devtools.css)
content/browser/devtools/profiler/cleopatra/js/parser.js (profiler/cleopatra/js/parser.js)
content/browser/devtools/profiler/cleopatra/js/parserWorker.js (profiler/cleopatra/js/parserWorker.js)
content/browser/devtools/profiler/cleopatra/js/tree.js (profiler/cleopatra/js/tree.js)
content/browser/devtools/profiler/cleopatra/js/ui.js (profiler/cleopatra/js/ui.js)
content/browser/devtools/profiler/cleopatra/js/ProgressReporter.js (profiler/cleopatra/js/ProgressReporter.js)
content/browser/devtools/profiler/cleopatra/js/devtools.js (profiler/cleopatra/js/devtools.js)
content/browser/devtools/profiler/cleopatra/images/circlearrow.svg (profiler/cleopatra/images/circlearrow.svg)
content/browser/devtools/profiler/cleopatra/images/filter.png (profiler/cleopatra/images/filter.png)
content/browser/devtools/profiler/cleopatra/images/noise.png (profiler/cleopatra/images/noise.png)
content/browser/devtools/profiler/cleopatra/images/showall.png (profiler/cleopatra/images/showall.png)
content/browser/devtools/profiler/cleopatra/images/throbber.svg (profiler/cleopatra/images/throbber.svg)
content/browser/devtools/profiler/cleopatra/images/treetwisty.svg (profiler/cleopatra/images/treetwisty.svg)
content/browser/devtools/commandline.css (commandline/commandline.css)
content/browser/devtools/commandlineoutput.xhtml (commandline/commandlineoutput.xhtml)
content/browser/devtools/commandlinetooltip.xhtml (commandline/commandlinetooltip.xhtml)

View File

@ -0,0 +1,17 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DEPTH = @DEPTH@
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
TEST_DIRS += test
include $(topsrcdir)/config/rules.mk
libs::
$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools

View File

@ -0,0 +1,208 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
let EXPORTED_SYMBOLS = ["ProfilerController"];
XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function () {
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
return DebuggerServer;
});
/**
* Object acting as a mediator between the ProfilerController and
* DebuggerServer.
*/
function ProfilerConnection() {
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
let transport = DebuggerServer.connectPipe();
this.client = new DebuggerClient(transport);
}
ProfilerConnection.prototype = {
actor: null,
/**
* Connects to a debugee and executes a callback when ready.
*
* @param function aCallback
* Function to be called once we're connected to the client.
*/
connect: function PCn_connect(aCallback) {
let client = this.client;
client.connect(function (aType, aTraits) {
client.listTabs(function (aResponse) {
this.actor = aResponse.profilerActor;
aCallback();
}.bind(this));
}.bind(this));
},
/**
* Sends a message to check if the profiler is currently active.
*
* @param function aCallback
* Function to be called once we have a response from
* the client. It will be called with a single argument
* containing a response object.
*/
isActive: function PCn_isActive(aCallback) {
var message = { to: this.actor, type: "isActive" };
this.client.request(message, aCallback);
},
/**
* Sends a message to start a profiler.
*
* @param function aCallback
* Function to be called once the profiler is running.
* It will be called with a single argument containing
* a response object.
*/
startProfiler: function PCn_startProfiler(aCallback) {
var message = {
to: this.actor,
type: "startProfiler",
entries: 1000000,
interval: 1,
features: ["js"],
};
this.client.request(message, aCallback);
},
/**
* Sends a message to stop a profiler.
*
* @param function aCallback
* Function to be called once the profiler is idle.
* It will be called with a single argument containing
* a response object.
*/
stopProfiler: function PCn_stopProfiler(aCallback) {
var message = { to: this.actor, type: "stopProfiler" };
this.client.request(message, aCallback);
},
/**
* Sends a message to get the generated profile data.
*
* @param function aCallback
* Function to be called once we have the data.
* It will be called with a single argument containing
* a response object.
*/
getProfileData: function PCn_getProfileData(aCallback) {
var message = { to: this.actor, type: "getProfile" };
this.client.request(message, aCallback);
},
/**
* Cleanup.
*/
destroy: function PCn_destroy() {
this.client.close(function () {
this.client = null;
}.bind(this));
}
};
/**
* Object defining the profiler controller components.
*/
function ProfilerController() {
this.profiler = new ProfilerConnection();
this._connected = false;
}
ProfilerController.prototype = {
/**
* Connects to the client unless we're already connected.
*
* @param function aCallback
* Function to be called once we're connected. If
* the controller is already connected, this function
* will be called immediately (synchronously).
*/
connect: function (aCallback) {
if (this._connected) {
aCallback();
}
this.profiler.connect(function onConnect() {
this._connected = true;
aCallback();
}.bind(this));
},
/**
* Checks whether the profiler is active.
*
* @param function aCallback
* Function to be called with a response from the
* client. It will be called with two arguments:
* an error object (may be null) and a boolean
* value indicating if the profiler is active or not.
*/
isActive: function PC_isActive(aCallback) {
this.profiler.isActive(function onActive(aResponse) {
aCallback(aResponse.error, aResponse.isActive);
});
},
/**
* Starts the profiler.
*
* @param function aCallback
* Function to be called once the profiler is started
* or we get an error. It will be called with a single
* argument: an error object (may be null).
*/
start: function PC_start(aCallback) {
this.profiler.startProfiler(function onStart(aResponse) {
aCallback(aResponse.error);
});
},
/**
* Stops the profiler.
*
* @param function aCallback
* Function to be called once the profiler is stopped
* or we get an error. It will be called with a single
* argument: an error object (may be null).
*/
stop: function PC_stop(aCallback) {
this.profiler.getProfileData(function onData(aResponse) {
let data = aResponse.profile;
if (aResponse.error) {
Cu.reportError("Failed to fetch profile data before stopping the profiler.");
}
this.profiler.stopProfiler(function onStop(aResponse) {
aCallback(aResponse.error, data);
});
}.bind(this));
},
/**
* Cleanup.
*/
destroy: function PC_destroy(aCallback) {
this.profiler.destroy();
this.profiler = null;
}
};

View File

@ -0,0 +1,393 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cu = Components.utils;
Cu.import("resource:///modules/devtools/ProfilerController.jsm");
Cu.import("resource://gre/modules/commonjs/promise/core.js");
Cu.import("resource://gre/modules/devtools/EventEmitter.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.EXPORTED_SYMBOLS = ["ProfilerPanel"];
XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function () {
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
return DebuggerServer;
});
/**
* An instance of a profile UI. Profile UI consists of
* an iframe with Cleopatra loaded in it and some
* surrounding meta-data (such as uids).
*
* Its main function is to talk to the Cleopatra instance
* inside of the iframe.
*
* ProfileUI is also an event emitter. Currently, it emits
* only one event, 'ready', when Cleopatra is done loading.
* You can also check 'isReady' property to see if a
* particular instance has been loaded yet.
*
* @param number uid
* Unique ID for this profile.
* @param ProfilerPanel panel
* A reference to the container panel.
*/
function ProfileUI(uid, panel) {
let doc = panel.document;
let win = panel.window;
new EventEmitter(this);
this.isReady = false;
this.panel = panel;
this.uid = uid;
this.iframe = doc.createElement("iframe");
this.iframe.setAttribute("flex", "1");
this.iframe.setAttribute("id", "profiler-cleo-" + uid);
this.iframe.setAttribute("src", "devtools/cleopatra.html?" + uid);
this.iframe.setAttribute("hidden", "true");
// Append our iframe and subscribe to postMessage events.
// They'll tell us when the underlying page is done loading
// or when user clicks on start/stop buttons.
doc.getElementById("profiler-report").appendChild(this.iframe);
win.addEventListener("message", function (event) {
if (parseInt(event.data.uid, 10) !== parseInt(this.uid, 10)) {
return;
}
switch (event.data.status) {
case "loaded":
this.isReady = true;
this.emit("ready");
break;
case "start":
// Start profiling and, once started, notify the
// underlying page so that it could update the UI.
this.panel.startProfiling(function onStart() {
var data = JSON.stringify({task: "onStarted"});
this.iframe.contentWindow.postMessage(data, "*");
}.bind(this));
break;
case "stop":
// Stop profiling and, once stopped, notify the
// underlying page so that it could update the UI.
this.panel.stopProfiling(function onStop() {
var data = JSON.stringify({task: "onStopped"});
this.iframe.contentWindow.postMessage(data, "*");
}.bind(this));
}
}.bind(this));
}
ProfileUI.prototype = {
show: function PUI_show() {
this.iframe.removeAttribute("hidden");
},
hide: function PUI_hide() {
this.iframe.setAttribute("hidden", true);
},
/**
* Send raw profiling data to Cleopatra for parsing.
*
* @param object data
* Raw profiling data from the SPS Profiler.
* @param function onParsed
* A callback to be called when Cleopatra finishes
* parsing and displaying results.
*
*/
parse: function PUI_parse(data, onParsed) {
if (!this.isReady) {
return;
}
let win = this.iframe.contentWindow;
win.postMessage(JSON.stringify({
task: "receiveProfileData",
rawProfile: data
}), "*");
let poll = function pollBreadcrumbs() {
let wait = this.panel.window.setTimeout.bind(null, poll, 100);
let trail = win.gBreadcrumbTrail;
if (!trail) {
return wait();
}
if (!trail._breadcrumbs || !trail._breadcrumbs.length) {
return wait();
}
onParsed();
}.bind(this);
poll();
},
/**
* Destroys the ProfileUI instance.
*/
destroy: function PUI_destroy() {
this.isReady = null
this.panel = null;
this.uid = null;
this.iframe = null;
}
};
/**
* Profiler panel. It is responsible for creating and managing
* different profile instances (see ProfileUI).
*
* ProfilerPanel is an event emitter. It can emit the following
* events:
*
* - ready: after the panel is done loading everything,
* including the default profile instance.
* - started: after the panel successfuly starts our SPS
* profiler.
* - stopped: after the panel successfuly stops our SPS
* profiler and is ready to hand over profiling
* data
* - parsed: after Cleopatra finishes parsing profiling
* data.
* - destroyed: after the panel cleans up after itself and
* is ready to be destroyed.
*
* The following events are used mainly by tests to prevent
* accidential oranges:
*
* - profileCreated: after a new profile is created.
* - profileSwitched: after user switches to a different
* profile.
*/
function ProfilerPanel(frame, toolbox) {
this.isReady = false;
this.window = frame.window;
this.document = frame.document;
this.target = toolbox.target;
this.controller = new ProfilerController();
this.profiles = new Map();
this._uid = 0;
new EventEmitter(this);
}
ProfilerPanel.prototype = {
isReady: null,
window: null,
document: null,
target: null,
controller: null,
profiles: null,
_uid: null,
_activeUid: null,
get activeProfile() {
return this.profiles.get(this._activeUid);
},
set activeProfile(profile) {
this._activeUid = profile.uid;
},
/**
* Open a debug connection and, on success, switch to
* the newly created profile.
*
* @return Promise
*/
open: function PP_open() {
let deferred = Promise.defer();
this.controller.connect(function onConnect() {
let create = this.document.getElementById("profiler-create");
create.addEventListener("click", this.createProfile.bind(this), false);
create.removeAttribute("disabled");
let profile = this.createProfile();
this.switchToProfile(profile, function () {
this.isReady = true;
this.emit("ready");
deferred.resolve(this);
}.bind(this))
}.bind(this));
return deferred.promise;
},
/**
* Creates a new profile instance (see ProfileUI) and
* adds an appropriate item to the sidebar. Note that
* this method doesn't automatically switch user to
* the newly created profile, they have do to switch
* explicitly.
*
* @return ProfilerPanel
*/
createProfile: function PP_addProfile() {
let uid = ++this._uid;
let list = this.document.getElementById("profiles-list");
let item = this.document.createElement("li");
let wrap = this.document.createElement("h1");
item.setAttribute("id", "profile-" + uid);
item.setAttribute("data-uid", uid);
item.addEventListener("click", function (ev) {
let uid = parseInt(ev.target.getAttribute("data-uid"), 10);
this.switchToProfile(this.profiles.get(uid));
}.bind(this), false);
wrap.className = "profile-name";
wrap.textContent = "Profile " + uid;
item.appendChild(wrap);
list.appendChild(item);
let profile = new ProfileUI(uid, this);
this.profiles.set(uid, profile);
this.emit("profileCreated", uid);
return profile;
},
/**
* Switches to a different profile by making its instance an
* active one.
*
* @param ProfileUI profile
* A profile instance to switch to.
* @param function onLoad
* A function to call when profile instance is ready.
* If the instance is already loaded, onLoad will be
* called synchronously.
*/
switchToProfile: function PP_switchToProfile(profile, onLoad) {
let doc = this.document;
if (this.activeProfile) {
this.activeProfile.hide();
}
let active = doc.querySelector("#profiles-list > li.splitview-active");
if (active) {
active.className = "";
}
doc.getElementById("profile-" + profile.uid).className = "splitview-active";
profile.show();
this.activeProfile = profile;
if (profile.isReady) {
this.emit("profileSwitched", profile.uid);
onLoad();
return;
}
profile.once("ready", function () {
this.emit("profileSwitched", profile.uid);
onLoad();
}.bind(this));
},
/**
* Start collecting profile data.
*
* @param function onStart
* A function to call once we get the message
* that profiling had been successfuly started.
*/
startProfiling: function PP_startProfiling(onStart) {
this.controller.start(function (err) {
if (err) {
Cu.reportError("ProfilerController.start: " + err.message);
return;
}
onStart();
this.emit("started");
}.bind(this));
},
/**
* Stop collecting profile data and send it to Cleopatra
* for parsing.
*
* @param function onStop
* A function to call once we get the message
* that profiling had been successfuly stopped.
*/
stopProfiling: function PP_stopProfiling(onStop) {
this.controller.isActive(function (err, isActive) {
if (err) {
Cu.reportError("ProfilerController.isActive: " + err.message);
return;
}
if (!isActive) {
return;
}
this.controller.stop(function (err, data) {
if (err) {
Cu.reportError("ProfilerController.stop: " + err.message);
return;
}
this.activeProfile.parse(data, function onParsed() {
this.emit("parsed");
}.bind(this));
onStop();
this.emit("stopped");
}.bind(this));
}.bind(this));
},
/**
* Cleanup.
*/
destroy: function PP_destroy() {
if (this.profiles) {
let uid = this._uid;
while (uid >= 0) {
if (this.profiles.has(uid)) {
this.profiles.get(uid).destroy();
this.profiles.delete(uid);
}
uid -= 1;
}
}
if (this.controller) {
this.controller.destroy();
}
this.isReady = null;
this.window = null;
this.document = null;
this.target = null;
this.controller = null;
this.profiles = null;
this._uid = null;
this._activeUid = null;
this.emit("destroyed");
}
};

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html>
<head>
<title>Firefox Profiler (SPS)</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/ui.css">
<link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/tree.css">
<link rel="stylesheet" type="text/css" href="profiler/cleopatra/css/devtools.css">
<script src="profiler/cleopatra/js/parser.js"></script>
<script src="profiler/cleopatra/js/tree.js"></script>
<script src="profiler/cleopatra/js/ui.js"></script>
<script src="profiler/cleopatra/js/ProgressReporter.js"></script>
<script src="profiler/cleopatra/js/devtools.js"></script>
<link rel="shortcut icon" href="favicon.png" />
</head>
<body onload="notifyParent('loaded');">
<script>
initUI();
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#mainarea > .controlPane {
font-size: 120%;
padding-top: 75px;
text-align: center;
}
#stopWrapper {
display: none;
}

View File

@ -0,0 +1,245 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.treeViewContainer {
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
cursor: default;
line-height: 16px;
height: 100%;
outline: none; /* override the browser's focus styling */
position: relative;
}
.treeHeader {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 16px;
margin: 0;
padding: 0;
}
.treeColumnHeader {
position: absolute;
display: block;
background: -moz-linear-gradient(#FFF 45%, #EEE 60%);
background: -webkit-linear-gradient(#FFF 45%, #EEE 60%);
background: linear-gradient(#FFF 45%, #EEE 60%);
margin: 0;
padding: 0;
top: 0;
height: 15px;
line-height: 15px;
border: 0 solid #CCC;
border-bottom-width: 1px;
text-indent: 5px;
}
.treeColumnHeader:not(:last-child) {
border-right-width: 1px;
}
.treeColumnHeader0 {
left: 0;
width: 86px;
}
.treeColumnHeader1 {
left: 99px;
width: 35px;
}
.treeColumnHeader0,
.treeColumnHeader1 {
text-align: right;
padding-right: 12px;
}
.treeColumnHeader2 {
left: 147px;
right: 0;
}
.treeViewVerticalScrollbox {
position: absolute;
top: 16px;
left: 0;
right: 0;
bottom: 0;
overflow-y: scroll;
overflow-x: hidden;
}
.treeViewNode,
.treeViewHorizontalScrollbox {
display: block;
margin: 0;
padding: 0;
}
.treeViewNode {
min-width: -moz-min-content;
white-space: nowrap;
}
.treeViewHorizontalScrollbox {
padding-left: 150px;
overflow: hidden;
}
.treeViewVerticalScrollbox,
.treeViewHorizontalScrollbox {
background: -moz-linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
background: -webkit-linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
background: linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
background-size: 100px 32px;
}
.leftColumnBackground {
background: -moz-linear-gradient(left, transparent, transparent 98px, #CCC 98px, #CCC 99px, transparent 99px),
-moz-linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
background: -webkit-linear-gradient(left, transparent, transparent 98px, #CCC 98px, #CCC 99px, transparent 99px),
-webkit-linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
background: linear-gradient(left, transparent, transparent 98px, #CCC 98px, #CCC 99px, transparent 99px),
linear-gradient(white, white 50%, #F0F5FF 50%, #F0F5FF);
background-size: auto, 100px 32px;
position: absolute;
top: 0;
left: 0;
width: 146px;
min-height: 100%;
border-right: 1px solid #CCC;
}
.sampleCount,
.samplePercentage,
.selfSampleCount {
position: absolute;
text-align: right;
}
.sampleCount {
left: 2px;
width: 50px;
}
.samplePercentage {
left: 55px;
width: 40px;
}
.selfSampleCount {
left: 98px;
width: 45px;
padding-right: 2px;
border: solid #CCC;
border-width: 0 1px;
}
.libraryName {
margin-left: 10px;
color: #999;
}
.treeViewNode > .treeViewNodeList {
margin-left: 1em;
}
.treeViewNode.collapsed > .treeViewNodeList {
display: none;
}
.treeLine {
/* extend the selection background almost infinitely to the left */
margin-left: -10000px;
padding-left: 10000px;
}
.treeLine.selected {
color: black;
background-color: -moz-dialog;
}
.treeLine.selected > .sampleCount {
background-color: inherit;
margin-left: -2px;
padding-left: 2px;
padding-right: 95px;
margin-right: -95px;
}
.treeViewContainer:focus .treeLine.selected {
color: highlighttext;
background-color: highlight;
}
.treeViewContainer:focus .treeLine.selected > .libraryName {
color: #CCC;
}
.expandCollapseButton,
.focusCallstackButton {
background: none 0 0 no-repeat transparent;
margin: 0;
padding: 0;
border: 0;
width: 16px;
height: 16px;
overflow: hidden;
vertical-align: top;
color: transparent;
font-size: 0;
}
.expandCollapseButton {
background-image: url(../images/treetwisty.svg);
}
.focusCallstackButton {
background-image: url(../images/circlearrow.svg);
margin-left: 5px;
visibility: hidden;
}
.expandCollapseButton:active:hover,
.focusCallstackButton:active:hover {
background-position: -16px 0;
}
.treeViewNode.collapsed > .treeLine > .expandCollapseButton {
background-position: 0 -16px;
}
.treeViewNode.collapsed > .treeLine > .expandCollapseButton:active:hover {
background-position: -16px -16px;
}
.treeViewContainer:focus .treeLine.selected > .expandCollapseButton,
.treeViewContainer:focus .treeLine.selected > .focusCallstackButton {
background-position: -32px 0;
}
.treeViewContainer:focus .treeViewNode.collapsed > .treeLine.selected > .expandCollapseButton {
background-position: -32px -16px;
}
.treeViewContainer:focus .treeLine.selected > .expandCollapseButton:active:hover,
.treeViewContainer:focus .treeLine.selected > .focusCallstackButton:active:hover {
background-position: -48px 0;
}
.treeViewContainer:focus .treeViewNode.collapsed > .treeLine.selected > .expandCollapseButton:active:hover {
background-position: -48px -16px;
}
.treeViewNode.leaf > * > .expandCollapseButton {
visibility: hidden;
}
.treeLine:hover > .focusCallstackButton {
visibility: visible;
}

View File

@ -0,0 +1,344 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
body {
margin: 0;
font-family: "Lucida Grande", sans-serif;
font-size: 11px;
}
#mainarea {
position: absolute;
top: 0;
left: 0px;
bottom: 0;
right: 0;
}
.finishedProfilePane,
.finishedProfilePaneBackgroundCover,
.profileEntryPane,
.profileProgressPane {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.profileEntryPane {
overflow: auto;
}
.profileEntryPane,
.profileProgressPane {
padding: 20px;
background-color: rgb(229,229,229);
background-image: url(../images/noise.png),
-moz-linear-gradient(rgba(255,255,255,.5),rgba(255,255,255,.2));
background-image: url(../images/noise.png),
-webkit-linear-gradient(rgba(255,255,255,.5),rgba(255,255,255,.2));
background-image: url(../images/noise.png),
linear-gradient(rgba(255,255,255,.5),rgba(255,255,255,.2));
text-shadow: rgba(255, 255, 255, 0.4) 0 1px;
}
.profileEntryPane h1 {
margin-top: 0;
font-size: 13px;
font-weight: normal;
}
.profileEntryPane input[type="file"] {
margin-bottom: 1em;
}
.profileProgressPane a {
position: absolute;
top: 30%;
left: 30%;
width: 40%;
height: 16px;
}
.profileProgressPane progress {
position: absolute;
top: 40%;
left: 30%;
width: 40%;
height: 16px;
}
.finishedProfilePaneBackgroundCover {
-webkit-animation: darken 300ms cubic-bezier(0, 0, 1, 0);
-moz-animation: darken 300ms cubic-bezier(0, 0, 1, 0);
background-color: rgba(0, 0, 0, 0.5);
}
.finishedProfilePane {
-webkit-animation: appear 300ms ease-out;
-moz-animation: appear 300ms ease-out;
}
.breadcrumbTrail {
top: 0;
right: 0;
height: 29px;
left: 0;
background: -moz-linear-gradient(#FFF 50%, #F3F3F3 55%);
background: -webkit-linear-gradient(#FFF 50%, #F3F3F3 55%);
background: linear-gradient(#FFF 50%, #F3F3F3 55%);
border-bottom: 1px solid #CCC;
margin: 0;
padding: 0;
overflow: hidden;
}
.breadcrumbTrailItem {
background: -moz-linear-gradient(#FFF 50%, #F3F3F3 55%);
background: -webkit-linear-gradient(#FFF 50%, #F3F3F3 55%);
background: linear-gradient(#FFF 50%, #F3F3F3 55%);
display: block;
margin: 0;
padding: 0;
float: left;
line-height: 29px;
padding: 0 10px;
font-size: 12px;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
cursor: default;
border-right: 1px solid #CCC;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
}
@-webkit-keyframes slide-out {
from {
margin-left: -270px;
opacity: 0;
}
to {
margin-left: 0;
opacity: 1;
}
}
@-moz-keyframes slide-out {
from {
margin-left: -270px;
opacity: 0;
}
to {
margin-left: 0;
opacity: 1;
}
}
.breadcrumbTrailItem:not(:first-child) {
-moz-animation: slide-out;
-moz-animation-duration: 400ms;
-moz-animation-timing-function: ease-out;
-webkit-animation: slide-out;
-webkit-animation-duration: 400ms;
-webkit-animation-timing-function: ease-out;
}
.breadcrumbTrailItem.selected {
background: linear-gradient(#E5E5E5 50%, #DADADA 55%);
}
.breadcrumbTrailItem:not(.selected):active:hover {
background: linear-gradient(#F2F2F2 50%, #E6E6E6 55%);
}
.breadcrumbTrailItem.deleted {
-moz-transition: 400ms ease-out;
-moz-transition-property: opacity, margin-left;
-webkit-transition: 400ms ease-out;
-webkit-transition-property: opacity, margin-left;
opacity: 0;
margin-left: -270px;
}
.treeContainer {
/*For asbolute position child*/
position: relative;
}
.tree {
height: 100%;
}
#sampleBar {
position: absolute;
float: right;
left: auto;
top: 0;
right: 0;
height: 100%;
}
#fileList {
position: absolute;
top: 0;
left: 0;
bottom: 360px;
width: 199px;
overflow: auto;
padding: 0;
margin: 0;
background: #DBDFE7;
border-right: 1px solid #BBB;
cursor: pointer;
}
#infoBar dl {
margin: 0;
}
#infoBar dt,
#infoBar dd {
display: inline;
}
#infoBar dt {
font-weight: bold;
}
#infoBar dt::after {
content: " ";
white-space: pre;
}
#infoBar dd {
margin-left: 0;
}
#infoBar dd::after {
content: "\a";
white-space:pre;
}
.sideBar {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
position: absolute;
left: 0;
bottom: 0;
width: 200px;
height: 480px;
overflow: auto;
padding: 3px;
background: #EEE;
border-top: 1px solid #BBB;
border-right: 1px solid #BBB;
}
.sideBar h2 {
font-size: 1em;
padding: 1px 3px;
margin: 3px -3px;
background: rgba(255, 255, 255, 0.6);
border: solid #CCC;
border-width: 1px 0;
}
.sideBar h2:first-child {
margin-top: -4px;
}
.sideBar ul {
margin: 2px 0;
padding-left: 18px;
}
.pluginview {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background-color: white;
}
.pluginviewIFrame {
border-style: none;
width: 100%;
height: 100%;
}
.histogram {
position: relative;
height: 60px;
right: 0;
left: 0;
border-bottom: 1px solid #CCC;
background: -moz-linear-gradient(#EEE, #CCC);
background: -webkit-linear-gradient(#EEE, #CCC);
background: linear-gradient(#EEE, #CCC);
}
.histogramHilite {
position: absolute;
pointer-events: none;
}
.histogramHilite:not(.collapsed) {
background: rgba(150, 150, 150, 0.5);
}
.histogramMouseMarker {
position: absolute;
pointer-events: none;
top: 0;
width: 1px;
height: 100%;
}
.histogramMouseMarker:not(.collapsed) {
background: rgba(0, 0, 150, 0.7);
}
#iconbox {
display: none;
}
#filter, #showall {
cursor: pointer;
}
.markers {
display: none;
}
.hidden {
display: none !important;
}
.fileListItem {
display: block;
margin: 0;
padding: 0;
height: 40px;
text-indent: 8px;
}
.fileListItem.selected {
background: -moz-linear-gradient(#4B91D7 1px, #5FA9E4 1px, #5FA9E4 2px, #58A0DE 3px, #2B70C7 39px, #2763B4 39px);
background: -webkit-linear-gradient(#4B91D7 1px, #5FA9E4 1px, #5FA9E4 2px, #58A0DE 3px, #2B70C7 39px, #2763B4 39px);
background: linear-gradient(#4B91D7 1px, #5FA9E4 1px, #5FA9E4 2px, #58A0DE 3px, #2B70C7 39px, #2763B4 39px);
color: #FFF;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
}
.fileListItemTitle {
display: block;
padding-top: 6px;
font-size: 12px;
}
.fileListItemDescription {
display: block;
line-height: 15px;
font-size: 9px;
}
.busyCover {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
visibility: hidden;
opacity: 0;
pointer-events: none;
background: rgba(120, 120, 120, 0.2);
-moz-transition: 200ms ease-in-out;
-moz-transition-property: visibility, opacity;
-webkit-transition: 200ms ease-in-out;
-webkit-transition-property: visibility, opacity;
}
.busyCover.busy {
visibility: visible;
opacity: 1;
}
.busyCover::before {
content: url(../images/throbber.svg);
position: absolute;
top: 50%;
left: 50%;
margin: -12px;
}
label {
-webkit-user-select: none;
-moz-user-select: none;
}
.videoPane {
background-color: white;
width: 100%;
}
.video {
display: block;
margin-left: auto;
margin-right: auto;
}

View File

@ -0,0 +1,27 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="16" viewBox="0 0 64 16">
<defs>
<mask id="arrowInCircle" maskContentUnits="userSpaceOnUse">
<circle cx="8" cy="8" r="6" fill="white"/>
<rect x="4.5" y="7" width="3.5" height="2" fill="black"/>
<polyline points="8 4 12 8 8 12" fill="black"/>
</mask>
</defs>
<g fill="#888">
<rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
</g>
<g fill="#444" transform="translate(16,0)">
<rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
</g>
<g fill="#FFF" transform="translate(32,0)">
<rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
</g>
<g fill="rgba(255, 255, 255, 0.7)" transform="translate(48,0)">
<rect x="0" y="0" width="16" height="16" mask="url(#arrowInCircle)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,23 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="24" height="24" viewBox="0 0 64 64">
<g>
<rect x="30" y="4" width="4" height="15" transform="rotate(0, 32, 32)" fill="#BBB"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(30, 32, 32)" fill="#AAA"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(60, 32, 32)" fill="#999"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(90, 32, 32)" fill="#888"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(120, 32, 32)" fill="#777"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(150, 32, 32)" fill="#666"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(180, 32, 32)" fill="#555"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(210, 32, 32)" fill="#444"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(240, 32, 32)" fill="#333"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(270, 32, 32)" fill="#222"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(300, 32, 32)" fill="#111"/>
<rect x="30" y="4" width="4" height="15" transform="rotate(330, 32, 32)" fill="#000"/>
<animateTransform attributeName="transform" type="rotate" calcMode="discrete" values="0 32 32;30 32 32;60 32 32;90 32 32;120 32 32;150 32 32;180 32 32;210 32 32;240 32 32;270 32 32;300 32 32;330 32 32" dur="0.8s" repeatCount="indefinite"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,32 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64" height="32" viewBox="0 0 64 32">
<g fill="#888">
<polyline points="3 4 12 4 7.5 12"/>
<g transform="translate(0,16)">
<polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
</g>
</g>
<g fill="#444" transform="translate(16,0)">
<polyline points="3 4 12 4 7.5 12"/>
<g transform="translate(0,16)">
<polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
</g>
</g>
<g fill="#FFF" transform="translate(32,0)">
<polyline points="3 4 12 4 7.5 12"/>
<g transform="translate(0,16)">
<polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
</g>
</g>
<g fill="rgba(255, 255, 255, 0.7)" transform="translate(48,0)">
<polyline points="3 4 12 4 7.5 12"/>
<g transform="translate(0,16)">
<polyline points="3 4 12 4 7.5 12" transform="rotate(-90, 7.5, 7.5)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,185 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* ProgressReporter
*
* This class is used by long-winded tasks to report progress to observers.
* If a task has subtasks that want to report their own progress, these
* subtasks can have their own progress reporters which are hooked up to the
* parent progress reporter, resulting in a tree structure. A parent progress
* reporter will calculate its progress value as a weighted sum of its
* subreporters' progress values.
*
* A progress reporter has a state, an action, and a progress value.
*
* - state is one of STATE_WAITING, STATE_DOING and STATE_FINISHED.
* - action is a string that describes the current task.
* - progress is the progress value as a number between 0 and 1, or NaN if
* indeterminate.
*
* A progress reporter starts out in the WAITING state. The DOING state is
* entered with the begin method which also sets the action. While the task is
* executing, the progress value can be updated with the setProgress method.
* When a task has finished, it can call the finish method which is just a
* shorthand for setProgress(1); this will set the state to FINISHED.
*
* Progress observers can be added with the addListener method which takes a
* function callback. Whenever the progress value or state change, all
* listener callbacks will be called with the progress reporter object. The
* observer can get state, progress value and action by calling the getter
* methods getState(), getProgress() and getAction().
*
* Creating child progress reporters for subtasks can be done with the
* addSubreporter(s) methods. If a progress reporter has subreporters, normal
* progress report functions (setProgress and finish) can no longer be called.
* Instead, the parent reporter will listen to progress changes on its
* subreporters and update its state automatically, and then notify its own
* listeners.
* When adding a subreporter, you are expected to provide an estimated
* duration for the subtask. This value will be used as a weight when
* calculating the progress of the parent reporter.
*/
const gDebugExpectedDurations = false;
function ProgressReporter() {
this._observers = [];
this._subreporters = [];
this._subreporterExpectedDurationsSum = 0;
this._progress = 0;
this._state = ProgressReporter.STATE_WAITING;
this._action = "";
}
ProgressReporter.STATE_WAITING = 0;
ProgressReporter.STATE_DOING = 1;
ProgressReporter.STATE_FINISHED = 2;
ProgressReporter.prototype = {
getProgress: function () {
return this._progress;
},
getState: function () {
return this._state;
},
setAction: function (action) {
this._action = action;
this._reportProgress();
},
getAction: function () {
switch (this._state) {
case ProgressReporter.STATE_WAITING:
return "Waiting for preceding tasks to finish...";
case ProgressReporter.STATE_DOING:
return this._action;
case ProgressReporter.STATE_FINISHED:
return "Finished.";
default:
throw "Broken state";
}
},
addListener: function (callback) {
this._observers.push(callback);
},
addSubreporter: function (expectedDuration) {
this._subreporterExpectedDurationsSum += expectedDuration;
var subreporter = new ProgressReporter();
var self = this;
subreporter.addListener(function (progress) {
self._recalculateProgressFromSubreporters();
self._recalculateStateAndActionFromSubreporters();
self._reportProgress();
});
this._subreporters.push({ expectedDuration: expectedDuration, reporter: subreporter });
return subreporter;
},
addSubreporters: function (expectedDurations) {
var reporters = {};
for (var key in expectedDurations) {
reporters[key] = this.addSubreporter(expectedDurations[key]);
}
return reporters;
},
begin: function (action) {
this._startTime = Date.now();
this._state = ProgressReporter.STATE_DOING;
this._action = action;
this._reportProgress();
},
setProgress: function (progress) {
if (this._subreporters.length > 0)
throw "Can't call setProgress on a progress reporter with subreporters";
if (progress != this._progress &&
(progress == 1 ||
(isNaN(progress) != isNaN(this._progress)) ||
(progress - this._progress >= 0.01))) {
this._progress = progress;
if (progress == 1)
this._transitionToFinished();
this._reportProgress();
}
},
finish: function () {
this.setProgress(1);
},
_recalculateProgressFromSubreporters: function () {
if (this._subreporters.length == 0)
throw "Can't _recalculateProgressFromSubreporters on a progress reporter without any subreporters";
this._progress = 0;
for (var i = 0; i < this._subreporters.length; i++) {
var expectedDuration = this._subreporters[i].expectedDuration;
var reporter = this._subreporters[i].reporter;
this._progress += reporter.getProgress() * expectedDuration / this._subreporterExpectedDurationsSum;
}
},
_recalculateStateAndActionFromSubreporters: function () {
if (this._subreporters.length == 0)
throw "Can't _recalculateStateAndActionFromSubreporters on a progress reporter without any subreporters";
var actions = [];
var allWaiting = true;
var allFinished = true;
for (var i = 0; i < this._subreporters.length; i++) {
var expectedDuration = this._subreporters[i].expectedDuration;
var reporter = this._subreporters[i].reporter;
var state = reporter.getState();
if (state != ProgressReporter.STATE_WAITING)
allWaiting = false;
if (state != ProgressReporter.STATE_FINISHED)
allFinished = false;
if (state == ProgressReporter.STATE_DOING)
actions.push(reporter.getAction());
}
if (allFinished) {
this._transitionToFinished();
} else if (!allWaiting) {
this._state = ProgressReporter.STATE_DOING;
if (actions.length == 0) {
this._action = "About to start next task..."
} else {
this._action = actions.join("\n");
}
}
},
_transitionToFinished: function () {
this._state = ProgressReporter.STATE_FINISHED;
if (gDebugExpectedDurations) {
this._realDuration = Date.now() - this._startTime;
if (this._subreporters.length) {
for (var i = 0; i < this._subreporters.length; i++) {
var expectedDuration = this._subreporters[i].expectedDuration;
var reporter = this._subreporters[i].reporter;
var realDuration = reporter._realDuration;
dump("For reporter with expectedDuration " + expectedDuration + ", real duration was " + realDuration + "\n");
}
}
}
},
_reportProgress: function () {
for (var i = 0; i < this._observers.length; i++) {
this._observers[i](this);
}
},
};

View File

@ -0,0 +1,104 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var gInstanceUID;
/**
* Sends a message to the parent window with a status
* update.
*
* @param string status
* Status to send to the parent page:
* - loaded, when page is loaded.
* - start, when user wants to start profiling.
* - stop, when user wants to stop profiling.
*/
function notifyParent(status) {
if (!gInstanceUID) {
gInstanceUID = window.location.search.substr(1);
}
window.parent.postMessage({
uid: gInstanceUID,
status: status
}, "*");
}
/**
* A listener for incoming messages from the parent
* page. All incoming messages must be stringified
* JSON objects to be compatible with Cleopatra's
* format:
*
* {
* task: string,
* ...
* }
*
* This listener recognizes two tasks: onStarted and
* onStopped.
*
* @param object event
* PostMessage event object.
*/
function onParentMessage(event) {
var start = document.getElementById("startWrapper");
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";
}
}
window.addEventListener("message", onParentMessage);
/**
* Main entry point. This function initializes Cleopatra
* in the light mode and creates all the UI we need.
*/
function initUI() {
gLightMode = true;
gJavaScriptOnly = true;
var container = document.createElement("div");
container.id = "ui";
gMainArea = document.createElement("div");
gMainArea.id = "mainarea";
container.appendChild(gMainArea);
document.body.appendChild(container);
var startButton = document.createElement("button");
startButton.innerHTML = "Start";
startButton.addEventListener("click", function (event) {
event.target.setAttribute("disabled", true);
notifyParent("start");
}, false);
var stopButton = document.createElement("button");
stopButton.innerHTML = "Stop";
stopButton.addEventListener("click", function (event) {
event.target.setAttribute("disabled", true);
notifyParent("stop");
}, false);
var controlPane = document.createElement("div");
controlPane.className = "controlPane";
controlPane.innerHTML =
"<p id='startWrapper'>Click <span class='btn'></span> to start profiling.</p>" +
"<p id='stopWrapper'>Click <span class='btn'></span> to stop profiling.</p>";
controlPane.querySelector("#startWrapper > span.btn").appendChild(startButton);
controlPane.querySelector("#stopWrapper > span.btn").appendChild(stopButton);
gMainArea.appendChild(controlPane);
}

View File

@ -0,0 +1,275 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Array.prototype.clone = function() { return this.slice(0); }
function makeSample(frames, extraInfo, lines) {
return {
frames: frames,
extraInfo: extraInfo,
lines: lines
};
}
function cloneSample(sample) {
return makeSample(sample.frames.clone(), sample.extraInfo, sample.lines.clone());
}
function bucketsBySplittingArray(array, maxItemsPerBucket) {
var buckets = [];
while (buckets.length * maxItemsPerBucket < array.length) {
buckets.push(array.slice(buckets.length * maxItemsPerBucket,
(buckets.length + 1) * maxItemsPerBucket));
}
return buckets;
}
var gParserWorker = new Worker("profiler/cleopatra/js/parserWorker.js");
gParserWorker.nextRequestID = 0;
function WorkerRequest(worker) {
var self = this;
this._eventListeners = {};
var requestID = worker.nextRequestID++;
this._requestID = requestID;
this._worker = worker;
this._totalReporter = new ProgressReporter();
this._totalReporter.addListener(function (reporter) {
self._fireEvent("progress", reporter.getProgress(), reporter.getAction());
})
this._sendChunkReporter = this._totalReporter.addSubreporter(500);
this._executeReporter = this._totalReporter.addSubreporter(3000);
this._receiveChunkReporter = this._totalReporter.addSubreporter(100);
this._totalReporter.begin("Processing task in worker...");
var partialResult = null;
function onMessageFromWorker(msg) {
pendingMessages.push(msg);
scheduleMessageProcessing();
}
function processMessage(msg) {
var startTime = Date.now();
var data = msg.data;
var readTime = Date.now() - startTime;
if (readTime > 10)
console.log("reading data from worker message: " + readTime + "ms");
if (data.requestID == requestID || !data.requestID) {
switch(data.type) {
case "error":
self._sendChunkReporter.setAction("Error in worker: " + data.error);
self._executeReporter.setAction("Error in worker: " + data.error);
self._receiveChunkReporter.setAction("Error in worker: " + data.error);
self._totalReporter.setAction("Error in worker: " + data.error);
PROFILERERROR("Error in worker: " + data.error);
self._fireEvent("error", data.error);
break;
case "progress":
self._executeReporter.setProgress(data.progress);
break;
case "finished":
self._executeReporter.finish();
self._receiveChunkReporter.begin("Receiving data from worker...");
self._receiveChunkReporter.finish();
self._fireEvent("finished", data.result);
worker.removeEventListener("message", onMessageFromWorker);
break;
case "finishedStart":
partialResult = null;
self._totalReceiveChunks = data.numChunks;
self._gotReceiveChunks = 0;
self._executeReporter.finish();
self._receiveChunkReporter.begin("Receiving data from worker...");
break;
case "finishedChunk":
partialResult = partialResult ? partialResult.concat(data.chunk) : data.chunk;
var chunkIndex = self._gotReceiveChunks++;
self._receiveChunkReporter.setProgress((chunkIndex + 1) / self._totalReceiveChunks);
break;
case "finishedEnd":
self._receiveChunkReporter.finish();
self._fireEvent("finished", partialResult);
worker.removeEventListener("message", onMessageFromWorker);
break;
}
// dump log if present
if (data.log) {
for (var line in data.log) {
PROFILERLOG(line);
}
}
}
}
var pendingMessages = [];
var messageProcessingTimer = 0;
function processMessages() {
messageProcessingTimer = 0;
processMessage(pendingMessages.shift());
if (pendingMessages.length)
scheduleMessageProcessing();
}
function scheduleMessageProcessing() {
if (messageProcessingTimer)
return;
messageProcessingTimer = setTimeout(processMessages, 10);
}
worker.addEventListener("message", onMessageFromWorker);
}
WorkerRequest.prototype = {
send: function WorkerRequest_send(task, taskData) {
this._sendChunkReporter.begin("Sending data to worker...");
var startTime = Date.now();
this._worker.postMessage({
requestID: this._requestID,
task: task,
taskData: taskData
});
var postTime = Date.now() - startTime;
if (true || postTime > 10)
console.log("posting message to worker: " + postTime + "ms");
this._sendChunkReporter.finish();
this._executeReporter.begin("Processing worker request...");
},
sendInChunks: function WorkerRequest_sendInChunks(task, taskData, params, maxChunkSize) {
this._sendChunkReporter.begin("Sending data to worker...");
var self = this;
var chunks = bucketsBySplittingArray(taskData, maxChunkSize);
var pendingMessages = [
{
requestID: this._requestID,
task: "chunkedStart",
numChunks: chunks.length
}
].concat(chunks.map(function (chunk) {
return {
requestID: self._requestID,
task: "chunkedChunk",
chunk: chunk
};
})).concat([
{
requestID: this._requestID,
task: "chunkedEnd"
},
{
requestID: this._requestID,
params: params,
task: task
},
]);
var totalMessages = pendingMessages.length;
var numSentMessages = 0;
function postMessage(msg) {
var msgIndex = numSentMessages++;
var startTime = Date.now();
self._worker.postMessage(msg);
var postTime = Date.now() - startTime;
if (postTime > 10)
console.log("posting message to worker: " + postTime + "ms");
self._sendChunkReporter.setProgress((msgIndex + 1) / totalMessages);
}
var messagePostingTimer = 0;
function postMessages() {
messagePostingTimer = 0;
postMessage(pendingMessages.shift());
if (pendingMessages.length) {
scheduleMessagePosting();
} else {
self._sendChunkReporter.finish();
self._executeReporter.begin("Processing worker request...");
}
}
function scheduleMessagePosting() {
if (messagePostingTimer)
return;
messagePostingTimer = setTimeout(postMessages, 10);
}
scheduleMessagePosting();
},
// TODO: share code with TreeView
addEventListener: function WorkerRequest_addEventListener(eventName, callbackFunction) {
if (!(eventName in this._eventListeners))
this._eventListeners[eventName] = [];
if (this._eventListeners[eventName].indexOf(callbackFunction) != -1)
return;
this._eventListeners[eventName].push(callbackFunction);
},
removeEventListener: function WorkerRequest_removeEventListener(eventName, callbackFunction) {
if (!(eventName in this._eventListeners))
return;
var index = this._eventListeners[eventName].indexOf(callbackFunction);
if (index == -1)
return;
this._eventListeners[eventName].splice(index, 1);
},
_fireEvent: function WorkerRequest__fireEvent(eventName, eventObject, p1) {
if (!(eventName in this._eventListeners))
return;
this._eventListeners[eventName].forEach(function (callbackFunction) {
callbackFunction(eventObject, p1);
});
},
}
var Parser = {
parse: function Parser_parse(data, params) {
console.log("profile num chars: " + data.length);
var request = new WorkerRequest(gParserWorker);
request.sendInChunks("parseRawProfile", data, params, 3000000);
return request;
},
updateFilters: function Parser_updateFilters(filters) {
var request = new WorkerRequest(gParserWorker);
request.send("updateFilters", {
filters: filters,
profileID: 0
});
return request;
},
updateViewOptions: function Parser_updateViewOptions(options) {
var request = new WorkerRequest(gParserWorker);
request.send("updateViewOptions", {
options: options,
profileID: 0
});
return request;
},
getSerializedProfile: function Parser_getSerializedProfile(complete, callback) {
var request = new WorkerRequest(gParserWorker);
request.send("getSerializedProfile", {
profileID: 0,
complete: complete
});
request.addEventListener("finished", callback);
},
calculateHistogramData: function Parser_calculateHistogramData() {
var request = new WorkerRequest(gParserWorker);
request.send("calculateHistogramData", {
profileID: 0
});
return request;
},
calculateDiagnosticItems: function Parser_calculateDiagnosticItems(meta) {
var request = new WorkerRequest(gParserWorker);
request.send("calculateDiagnosticItems", {
profileID: 0,
meta: meta
});
return request;
},
updateLogSetting: function Parser_updateLogSetting() {
var request = new WorkerRequest(gParserWorker);
request.send("initWorker", {
debugLog: gDebugLog,
debugTrace: gDebugTrace,
});
return request;
},
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,641 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var kMaxChunkDuration = 30; // ms
var escape = document.createElement('textarea');
function escapeHTML(html) {
escape.innerHTML = html;
return escape.innerHTML;
}
function unescapeHTML(html) {
escape.innerHTML = html;
return escape.value;
}
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);
};
var cancelAnimationFrame = window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function(callback, element) {
if (requestAnimationFrame_timeout) {
window.clearTimeout(requestAnimationFrame_timeout);
requestAnimationFrame_timeout = null;
}
};
function TreeView() {
this._eventListeners = {};
this._pendingActions = [];
this._pendingActionsProcessingCallback = null;
this._container = document.createElement("div");
this._container.className = "treeViewContainer";
this._container.setAttribute("tabindex", "0"); // make it focusable
this._header = document.createElement("ul");
this._header.className = "treeHeader";
this._container.appendChild(this._header);
this._verticalScrollbox = document.createElement("div");
this._verticalScrollbox.className = "treeViewVerticalScrollbox";
this._container.appendChild(this._verticalScrollbox);
this._leftColumnBackground = document.createElement("div");
this._leftColumnBackground.className = "leftColumnBackground";
this._verticalScrollbox.appendChild(this._leftColumnBackground);
this._horizontalScrollbox = document.createElement("div");
this._horizontalScrollbox.className = "treeViewHorizontalScrollbox";
this._verticalScrollbox.appendChild(this._horizontalScrollbox);
this._contextMenu = document.createElement("menu");
this._contextMenu.setAttribute("type", "context");
this._contextMenu.id = "contextMenuForTreeView" + TreeView.instanceCounter++;
this._container.appendChild(this._contextMenu);
this._busyCover = document.createElement("div");
this._busyCover.className = "busyCover";
this._container.appendChild(this._busyCover);
this._abortToggleAll = false;
var self = this;
this._container.onkeydown = function (e) {
self._onkeypress(e);
};
this._container.onclick = function (e) {
self._onclick(e);
};
this._verticalScrollbox.addEventListener("contextmenu", function(event) {
self._populateContextMenu(event);
}, true);
this._setUpScrolling();
};
TreeView.instanceCounter = 0;
TreeView.prototype = {
getContainer: function TreeView_getContainer() {
return this._container;
},
setColumns: function TreeView_setColumns(columns) {
this._header.innerHTML = "";
for (var i = 0; i < columns.length; i++) {
var li = document.createElement("li");
li.className = "treeColumnHeader treeColumnHeader" + i;
li.id = columns[i].name + "Header";
li.textContent = columns[i].title;
this._header.appendChild(li);
}
},
dataIsOutdated: function TreeView_dataIsOutdated() {
this._busyCover.classList.add("busy");
},
display: function TreeView_display(data, filterByName) {
this._busyCover.classList.remove("busy");
this._filterByName = filterByName;
this._filterByNameReg = null; // lazy init
if (this._filterByName === "")
this._filterByName = null;
this._horizontalScrollbox.innerHTML = "";
this._horizontalScrollbox.data = data[0].getData();
if (this._pendingActionsProcessingCallback) {
cancelAnimationFrame(this._pendingActionsProcessingCallback);
this._pendingActionsProcessingCallback = 0;
}
this._pendingActions = [];
this._pendingActions.push({
parentElement: this._horizontalScrollbox,
parentNode: null,
data: data[0].getData()
});
this._processPendingActionsChunk();
this._select(this._horizontalScrollbox.firstChild);
this._toggle(this._horizontalScrollbox.firstChild);
this._container.focus();
},
// Provide a snapshot of the reverse selection to restore with 'invert callback'
getReverseSelectionSnapshot: function TreeView__getReverseSelectionSnapshot(isJavascriptOnly) {
if (!this._selectedNode)
return;
var snapshot = [];
var curr = this._selectedNode.data;
while(curr) {
if (isJavascriptOnly && curr.isJSFrame || !isJavascriptOnly) {
snapshot.push(curr.name);
//dump(JSON.stringify(curr.name) + "\n");
}
if (curr.children && curr.children.length >= 1) {
curr = curr.children[0].getData();
} else {
break;
}
}
return snapshot.reverse();
},
// Provide a snapshot of the current selection to restore
getSelectionSnapshot: function TreeView__getSelectionSnapshot(isJavascriptOnly) {
var snapshot = [];
var curr = this._selectedNode;
while(curr) {
if (isJavascriptOnly && curr.data.isJSFrame || !isJavascriptOnly) {
snapshot.push(curr.data.name);
//dump(JSON.stringify(curr.data.name) + "\n");
}
curr = curr.treeParent;
}
return snapshot.reverse();
},
setSelection: function TreeView_setSelection(frames) {
this.restoreSelectionSnapshot(frames, false);
},
// Take a selection snapshot and restore the selection
restoreSelectionSnapshot: function TreeView_restoreSelectionSnapshot(snapshot, allowNonContigious) {
var currNode = this._horizontalScrollbox.firstChild;
if (currNode.data.name == snapshot[0] || snapshot[0] == "(total)") {
snapshot.shift();
}
//dump("len: " + snapshot.length + "\n");
next_level: while (currNode && snapshot.length > 0) {
this._toggle(currNode, false, true);
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");
snapshot.shift();
this._toggle(currNode, false, true);
currNode = currNode.treeChildren[i];
continue next_level;
}
}
if (allowNonContigious === true) {
// We need to do a Breadth-first search to find a match
var pendingSearch = [currNode.data];
while (pendingSearch.length > 0) {
var node = pendingSearch.shift();
//dump("searching: " + node.name + " for: " + snapshot[0] + "\n");
if (!node.children)
continue;
for (var i = 0; i < node.children.length; i++) {
var childNode = node.children[i].getData();
if (childNode.name == snapshot[0]) {
//dump("found: " + childNode.name + "\n");
snapshot.shift();
var nodesToToggle = [childNode];
while (nodesToToggle[0].name != currNode.data.name) {
nodesToToggle.splice(0, 0, nodesToToggle[0].parent);
}
var lastToggle = currNode;
for (var j = 0; j < nodesToToggle.length; j++) {
for (var k = 0; k < lastToggle.treeChildren.length; k++) {
if (lastToggle.treeChildren[k].data.name == nodesToToggle[j].name) {
//dump("Expend: " + nodesToToggle[j].name + "\n");
this._toggle(lastToggle.treeChildren[k], false, true);
lastToggle = lastToggle.treeChildren[k];
this._syncProcessPendingActionProcessing();
}
}
}
currNode = lastToggle;
continue next_level;
}
//dump("pending: " + childNode.name + "\n");
pendingSearch.push(childNode);
}
}
}
break; // Didn't find child node matching
}
if (currNode == this._horizontalScrollbox) {
PROFILERERROR("Failed to restore selection, could not find root.\n");
return;
}
this._toggle(currNode, true, true);
this._select(currNode);
},
_processPendingActionsChunk: function TreeView__processPendingActionsChunk(isSync) {
this._pendingActionsProcessingCallback = 0;
var startTime = Date.now();
var endTime = startTime + kMaxChunkDuration;
while ((isSync == true || Date.now() < endTime) && this._pendingActions.length > 0) {
this._processOneAction(this._pendingActions.shift());
}
this._scrollHeightChanged();
this._schedulePendingActionProcessing();
},
_schedulePendingActionProcessing: function TreeView__schedulePendingActionProcessing() {
if (!this._pendingActionsProcessingCallback && this._pendingActions.length > 0) {
var self = this;
this._pendingActionsProcessingCallback = requestAnimationFrame(function () {
self._processPendingActionsChunk();
});
}
},
_syncProcessPendingActionProcessing: function TreeView__syncProcessPendingActionProcessing() {
this._processPendingActionsChunk(true);
},
_processOneAction: function TreeView__processOneAction(action) {
var li = this._createTree(action.parentElement, action.parentNode, action.data);
if ("allChildrenCollapsedValue" in action) {
if (this._abortToggleAll)
return;
this._toggleAll(li, action.allChildrenCollapsedValue, true);
}
},
addEventListener: function TreeView_addEventListener(eventName, callbackFunction) {
if (!(eventName in this._eventListeners))
this._eventListeners[eventName] = [];
if (this._eventListeners[eventName].indexOf(callbackFunction) != -1)
return;
this._eventListeners[eventName].push(callbackFunction);
},
removeEventListener: function TreeView_removeEventListener(eventName, callbackFunction) {
if (!(eventName in this._eventListeners))
return;
var index = this._eventListeners[eventName].indexOf(callbackFunction);
if (index == -1)
return;
this._eventListeners[eventName].splice(index, 1);
},
_fireEvent: function TreeView__fireEvent(eventName, eventObject) {
if (!(eventName in this._eventListeners))
return;
this._eventListeners[eventName].forEach(function (callbackFunction) {
callbackFunction(eventObject);
});
},
_setUpScrolling: function TreeView__setUpScrolling() {
var waitingForPaint = false;
var accumulatedDeltaX = 0;
var accumulatedDeltaY = 0;
var self = this;
function scrollListener(e) {
if (!waitingForPaint) {
requestAnimationFrame(function () {
self._horizontalScrollbox.scrollLeft += accumulatedDeltaX;
self._verticalScrollbox.scrollTop += accumulatedDeltaY;
accumulatedDeltaX = 0;
accumulatedDeltaY = 0;
waitingForPaint = false;
});
waitingForPaint = true;
}
if (e.axis == e.HORIZONTAL_AXIS) {
accumulatedDeltaX += e.detail;
} else {
accumulatedDeltaY += e.detail;
}
e.preventDefault();
}
this._verticalScrollbox.addEventListener("MozMousePixelScroll", scrollListener, false);
this._verticalScrollbox.cleanUp = function () {
self._verticalScrollbox.removeEventListener("MozMousePixelScroll", scrollListener, false);
};
},
_scrollHeightChanged: function TreeView__scrollHeightChanged() {
this._leftColumnBackground.style.height = this._horizontalScrollbox.getBoundingClientRect().height + 'px';
},
_createTree: function TreeView__createTree(parentElement, parentNode, data) {
var div = document.createElement("div");
div.className = "treeViewNode collapsed";
var hasChildren = ("children" in data) && (data.children.length > 0);
if (!hasChildren)
div.classList.add("leaf");
var treeLine = document.createElement("div");
treeLine.className = "treeLine";
treeLine.innerHTML = this._HTMLForFunction(data);
// When this item is toggled we will expand its children
div.pendingExpand = [];
div.treeLine = treeLine;
div.data = data;
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.appendChild(parent);
}
if (parentNode) {
parentNode.treeChildren.push(div);
}
parentElement.appendChild(div);
return div;
},
_populateContextMenu: function TreeView__populateContextMenu(event) {
this._verticalScrollbox.setAttribute("contextmenu", "");
var target = event.target;
if (target.classList.contains("expandCollapseButton") ||
target.classList.contains("focusCallstackButton"))
return;
var li = this._getParentTreeViewNode(target);
if (!li)
return;
this._select(li);
this._contextMenu.innerHTML = "";
var self = this;
this._contextMenuForFunction(li.data).forEach(function (menuItem) {
var menuItemNode = document.createElement("menuitem");
menuItemNode.onclick = (function (menuItem) {
return function() {
self._contextMenuClick(li.data, menuItem);
};
})(menuItem);
menuItemNode.label = menuItem;
self._contextMenu.appendChild(menuItemNode);
});
this._verticalScrollbox.setAttribute("contextmenu", this._contextMenu.id);
},
_contextMenuClick: function TreeView__contextMenuClick(node, menuItem) {
this._fireEvent("contextMenuClick", { node: node, menuItem: menuItem });
},
_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"
)) {
menu.push("View Source");
}
if (node.isJSFrame && node.scriptLocation) {
menu.push("View JS Source");
}
menu.push("Focus Frame");
menu.push("Focus Callstack");
menu.push("Google Search");
menu.push("Plugin View: Pie");
menu.push("Plugin View: Tree");
return menu;
},
_HTMLForFunction: function TreeView__HTMLForFunction(node) {
var nodeName = escapeHTML(node.name);
var libName = node.library;
if (this._filterByName) {
if (!this._filterByNameReg) {
this._filterByName = RegExp.escape(this._filterByName);
this._filterByNameReg = new RegExp("(" + this._filterByName + ")","gi");
}
nodeName = nodeName.replace(this._filterByNameReg, "<a style='color:red;'>$1</a>");
libName = libName.replace(this._filterByNameReg, "<a style='color:red;'>$1</a>");
}
var samplePercentage;
if (isNaN(node.ratio)) {
samplePercentage = "";
} else {
samplePercentage = (100 * node.ratio).toFixed(1) + "%";
}
return '<input type="button" value="Expand / Collapse" class="expandCollapseButton" tabindex="-1"> ' +
'<span class="sampleCount">' + node.counter + '</span> ' +
'<span class="samplePercentage">' + samplePercentage + '</span> ' +
'<span class="selfSampleCount">' + node.selfCounter + '</span> ' +
'<span class="functionName">' + nodeName + '</span>' +
'<span class="libraryName">' + libName + '</span>' +
'<input type="button" value="Focus Callstack" title="Focus Callstack" class="focusCallstackButton" tabindex="-1">';
},
_resolveChildren: function TreeView__resolveChildren(div, childrenCollapsedValue) {
while (div.pendingExpand != null && div.pendingExpand.length > 0) {
var pendingExpand = div.pendingExpand.shift();
pendingExpand.allChildrenCollapsedValue = childrenCollapsedValue;
this._pendingActions.push(pendingExpand);
this._schedulePendingActionProcessing();
}
},
_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");
} else {
this._resolveChildren(div, true);
div.classList.remove("collapsed");
}
if (!suppressScrollHeightNotification)
this._scrollHeightChanged();
},
_toggleAll: function TreeView__toggleAll(subtreeRoot, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) {
// Reset abort
this._abortToggleAll = false;
// Expands / collapses all child nodes, too.
if (newCollapsedValue === undefined)
newCollapsedValue = !this._isCollapsed(subtreeRoot);
if (!newCollapsedValue) {
// expanding
this._resolveChildren(subtreeRoot, newCollapsedValue);
}
this._toggle(subtreeRoot, newCollapsedValue, true);
for (var i = 0; i < subtreeRoot.treeChildren.length; ++i) {
this._toggleAll(subtreeRoot.treeChildren[i], newCollapsedValue, true);
}
if (!suppressScrollHeightNotification)
this._scrollHeightChanged();
},
_getParent: function TreeView__getParent(div) {
return div.treeParent;
},
_getFirstChild: function TreeView__getFirstChild(div) {
if (this._isCollapsed(div))
return null;
var child = div.treeChildren[0];
return child;
},
_getLastChild: function TreeView__getLastChild(div) {
if (this._isCollapsed(div))
return div;
var lastChild = div.treeChildren[div.treeChildren.length-1];
if (lastChild == null)
return div;
return this._getLastChild(lastChild);
},
_getPrevSib: function TreeView__getPevSib(div) {
if (div.treeParent == null)
return null;
var nodeIndex = div.treeParent.treeChildren.indexOf(div);
if (nodeIndex == 0)
return null;
return div.treeParent.treeChildren[nodeIndex-1];
},
_getNextSib: function TreeView__getNextSib(div) {
if (div.treeParent == null)
return null;
var nodeIndex = div.treeParent.treeChildren.indexOf(div);
if (nodeIndex == div.treeParent.treeChildren.length - 1)
return this._getNextSib(div.treeParent);
return div.treeParent.treeChildren[nodeIndex+1];
},
_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
// higher than the scroll port, the left and top edges are prioritized over
// the right and bottom edges.
// If maxImportantWidth is set, parts of the beyond this widths are
// considered as not important; they'll not be moved into view.
if (maxImportantWidth === undefined)
maxImportantWidth = Infinity;
var visibleRect = {
left: this._horizontalScrollbox.getBoundingClientRect().left + 150, // TODO: un-hardcode 150
top: this._verticalScrollbox.getBoundingClientRect().top,
right: this._horizontalScrollbox.getBoundingClientRect().right,
bottom: this._verticalScrollbox.getBoundingClientRect().bottom
}
var r = element.getBoundingClientRect();
var right = Math.min(r.right, r.left + maxImportantWidth);
var leftCutoff = visibleRect.left - r.left;
var rightCutoff = right - visibleRect.right;
var topCutoff = visibleRect.top - r.top;
var bottomCutoff = r.bottom - visibleRect.bottom;
if (leftCutoff > 0)
this._horizontalScrollbox.scrollLeft -= leftCutoff;
else if (rightCutoff > 0)
this._horizontalScrollbox.scrollLeft += Math.min(rightCutoff, -leftCutoff);
if (topCutoff > 0)
this._verticalScrollbox.scrollTop -= topCutoff;
else if (bottomCutoff > 0)
this._verticalScrollbox.scrollTop += Math.min(bottomCutoff, -topCutoff);
},
_select: function TreeView__select(li) {
if (this._selectedNode != null) {
this._selectedNode.treeLine.classList.remove("selected");
this._selectedNode = null;
}
if (li) {
li.treeLine.classList.add("selected");
this._selectedNode = li;
var functionName = li.treeLine.querySelector(".functionName");
this._scrollIntoView(functionName, 400);
this._fireEvent("select", li.data);
}
},
_isCollapsed: function TreeView__isCollapsed(div) {
return div.classList.contains("collapsed");
},
_getParentTreeViewNode: function TreeView__getParentTreeViewNode(node) {
while (node) {
if (node.nodeType != node.ELEMENT_NODE)
break;
if (node.classList.contains("treeViewNode"))
return node;
node = node.parentNode;
}
return null;
},
_onclick: function TreeView__onclick(event) {
var target = event.target;
var node = this._getParentTreeViewNode(target);
if (!node)
return;
if (target.classList.contains("expandCollapseButton")) {
if (event.altKey)
this._toggleAll(node);
else
this._toggle(node);
} else if (target.classList.contains("focusCallstackButton")) {
this._fireEvent("focusCallstackButtonClicked", node.data);
} else {
this._select(node);
if (event.detail == 2) // dblclick
this._toggle(node);
}
},
_onkeypress: function TreeView__onkeypress(event) {
if (event.ctrlKey || event.altKey || event.metaKey)
return;
this._abortToggleAll = true;
var selected = this._selectedNode;
if (event.keyCode < 37 || event.keyCode > 40) {
if (event.keyCode != 0 ||
String.fromCharCode(event.charCode) != '*') {
return;
}
}
event.stopPropagation();
event.preventDefault();
if (!selected)
return;
if (event.keyCode == 37) { // KEY_LEFT
var isCollapsed = this._isCollapsed(selected);
if (!isCollapsed) {
this._toggle(selected);
} else {
var parent = this._getParent(selected);
if (parent != null) {
this._select(parent);
}
}
} else if (event.keyCode == 38) { // KEY_UP
var prevSib = this._getPrevSib(selected);
var parent = this._getParent(selected);
if (prevSib != null) {
this._select(this._getLastChild(prevSib));
} else if (parent != null) {
this._select(parent);
}
} else if (event.keyCode == 39) { // KEY_RIGHT
var isCollapsed = this._isCollapsed(selected);
if (isCollapsed) {
this._toggle(selected);
} else {
// Do KEY_DOWN
var nextSib = this._getNextSib(selected);
var child = this._getFirstChild(selected);
if (child != null) {
this._select(child);
} else if (nextSib) {
this._select(nextSib);
}
}
} else if (event.keyCode == 40) { // KEY_DOWN
var nextSib = this._getNextSib(selected);
var child = this._getFirstChild(selected);
if (child != null) {
this._select(child);
} else if (nextSib) {
this._select(nextSib);
}
} else if (String.fromCharCode(event.charCode) == '*') {
this._toggleAll(selected);
}
},
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
.profile-name {
font-size: 13px;
padding: 8px;
}
#profiles-list > li {
width: 180px;
cursor: pointer;
}
.splitview-nav-container button {
color: white;
background-clip: padding-box;
border-bottom: 1px solid hsla(210,16%,76%,.1);
box-shadow: inset 0 -1px 0 hsla(210,8%,5%,.25);
-moz-padding-end: 8px;
-moz-box-align: center;
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/splitview.css"?>
<?xml-stylesheet href="chrome://browser/content/splitview.css"?>
<?xml-stylesheet href="chrome://browser/content/profiler.css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<box flex="1" id="profiler-chrome" class="splitview-root">
<box class="splitview-controller" width="180px">
<box class="splitview-main"></box>
<box class="splitview-nav-container">
<ol class="splitview-nav" id="profiles-list">
<!-- Example:
<li class="splitview-active" id="profile-1" data-uid="1">
<h1 class="profile-name">Profile 1</h1>
</li>
-->
</ol>
<spacer flex="1"/>
<toolbar class="devtools-toolbar" mode="full">
<toolbarbutton id="profiler-create"
class="devtools-toolbarbutton"
label="New"
disabled="true"/>
</toolbar>
</box> <!-- splitview-nav-container -->
</box> <!-- splitview-controller -->
<box flex="1">
<vbox flex="1" id="profiler-report">
<!-- Example:
<iframe id="profiler-cleo-1"
src="devtools/cleopatra.html" flex="1"></iframe>
-->
</vbox>
</box>
</box>
</window>

View File

@ -0,0 +1,19 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DEPTH = @DEPTH@
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
relativesrcdir = @relativesrcdir@
include $(DEPTH)/config/autoconf.mk
MOCHITEST_BROWSER_FILES = \
browser_profiler_run.js \
browser_profiler_controller.js \
browser_profiler_profiles.js \
head.js \
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,54 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
let gTab, gPanel;
function test() {
waitForExplicitFinish();
setUp(URL, function onSetUp(tab, browser, panel) {
gTab = tab;
gPanel = panel;
testInactive(testStart);
});
}
function testInactive(next=function(){}) {
gPanel.controller.isActive(function (err, isActive) {
ok(!err, "isActive didn't return any errors");
ok(!isActive, "Profiler is not active");
next();
});
}
function testActive(next=function(){}) {
gPanel.controller.isActive(function (err, isActive) {
ok(!err, "isActive didn't return any errors");
ok(isActive, "Profiler is active");
next();
});
}
function testStart() {
gPanel.controller.start(function (err) {
ok(!err, "Profiler started without errors");
testActive(testStop);
});
}
function testStop() {
gPanel.controller.stop(function (err, data) {
ok(!err, "Profiler stopped without errors");
ok(data, "Profiler returned some data");
testInactive(function () {
tearDown(gTab, function () {
gTab = null;
gPanel = null;
});
});
});
}

View File

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
let gTab, gPanel;
function test() {
waitForExplicitFinish();
setUp(URL, function onSetUp(tab, browser, panel) {
gTab = tab;
gPanel = panel;
panel.once("profileCreated", onProfileCreated);
panel.once("profileSwitched", onProfileSwitched);
testNewProfile();
});
}
function testNewProfile() {
is(gPanel.profiles.size, 1, "There is only one profile");
let btn = gPanel.document.getElementById("profiler-create");
ok(!btn.getAttribute("disabled"), "Create Profile button is not disabled");
btn.click();
}
function onProfileCreated(name, uid) {
is(gPanel.profiles.size, 2, "There are two profiles now");
ok(gPanel.activeProfile.uid !== uid, "New profile is not yet active");
let btn = gPanel.document.getElementById("profile-" + uid);
ok(btn, "Profile item has been added to the sidebar");
btn.click();
}
function onProfileSwitched(name, uid) {
ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
tearDown(gTab, function onTearDown() {
gPanel = null;
gTab = null;
});
}

View File

@ -0,0 +1,90 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "data:text/html;charset=utf8,<p>JavaScript Profiler test</p>";
let gTab, gPanel, gAttempts = 0;
function test() {
waitForExplicitFinish();
setUp(URL, function onSetUp(tab, browser, panel) {
gTab = tab;
gPanel = panel;
panel.once("started", onStart);
panel.once("stopped", onStop);
panel.once("parsed", onParsed);
testUI();
});
}
function attemptTearDown() {
gAttempts += 1;
if (gAttempts < 2) {
return;
}
tearDown(gTab, function onTearDown() {
gPanel = null;
gTab = null;
});
}
function getProfileInternals() {
let win = gPanel.activeProfile.iframe.contentWindow;
let doc = win.document;
return [win, doc];
}
function testUI() {
ok(gPanel, "Profiler panel exists");
ok(gPanel.activeProfile, "Active profile exists");
let [win, doc] = getProfileInternals();
let startButton = doc.querySelector(".controlPane #startWrapper button");
let stopButton = doc.querySelector(".controlPane #stopWrapper button");
ok(startButton, "Start button exists");
ok(stopButton, "Stop button exists");
startButton.click();
}
function onStart() {
gPanel.controller.isActive(function (err, isActive) {
ok(isActive, "Profiler is running");
let [win, doc] = getProfileInternals();
let stopButton = doc.querySelector(".controlPane #stopWrapper button");
stopButton.click();
});
}
function onStop() {
gPanel.controller.isActive(function (err, isActive) {
ok(!isActive, "Profiler is idle");
attemptTearDown();
});
}
function onParsed() {
function assertSample() {
let [win,doc] = getProfileInternals();
let sample = doc.getElementsByClassName("samplePercentage");
if (sample.length <= 0) {
return void setTimeout(assertSample, 100);
}
ok(sample.length > 0, "We have some items displayed");
is(sample[0].innerHTML, "100.0%", "First percentage is 100%");
attemptTearDown();
}
assertSample();
}

View File

@ -0,0 +1,68 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let temp = {};
const PROFILER_ENABLED = "devtools.profiler.enabled";
Cu.import("resource:///modules/devtools/Target.jsm", temp);
let TargetFactory = temp.TargetFactory;
Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
let gDevTools = temp.gDevTools;
function loadTab(url, callback) {
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
content.location.assign(url);
let browser = gBrowser.getBrowserForTab(tab);
if (browser.contentDocument.readyState === "complete") {
callback(tab, browser);
return;
}
let onLoad = function onLoad() {
browser.removeEventListener("load", onLoad, true);
callback(tab, browser);
};
browser.addEventListener("load", onLoad, true);
}
function openProfiler(tab, callback) {
let target = TargetFactory.forTab(tab);
gDevTools.showToolbox(target, "jsprofiler").then(callback);
}
function closeProfiler(tab, callback) {
let target = TargetFactory.forTab(tab);
let panel = gDevTools.getToolbox(target).getPanel("jsprofiler");
panel.once("destroyed", callback);
gDevTools.closeToolbox(target);
}
function setUp(url, callback=function(){}) {
Services.prefs.setBoolPref(PROFILER_ENABLED, true);
loadTab(url, function onTabLoad(tab, browser) {
openProfiler(tab, function onProfilerOpen() {
let target = TargetFactory.forTab(tab);
let panel = gDevTools.getToolbox(target).getPanel("jsprofiler");
callback(tab, browser, panel);
});
});
}
function tearDown(tab, callback=function(){}) {
closeProfiler(tab, function onProfilerClose() {
callback();
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
finish();
Services.prefs.setBoolPref(PROFILER_ENABLED, false);
});
}

View File

@ -0,0 +1,16 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# LOCALIZATION NOTE These strings are used inside the Debugger
# which is available from the Web Developer sub-menu -> 'Debugger'.
# The correct localization of this file might be to keep it in
# English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best
# documentation on web development on the web.
# LOCALIZATION NOTE (profiler.label):
# This string is displayed in the title of the tab when the profiler is
# displayed inside the developer tools window and in the Developer Tools Menu.
profiler.label=Profiler

View File

@ -35,6 +35,7 @@
locale/browser/devtools/webConsole.dtd (%chrome/browser/devtools/webConsole.dtd)
locale/browser/devtools/sourceeditor.properties (%chrome/browser/devtools/sourceeditor.properties)
locale/browser/devtools/sourceeditor.dtd (%chrome/browser/devtools/sourceeditor.dtd)
locale/browser/devtools/profiler.properties (%chrome/browser/devtools/profiler.properties)
locale/browser/devtools/layoutview.dtd (%chrome/browser/devtools/layoutview.dtd)
locale/browser/devtools/responsiveUI.properties (%chrome/browser/devtools/responsiveUI.properties)
locale/browser/devtools/toolbox.dtd (%chrome/browser/devtools/toolbox.dtd)