Bug 828046 - Save/load profiles to/from disk; r=robcee

This commit is contained in:
Anton Kovalyov 2013-07-16 16:03:33 -07:00
Родитель 15dfc43e10
Коммит 58d0a3be29
11 изменённых файлов: 318 добавлений и 67 удалений

Просмотреть файл

@ -26,7 +26,9 @@ const { PROFILE_IDLE, PROFILE_COMPLETED, PROFILE_RUNNING } = require("devtools/p
function Cleopatra(panel, opts) {
let doc = panel.document;
let win = panel.window;
let { uid, name, showPlatformData } = opts;
let { uid, name } = opts;
let spd = opts.showPlatformData;
let ext = opts.external;
EventEmitter.decorate(this);
@ -41,7 +43,7 @@ function Cleopatra(panel, opts) {
this.iframe = doc.createElement("iframe");
this.iframe.setAttribute("flex", "1");
this.iframe.setAttribute("id", "profiler-cleo-" + uid);
this.iframe.setAttribute("src", "cleopatra.html?uid=" + uid + "&showPlatformData=" + showPlatformData);
this.iframe.setAttribute("src", "cleopatra.html?uid=" + uid + "&spd=" + spd + "&ext=" + ext);
this.iframe.setAttribute("hidden", "true");
// Append our iframe and subscribe to postMessage events.

Просмотреть файл

@ -4,6 +4,7 @@
var gInstanceUID;
var gParsedQS;
var gHideSourceLinks;
function getParam(key) {
if (gParsedQS)
@ -93,6 +94,7 @@ window.addEventListener("message", onParentMessage);
* in the light mode and creates all the UI we need.
*/
function initUI() {
gHideSourceLinks = getParam("ext") === "true";
gLightMode = true;
gFileList = { profileParsingFinished: function () {} };
@ -106,25 +108,6 @@ function initUI() {
container.appendChild(gMainArea);
document.body.appendChild(container);
var startButton = document.createElement("button");
startButton.innerHTML = gStrings.getStr("profiler.start");
startButton.addEventListener("click", function (event) {
event.target.setAttribute("disabled", true);
notifyParent("start");
}, false);
var stopButton = document.createElement("button");
stopButton.innerHTML = gStrings.getStr("profiler.stop");
stopButton.addEventListener("click", function (event) {
event.target.setAttribute("disabled", true);
notifyParent("stop");
}, false);
var message = document.createElement("div");
message.className = "message";
message.innerHTML = "To start profiling click the button above.";
gMainArea.appendChild(message);
}
/**
@ -224,7 +207,8 @@ function enterFinishedProfileUI() {
}
}
if (getParam("showPlatformData") !== "true")
// Show platform data?
if (getParam("spd") !== "true")
toggleJavascriptOnly();
}

Просмотреть файл

@ -458,7 +458,7 @@ TreeView.prototype = {
'<span class="resourceIcon" data-resource="' + node.library + '"></span> ' +
'<span class="functionName">' + nodeName + '</span>' +
'<span class="libraryName">' + libName + '</span>' +
(nodeName === '(total)' ? '' :
((nodeName === '(total)' || gHideSourceLinks) ? '' :
'<input type="button" value="Focus Callstack" title="Focus Callstack" class="focusCallstackButton" tabindex="-1">');
},
_resolveChildren: function TreeView__resolveChildren(div, childrenCollapsedValue) {

Просмотреть файл

@ -4,14 +4,18 @@
"use strict";
const { Cu } = require("chrome");
const { Cu, Cc, Ci, components } = require("chrome");
const {
PROFILE_IDLE,
PROFILE_RUNNING,
PROFILE_COMPLETED,
SHOW_PLATFORM_DATA
SHOW_PLATFORM_DATA,
L10N_BUNDLE
} = require("devtools/profiler/consts");
const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
var EventEmitter = require("devtools/shared/event-emitter");
var promise = require("sdk/core/promise");
var Cleopatra = require("devtools/profiler/cleopatra");
@ -21,6 +25,11 @@ var ProfilerController = require("devtools/profiler/controller");
Cu.import("resource:///modules/devtools/gDevTools.jsm");
Cu.import("resource://gre/modules/devtools/Console.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
/**
* Profiler panel. It is responsible for creating and managing
@ -80,7 +89,8 @@ ProfilerPanel.prototype = {
let doc = this.document;
return {
get record() doc.querySelector("#profiler-start")
get record() doc.querySelector("#profiler-start"),
get import() doc.querySelector("#profiler-import"),
};
},
@ -104,7 +114,7 @@ ProfilerPanel.prototype = {
this._runningUid = profile ? profile.uid : null;
if (this._runningUid)
btn.setAttribute("checked", true)
btn.setAttribute("checked", true);
else
btn.removeAttribute("checked");
},
@ -152,13 +162,22 @@ ProfilerPanel.prototype = {
let deferred = promise.defer();
this.controller = new ProfilerController(this.target);
this.sidebar = new Sidebar(this.document.querySelector("#profiles-list"));
this.sidebar.widget.addEventListener("select", (ev) => {
if (!ev.detail)
return;
let profile = this.profiles.get(parseInt(ev.detail.value, 10));
this.sidebar.on("save", (_, uid) => {
let profile = this.profiles.get(uid);
if (!profile.data)
return void Cu.reportError("Can't save profile because there's no data.");
this.openFileDialog({ mode: "save", name: profile.name }).then((file) => {
if (file)
this.saveProfile(file, profile.data);
});
});
this.sidebar.on("select", (_, uid) => {
let profile = this.profiles.get(uid);
this.activeProfile = profile;
if (profile.isReady) {
@ -175,18 +194,18 @@ ProfilerPanel.prototype = {
btn.addEventListener("click", () => this.toggleRecording(), false);
btn.removeAttribute("disabled");
let imp = this.controls.import;
imp.addEventListener("click", () => {
this.openFileDialog({ mode: "open" }).then((file) => {
if (file)
this.loadProfile(file);
});
}, false);
imp.removeAttribute("disabled");
// Import queued profiles.
for (let [name, data] of this.controller.profiles) {
let profile = this.createProfile(name);
profile.isStarted = false;
profile.isFinished = true;
profile.data = data.data;
profile.parse(data.data, () => this.emit("parsed"));
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
if (!this.sidebar.selectedItem) {
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
}
this.importProfile(name, data.data);
}
this.isReady = true;
@ -195,15 +214,7 @@ ProfilerPanel.prototype = {
});
this.controller.on("profileEnd", (_, data) => {
let profile = this.createProfile(data.name);
profile.isStarted = false;
profile.isFinished = true;
profile.data = data.data;
profile.parse(data.data, () => this.emit("parsed"));
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
if (!this.sidebar.selectedItem)
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
this.importProfile(data.name, data.data);
if (this.recordingProfile && !data.fromConsole)
this.recordingProfile = null;
@ -227,9 +238,9 @@ ProfilerPanel.prototype = {
* @param string name
* (optional) name of the new profile
*
* @return ProfilerPanel
* @return Profile
*/
createProfile: function (name) {
createProfile: function (name, opts={}) {
if (name && this.getProfileByName(name)) {
return this.getProfileByName(name);
}
@ -239,7 +250,8 @@ ProfilerPanel.prototype = {
let profile = new Cleopatra(this, {
uid: uid,
name: name,
showPlatformData: this.showPlatformData
showPlatformData: this.showPlatformData,
external: opts.external
});
this.profiles.set(uid, profile);
@ -249,6 +261,30 @@ ProfilerPanel.prototype = {
return profile;
},
/**
* Imports profile data
*
* @param string name, new profile name
* @param object data, profile data to import
* @param object opts, (optional) if property 'external' is found
* Cleopatra will hide arrow buttons.
*
* @return Profile
*/
importProfile: function (name, data, opts={}) {
let profile = this.createProfile(name, { external: opts.external });
profile.isStarted = false;
profile.isFinished = true;
profile.data = data;
profile.parse(data, () => this.emit("parsed"));
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
if (!this.sidebar.selectedItem)
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
return profile;
},
/**
* Starts or stops profile recording.
*/
@ -422,7 +458,7 @@ ProfilerPanel.prototype = {
*/
displaySource: function PP_displaySource(data, onOpen=function() {}) {
let win = this.window;
let panelWin, timeout;
let panelWin;
function onSourceShown(event) {
if (event.detail.url !== data.uri) {
@ -455,6 +491,81 @@ ProfilerPanel.prototype = {
}.bind(this));
},
/**
* Opens a normal file dialog.
*
* @params object opts, (optional) property 'mode' can be used to
* specify which dialog to open. Can be either
* 'save' or 'open' (default is 'open').
* @return promise
*/
openFileDialog: function (opts={}) {
let deferred = promise.defer();
let picker = Ci.nsIFilePicker;
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(picker);
let { name, mode } = opts;
let save = mode === "save";
let title = L10N.getStr(save ? "profiler.saveFileAs" : "profiler.openFile");
fp.init(this.window, title, save ? picker.modeSave : picker.modeOpen);
fp.appendFilter("JSON", "*.json");
fp.appendFilters(picker.filterText | picker.filterAll);
if (save)
fp.defaultString = (name || "profile") + ".json";
fp.open((result) => {
deferred.resolve(result === picker.returnCancel ? null : fp.file);
});
return deferred.promise;
},
/**
* Saves profile data to disk
*
* @param File file
* @param object data
*
* @return promise
*/
saveProfile: function (file, data) {
let encoder = new TextEncoder();
let buffer = encoder.encode(JSON.stringify({ profile: data }, null, " "));
let opts = { tmpPath: file.path + ".tmp" };
return OS.File.writeAtomic(file.path, buffer, opts);
},
/**
* Reads profile data from disk
*
* @param File file
* @return promise
*/
loadProfile: function (file) {
let deferred = promise.defer();
let ch = NetUtil.newChannel(file);
ch.contentType = "application/json";
NetUtil.asyncFetch(ch, (input, status) => {
if (!components.isSuccessCode(status)) throw new Error(status);
let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
conv.charset = "UTF-8";
let data = NetUtil.readInputStreamToString(input, input.available());
data = conv.ConvertToUnicode(data);
this.importProfile(file.leafName, JSON.parse(data).profile, { external: true });
deferred.resolve();
});
return deferred.promise;
},
/**
* Cleanup.
*/

Просмотреть файл

@ -20,7 +20,13 @@
<vbox class="profiler-sidebar">
<toolbar class="devtools-toolbar">
<hbox id="profiler-controls">
<toolbarbutton id="profiler-start" class="devtools-toolbarbutton"/>
<toolbarbutton id="profiler-start"
class="devtools-toolbarbutton"
disabled="true"/>
<toolbarbutton id="profiler-import"
class="devtools-toolbarbutton"
disabled="true"
label="&importProfile.label;"/>
</hbox>
</toolbar>

Просмотреть файл

@ -25,25 +25,57 @@ function Sidebar(el) {
this.document = el.ownerDocument;
this.widget = new SideMenuWidget(el);
this.widget.notice = L10N.getStr("profiler.sidebarNotice");
this.widget.addEventListener("select", (ev) => {
if (!ev.detail)
return;
this.emit("select", parseInt(ev.detail.value, 10));
});
}
Sidebar.prototype = Heritage.extend(WidgetMethods, {
/**
* Adds a new item for a profile to the sidebar. Markup
* example:
*
* <vbox id="profile-1" class="profiler-sidebar-item">
* <h3>Profile 1</h3>
* <hbox>
* <span flex="1">Completed</span>
* <a>Save</a>
* </hbox>
* </vbox>
*
*/
addProfile: function (profile) {
let doc = this.document;
let box = doc.createElement("vbox");
let vbox = doc.createElement("vbox");
let hbox = doc.createElement("hbox");
let h3 = doc.createElement("h3");
let span = doc.createElement("span");
let save = doc.createElement("a");
box.id = "profile-" + profile.uid;
box.className = "profiler-sidebar-item";
vbox.id = "profile-" + profile.uid;
vbox.className = "profiler-sidebar-item";
h3.textContent = profile.name;
span.setAttribute("flex", 1);
span.textContent = L10N.getStr("profiler.stateIdle");
box.appendChild(h3);
box.appendChild(span);
save.textContent = L10N.getStr("profiler.save");
save.addEventListener("click", (ev) => {
ev.preventDefault();
this.emit("save", profile.uid);
});
this.push([box, profile.uid], {
hbox.appendChild(span);
hbox.appendChild(save);
vbox.appendChild(h3);
vbox.appendChild(hbox);
this.push([vbox, profile.uid], {
attachment: {
name: profile.name,
state: PROFILE_IDLE
@ -61,16 +93,19 @@ Sidebar.prototype = Heritage.extend(WidgetMethods, {
setProfileState: function (profile, state) {
let item = this.getItemByProfile(profile);
let label = item.target.querySelector(".profiler-sidebar-item > span");
let label = item.target.querySelector(".profiler-sidebar-item > hbox > span");
switch (state) {
case PROFILE_IDLE:
item.target.setAttribute("state", "idle");
label.textContent = L10N.getStr("profiler.stateIdle");
break;
case PROFILE_RUNNING:
item.target.setAttribute("state", "running");
label.textContent = L10N.getStr("profiler.stateRunning");
break;
case PROFILE_COMPLETED:
item.target.setAttribute("state", "completed");
label.textContent = L10N.getStr("profiler.stateCompleted");
break;
default: // Wrong state, do nothing.

Просмотреть файл

@ -23,6 +23,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_profiler_console_api_content.js \
browser_profiler_escape.js \
browser_profiler_gecko_data.js \
browser_profiler_io.js \
head.js \
$(NULL)

Просмотреть файл

@ -0,0 +1,80 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "data:text/html;charset=utf8,<p>browser_profiler_io</p>";
let temp = {};
Cu.import("resource://gre/modules/FileUtils.jsm", temp);
let FileUtils = temp.FileUtils;
let gTab, gPanel;
let gData = {
"libs": "[]", // This property is not important for this test.
"meta": {
"version": 2,
"interval": 1,
"stackwalk": 0,
"jank": 0,
"processType": 0,
"platform": "Macintosh",
"oscpu": "Intel Mac OS X 10.8",
"misc": "rv:25.0",
"abi": "x86_64-gcc3",
"toolkit": "cocoa",
"product": "Firefox"
},
"threads": [
{
"samples": [
{
"name": "(root)",
"frames": [
{
"location": "Startup::XRE_Main",
"line": 3871
},
{
"location": "Events::ProcessGeckoEvents",
"line": 355
},
{
"location": "Events::ProcessGeckoEvents",
"line": 355
}
],
"responsiveness": -0.002963,
"time": 8.120823
}
]
}
]
};
function test() {
waitForExplicitFinish();
setUp(URL, function onSetUp(tab, browser, panel) {
gTab = tab;
gPanel = panel;
let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
gPanel.saveProfile(file, gData).then(() => {dump("\n\nsup\n\n");}, () => {dump("\n\n:(((\n\n")})
.then(gPanel.loadProfile.bind(gPanel, file))
.then(checkData);
});
}
function checkData() {
let profile = gPanel.activeProfile;
let item = gPanel.sidebar.getItemByProfile(profile);
let data = profile.data;
is(item.attachment.state, PROFILE_COMPLETED, "Profile is COMPLETED");
is(gData.meta.oscpu, data.meta.oscpu, "Meta data is correct");
is(gData.threads[0].samples.length, 1, "There's one sample");
is(gData.threads[0].samples[0].name, "(root)", "Sample is correct");
tearDown(gTab, () => { gPanel = null; gTab = null; });
}

Просмотреть файл

@ -20,4 +20,8 @@
<!-- LOCALIZATION NOTE (profilerStop.label): This is the label for the
- button that stops the profiler. -->
<!ENTITY profilerStop.label "Stop">
<!ENTITY profilerStop.label "Stop">
<!-- LOCALIZATION NOTE (profiler.importProfile): This string is displayed
- on a button that opens a dialog to import a saved profile data file. -->
<!ENTITY importProfile.label "Import…">

Просмотреть файл

@ -102,4 +102,17 @@ profiler.stateCompleted=Completed
# This string is displayed in the profiler sidebar when there are no
# existing profiles to show (usually happens when the user opens the
# profiler for the first time).
profiler.sidebarNotice=There are no profiles yet.
profiler.sidebarNotice=There are no profiles yet.
# LOCALIZATION NOTE (profiler.save)
# This string is displayed as a label for a button that opens a Save File
# dialog where user can save generated profiler to a file.
profiler.save=Save
# LOCALIZATION NOTE (profiler.saveFileAs)
# This string as a title for a Save File dialog.
profiler.saveFileAs=Save Profile As
# LOCALIZATION NOTE (profiler.openFile)
# This string as a title for a Open File dialog.
profiler.openFile=Import Profile

Просмотреть файл

@ -28,17 +28,32 @@
padding: 3px 5px;
}
.profiler-sidebar-item, .side-menu-widget-item-contents {
cursor: default;
}
.profiler-sidebar-item > h3 {
font-size: 13px;
display: block;
cursor: pointer;
}
.profiler-sidebar-item > span {
.profiler-sidebar-item > hbox {
margin-top: 2px;
color: rgb(140, 152, 165);
}
.selected .profiler-sidebar-item > span {
.profiler-sidebar-item > hbox > a {
display: none;
text-decoration: underline;
cursor: pointer;
}
[state=completed].selected .profiler-sidebar-item > hbox > a {
display: block;
}
.selected .profiler-sidebar-item > hbox {
color: rgb(128, 195, 228);
}