Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const { Cu, Cc, Ci, components } = require("chrome");
9 const {
10 PROFILE_IDLE,
11 PROFILE_RUNNING,
12 PROFILE_COMPLETED,
13 SHOW_PLATFORM_DATA,
14 L10N_BUNDLE
15 } = require("devtools/profiler/consts");
17 const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
19 var EventEmitter = require("devtools/toolkit/event-emitter");
20 var Cleopatra = require("devtools/profiler/cleopatra");
21 var Sidebar = require("devtools/profiler/sidebar");
22 var ProfilerController = require("devtools/profiler/controller");
23 var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
25 Cu.import("resource:///modules/devtools/gDevTools.jsm");
26 Cu.import("resource://gre/modules/devtools/Console.jsm");
27 Cu.import("resource://gre/modules/Services.jsm");
28 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
29 Cu.import("resource://gre/modules/osfile.jsm");
30 Cu.import("resource://gre/modules/NetUtil.jsm");
32 loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
34 /**
35 * Profiler panel. It is responsible for creating and managing
36 * different profile instances (see cleopatra.js).
37 *
38 * ProfilerPanel is an event emitter. It can emit the following
39 * events:
40 *
41 * - ready: after the panel is done loading everything,
42 * including the default profile instance.
43 * - started: after the panel successfuly starts our SPS
44 * profiler.
45 * - stopped: after the panel successfuly stops our SPS
46 * profiler and is ready to hand over profiling
47 * data
48 * - parsed: after Cleopatra finishes parsing profiling
49 * data.
50 * - destroyed: after the panel cleans up after itself and
51 * is ready to be destroyed.
52 *
53 * The following events are used mainly by tests to prevent
54 * accidential oranges:
55 *
56 * - profileCreated: after a new profile is created.
57 * - profileSwitched: after user switches to a different
58 * profile.
59 */
60 function ProfilerPanel(frame, toolbox) {
61 this.isReady = false;
62 this.window = frame.window;
63 this.document = frame.document;
64 this.target = toolbox.target;
66 this.profiles = new Map();
67 this._uid = 0;
68 this._msgQueue = {};
70 EventEmitter.decorate(this);
71 }
73 ProfilerPanel.prototype = {
74 isReady: null,
75 window: null,
76 document: null,
77 target: null,
78 controller: null,
79 profiles: null,
80 sidebar: null,
82 _uid: null,
83 _activeUid: null,
84 _runningUid: null,
85 _browserWin: null,
86 _msgQueue: null,
88 get controls() {
89 let doc = this.document;
91 return {
92 get record() doc.querySelector("#profiler-start"),
93 get import() doc.querySelector("#profiler-import"),
94 };
95 },
97 get activeProfile() {
98 return this.profiles.get(this._activeUid);
99 },
101 set activeProfile(profile) {
102 if (this._activeUid === profile.uid)
103 return;
105 if (this.activeProfile)
106 this.activeProfile.hide();
108 this._activeUid = profile.uid;
109 profile.show();
110 },
112 set recordingProfile(profile) {
113 let btn = this.controls.record;
114 this._runningUid = profile ? profile.uid : null;
116 if (this._runningUid)
117 btn.setAttribute("checked", true);
118 else
119 btn.removeAttribute("checked");
120 },
122 get recordingProfile() {
123 return this.profiles.get(this._runningUid);
124 },
126 get browserWindow() {
127 if (this._browserWin) {
128 return this._browserWin;
129 }
131 let win = this.window.top;
132 let type = win.document.documentElement.getAttribute("windowtype");
134 if (type !== "navigator:browser") {
135 win = Services.wm.getMostRecentWindow("navigator:browser");
136 }
138 return this._browserWin = win;
139 },
141 get showPlatformData() {
142 return Services.prefs.getBoolPref(SHOW_PLATFORM_DATA);
143 },
145 set showPlatformData(enabled) {
146 Services.prefs.setBoolPref(SHOW_PLATFORM_DATA, enabled);
147 },
149 /**
150 * Open a debug connection and, on success, switch to the newly created
151 * profile.
152 *
153 * @return Promise
154 */
155 open: function PP_open() {
156 // Local profiling needs to make the target remote.
157 let target = this.target;
158 let targetPromise = !target.isRemote ? target.makeRemote() : promise.resolve(target);
160 return targetPromise
161 .then((target) => {
162 let deferred = promise.defer();
164 this.controller = new ProfilerController(this.target);
165 this.sidebar = new Sidebar(this.document.querySelector("#profiles-list"));
167 this.sidebar.on("save", (_, uid) => {
168 let profile = this.profiles.get(uid);
170 if (!profile.data)
171 return void Cu.reportError("Can't save profile because there's no data.");
173 this.openFileDialog({ mode: "save", name: profile.name }).then((file) => {
174 if (file)
175 this.saveProfile(file, profile.data);
176 });
177 });
179 this.sidebar.on("select", (_, uid) => {
180 let profile = this.profiles.get(uid);
181 this.activeProfile = profile;
183 if (profile.isReady) {
184 return void this.emit("profileSwitched", profile.uid);
185 }
187 profile.once("ready", () => {
188 this.emit("profileSwitched", profile.uid);
189 });
190 });
192 this.controller.connect(() => {
193 let btn = this.controls.record;
194 btn.addEventListener("click", () => this.toggleRecording(), false);
195 btn.removeAttribute("disabled");
197 let imp = this.controls.import;
198 imp.addEventListener("click", () => {
199 this.openFileDialog({ mode: "open" }).then((file) => {
200 if (file)
201 this.loadProfile(file);
202 });
203 }, false);
204 imp.removeAttribute("disabled");
206 // Import queued profiles.
207 for (let [name, data] of this.controller.profiles) {
208 this.importProfile(name, data.data);
209 }
211 this.isReady = true;
212 this.emit("ready");
213 deferred.resolve(this);
214 });
216 this.controller.on("profileEnd", (_, data) => {
217 this.importProfile(data.name, data.data);
219 if (this.recordingProfile && !data.fromConsole)
220 this.recordingProfile = null;
222 this.emit("stopped");
223 });
225 return deferred.promise;
226 })
227 .then(null, (reason) =>
228 Cu.reportError("ProfilePanel open failed: " + reason.message));
229 },
231 /**
232 * Creates a new profile instance (see cleopatra.js) and
233 * adds an appropriate item to the sidebar. Note that
234 * this method doesn't automatically switch user to
235 * the newly created profile, they have do to switch
236 * explicitly.
237 *
238 * @param string name
239 * (optional) name of the new profile
240 *
241 * @return Profile
242 */
243 createProfile: function (name, opts={}) {
244 if (name && this.getProfileByName(name)) {
245 return this.getProfileByName(name);
246 }
248 let uid = ++this._uid;
249 let name = name || this.controller.getProfileName();
250 let profile = new Cleopatra(this, {
251 uid: uid,
252 name: name,
253 showPlatformData: this.showPlatformData,
254 external: opts.external
255 });
257 this.profiles.set(uid, profile);
258 this.sidebar.addProfile(profile);
259 this.emit("profileCreated", uid);
261 return profile;
262 },
264 /**
265 * Imports profile data
266 *
267 * @param string name, new profile name
268 * @param object data, profile data to import
269 * @param object opts, (optional) if property 'external' is found
270 * Cleopatra will hide arrow buttons.
271 *
272 * @return Profile
273 */
274 importProfile: function (name, data, opts={}) {
275 let profile = this.createProfile(name, { external: opts.external });
276 profile.isStarted = false;
277 profile.isFinished = true;
278 profile.data = data;
279 profile.parse(data, () => this.emit("parsed"));
281 this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
282 if (!this.sidebar.selectedItem)
283 this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
285 return profile;
286 },
288 /**
289 * Starts or stops profile recording.
290 */
291 toggleRecording: function () {
292 let profile = this.recordingProfile;
294 if (!profile) {
295 profile = this.createProfile();
297 this.startProfiling(profile.name, () => {
298 profile.isStarted = true;
300 this.sidebar.setProfileState(profile, PROFILE_RUNNING);
301 this.recordingProfile = profile;
302 this.emit("started");
303 });
305 return;
306 }
308 this.stopProfiling(profile.name, (data) => {
309 profile.isStarted = false;
310 profile.isFinished = true;
311 profile.data = data;
312 profile.parse(data, () => this.emit("parsed"));
314 this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
315 this.activeProfile = profile;
316 this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
317 this.recordingProfile = null;
318 this.emit("stopped");
319 });
320 },
322 /**
323 * Start collecting profile data.
324 *
325 * @param function onStart
326 * A function to call once we get the message
327 * that profiling had been successfuly started.
328 */
329 startProfiling: function (name, onStart) {
330 this.controller.start(name, (err) => {
331 if (err) {
332 return void Cu.reportError("ProfilerController.start: " + err.message);
333 }
335 onStart();
336 this.emit("started");
337 });
338 },
340 /**
341 * Stop collecting profile data.
342 *
343 * @param function onStop
344 * A function to call once we get the message
345 * that profiling had been successfuly stopped.
346 */
347 stopProfiling: function (name, onStop) {
348 this.controller.isActive((err, isActive) => {
349 if (err) {
350 Cu.reportError("ProfilerController.isActive: " + err.message);
351 return;
352 }
354 if (!isActive) {
355 return;
356 }
358 this.controller.stop(name, (err, data) => {
359 if (err) {
360 Cu.reportError("ProfilerController.stop: " + err.message);
361 return;
362 }
364 onStop(data);
365 this.emit("stopped", data);
366 });
367 });
368 },
370 /**
371 * Lookup an individual profile by its name.
372 *
373 * @param string name name of the profile
374 * @return profile object or null
375 */
376 getProfileByName: function PP_getProfileByName(name) {
377 if (!this.profiles) {
378 return null;
379 }
381 for (let [ uid, profile ] of this.profiles) {
382 if (profile.name === name) {
383 return profile;
384 }
385 }
387 return null;
388 },
390 /**
391 * Lookup an individual profile by its UID.
392 *
393 * @param number uid UID of the profile
394 * @return profile object or null
395 */
396 getProfileByUID: function PP_getProfileByUID(uid) {
397 if (!this.profiles) {
398 return null;
399 }
401 return this.profiles.get(uid) || null;
402 },
404 /**
405 * Iterates over each available profile and calls
406 * a callback with it as a parameter.
407 *
408 * @param function cb a callback to call
409 */
410 eachProfile: function PP_eachProfile(cb) {
411 let uid = this._uid;
413 if (!this.profiles) {
414 return;
415 }
417 while (uid >= 0) {
418 if (this.profiles.has(uid)) {
419 cb(this.profiles.get(uid));
420 }
422 uid -= 1;
423 }
424 },
426 /**
427 * Broadcast messages to all Cleopatra instances.
428 *
429 * @param number target
430 * UID of the recepient profile. All profiles will receive the message
431 * but the profile specified by 'target' will have a special property,
432 * isCurrent, set to true.
433 * @param object data
434 * An object with a property 'task' that will be sent over to Cleopatra.
435 */
436 broadcast: function PP_broadcast(target, data) {
437 if (!this.profiles) {
438 return;
439 }
441 this.eachProfile((profile) => {
442 profile.message({
443 uid: target,
444 isCurrent: target === profile.uid,
445 task: data.task
446 });
447 });
448 },
450 /**
451 * Open file specified in data in either a debugger or view-source.
452 *
453 * @param object data
454 * An object describing the file. It must have three properties:
455 * - uri
456 * - line
457 * - isChrome (chrome files are opened via view-source)
458 */
459 displaySource: function PP_displaySource(data) {
460 let { browserWindow: win, document: doc } = this;
461 let { uri, line, isChrome } = data;
462 let deferred = promise.defer();
464 if (isChrome) {
465 return void win.gViewSourceUtils.viewSource(uri, null, doc, line);
466 }
468 let showSource = ({ DebuggerView }) => {
469 if (DebuggerView.Sources.containsValue(uri)) {
470 DebuggerView.setEditorLocation(uri, line).then(deferred.resolve);
471 }
472 // XXX: What to do if the source isn't present in the Debugger?
473 // Switch back to the Profiler panel and viewSource()?
474 }
476 // If the Debugger was already open, switch to it and try to show the
477 // source immediately. Otherwise, initialize it and wait for the sources
478 // to be added first.
479 let toolbox = gDevTools.getToolbox(this.target);
480 let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
481 toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
482 if (debuggerAlreadyOpen) {
483 showSource(dbg);
484 } else {
485 dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
486 }
487 });
489 return deferred.promise;
490 },
492 /**
493 * Opens a normal file dialog.
494 *
495 * @params object opts, (optional) property 'mode' can be used to
496 * specify which dialog to open. Can be either
497 * 'save' or 'open' (default is 'open').
498 * @return promise
499 */
500 openFileDialog: function (opts={}) {
501 let deferred = promise.defer();
503 let picker = Ci.nsIFilePicker;
504 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(picker);
505 let { name, mode } = opts;
506 let save = mode === "save";
507 let title = L10N.getStr(save ? "profiler.saveFileAs" : "profiler.openFile");
509 fp.init(this.window, title, save ? picker.modeSave : picker.modeOpen);
510 fp.appendFilter("JSON", "*.json");
511 fp.appendFilters(picker.filterText | picker.filterAll);
513 if (save)
514 fp.defaultString = (name || "profile") + ".json";
516 fp.open((result) => {
517 deferred.resolve(result === picker.returnCancel ? null : fp.file);
518 });
520 return deferred.promise;
521 },
523 /**
524 * Saves profile data to disk
525 *
526 * @param File file
527 * @param object data
528 *
529 * @return promise
530 */
531 saveProfile: function (file, data) {
532 let encoder = new TextEncoder();
533 let buffer = encoder.encode(JSON.stringify({ profile: data }, null, " "));
534 let opts = { tmpPath: file.path + ".tmp" };
536 return OS.File.writeAtomic(file.path, buffer, opts);
537 },
539 /**
540 * Reads profile data from disk
541 *
542 * @param File file
543 * @return promise
544 */
545 loadProfile: function (file) {
546 let deferred = promise.defer();
547 let ch = NetUtil.newChannel(file);
548 ch.contentType = "application/json";
550 NetUtil.asyncFetch(ch, (input, status) => {
551 if (!components.isSuccessCode(status)) throw new Error(status);
553 let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
554 .createInstance(Ci.nsIScriptableUnicodeConverter);
555 conv.charset = "UTF-8";
557 let data = NetUtil.readInputStreamToString(input, input.available());
558 data = conv.ConvertToUnicode(data);
559 this.importProfile(file.leafName, JSON.parse(data).profile, { external: true });
561 deferred.resolve();
562 });
564 return deferred.promise;
565 },
567 /**
568 * Cleanup.
569 */
570 destroy: function PP_destroy() {
571 if (this.profiles) {
572 let uid = this._uid;
574 while (uid >= 0) {
575 if (this.profiles.has(uid)) {
576 this.profiles.get(uid).destroy();
577 this.profiles.delete(uid);
578 }
579 uid -= 1;
580 }
581 }
583 if (this.controller) {
584 this.controller.destroy();
585 }
587 this.isReady = null;
588 this.window = null;
589 this.document = null;
590 this.target = null;
591 this.controller = null;
592 this.profiles = null;
593 this._uid = null;
594 this._activeUid = null;
596 this.emit("destroyed");
597 }
598 };
600 module.exports = ProfilerPanel;