diff --git a/browser/devtools/profiler/cleopatra.js b/browser/devtools/profiler/cleopatra.js index 42112e9f2d62..4da6335602bf 100644 --- a/browser/devtools/profiler/cleopatra.js +++ b/browser/devtools/profiler/cleopatra.js @@ -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. diff --git a/browser/devtools/profiler/cleopatra/js/devtools.js b/browser/devtools/profiler/cleopatra/js/devtools.js index fc8a001db775..584c6ab85548 100644 --- a/browser/devtools/profiler/cleopatra/js/devtools.js +++ b/browser/devtools/profiler/cleopatra/js/devtools.js @@ -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(); } diff --git a/browser/devtools/profiler/cleopatra/js/tree.js b/browser/devtools/profiler/cleopatra/js/tree.js index 27e79f0d9118..4b4cccad2711 100755 --- a/browser/devtools/profiler/cleopatra/js/tree.js +++ b/browser/devtools/profiler/cleopatra/js/tree.js @@ -458,7 +458,7 @@ TreeView.prototype = { ' ' + '' + nodeName + '' + '' + libName + '' + - (nodeName === '(total)' ? '' : + ((nodeName === '(total)' || gHideSourceLinks) ? '' : ''); }, _resolveChildren: function TreeView__resolveChildren(div, childrenCollapsedValue) { diff --git a/browser/devtools/profiler/panel.js b/browser/devtools/profiler/panel.js index 45f46adf4d6a..84688f261682 100644 --- a/browser/devtools/profiler/panel.js +++ b/browser/devtools/profiler/panel.js @@ -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. */ diff --git a/browser/devtools/profiler/profiler.xul b/browser/devtools/profiler/profiler.xul index 5ee105cb039a..2a9838bd3c29 100644 --- a/browser/devtools/profiler/profiler.xul +++ b/browser/devtools/profiler/profiler.xul @@ -20,7 +20,13 @@ - + + diff --git a/browser/devtools/profiler/sidebar.js b/browser/devtools/profiler/sidebar.js index 462a80daa0cb..358ed462fd70 100644 --- a/browser/devtools/profiler/sidebar.js +++ b/browser/devtools/profiler/sidebar.js @@ -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: + * + * + *

Profile 1

+ * + * Completed + * Save + * + *
+ * + */ 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. diff --git a/browser/devtools/profiler/test/Makefile.in b/browser/devtools/profiler/test/Makefile.in index c3adbbdb0c96..56381c216597 100644 --- a/browser/devtools/profiler/test/Makefile.in +++ b/browser/devtools/profiler/test/Makefile.in @@ -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) diff --git a/browser/devtools/profiler/test/browser_profiler_io.js b/browser/devtools/profiler/test/browser_profiler_io.js new file mode 100644 index 000000000000..d2533986fa04 --- /dev/null +++ b/browser/devtools/profiler/test/browser_profiler_io.js @@ -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,

browser_profiler_io

"; + +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; }); +} \ No newline at end of file diff --git a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd index c78244ac16cd..3f505707b4d0 100644 --- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd @@ -20,4 +20,8 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/browser/locales/en-US/chrome/browser/devtools/profiler.properties b/browser/locales/en-US/chrome/browser/devtools/profiler.properties index b46bd9d0417b..bc89c6e2d172 100644 --- a/browser/locales/en-US/chrome/browser/devtools/profiler.properties +++ b/browser/locales/en-US/chrome/browser/devtools/profiler.properties @@ -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. \ No newline at end of file +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 \ No newline at end of file diff --git a/browser/themes/shared/devtools/profiler.inc.css b/browser/themes/shared/devtools/profiler.inc.css index d66cf1c7d6b3..ed6c994bbc07 100644 --- a/browser/themes/shared/devtools/profiler.inc.css +++ b/browser/themes/shared/devtools/profiler.inc.css @@ -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); }