|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const { Cu, Cc, Ci, components } = require("chrome"); |
|
8 |
|
9 const { |
|
10 PROFILE_IDLE, |
|
11 PROFILE_RUNNING, |
|
12 PROFILE_COMPLETED, |
|
13 SHOW_PLATFORM_DATA, |
|
14 L10N_BUNDLE |
|
15 } = require("devtools/profiler/consts"); |
|
16 |
|
17 const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}); |
|
18 |
|
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", {}); |
|
24 |
|
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"); |
|
31 |
|
32 loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE)); |
|
33 |
|
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; |
|
65 |
|
66 this.profiles = new Map(); |
|
67 this._uid = 0; |
|
68 this._msgQueue = {}; |
|
69 |
|
70 EventEmitter.decorate(this); |
|
71 } |
|
72 |
|
73 ProfilerPanel.prototype = { |
|
74 isReady: null, |
|
75 window: null, |
|
76 document: null, |
|
77 target: null, |
|
78 controller: null, |
|
79 profiles: null, |
|
80 sidebar: null, |
|
81 |
|
82 _uid: null, |
|
83 _activeUid: null, |
|
84 _runningUid: null, |
|
85 _browserWin: null, |
|
86 _msgQueue: null, |
|
87 |
|
88 get controls() { |
|
89 let doc = this.document; |
|
90 |
|
91 return { |
|
92 get record() doc.querySelector("#profiler-start"), |
|
93 get import() doc.querySelector("#profiler-import"), |
|
94 }; |
|
95 }, |
|
96 |
|
97 get activeProfile() { |
|
98 return this.profiles.get(this._activeUid); |
|
99 }, |
|
100 |
|
101 set activeProfile(profile) { |
|
102 if (this._activeUid === profile.uid) |
|
103 return; |
|
104 |
|
105 if (this.activeProfile) |
|
106 this.activeProfile.hide(); |
|
107 |
|
108 this._activeUid = profile.uid; |
|
109 profile.show(); |
|
110 }, |
|
111 |
|
112 set recordingProfile(profile) { |
|
113 let btn = this.controls.record; |
|
114 this._runningUid = profile ? profile.uid : null; |
|
115 |
|
116 if (this._runningUid) |
|
117 btn.setAttribute("checked", true); |
|
118 else |
|
119 btn.removeAttribute("checked"); |
|
120 }, |
|
121 |
|
122 get recordingProfile() { |
|
123 return this.profiles.get(this._runningUid); |
|
124 }, |
|
125 |
|
126 get browserWindow() { |
|
127 if (this._browserWin) { |
|
128 return this._browserWin; |
|
129 } |
|
130 |
|
131 let win = this.window.top; |
|
132 let type = win.document.documentElement.getAttribute("windowtype"); |
|
133 |
|
134 if (type !== "navigator:browser") { |
|
135 win = Services.wm.getMostRecentWindow("navigator:browser"); |
|
136 } |
|
137 |
|
138 return this._browserWin = win; |
|
139 }, |
|
140 |
|
141 get showPlatformData() { |
|
142 return Services.prefs.getBoolPref(SHOW_PLATFORM_DATA); |
|
143 }, |
|
144 |
|
145 set showPlatformData(enabled) { |
|
146 Services.prefs.setBoolPref(SHOW_PLATFORM_DATA, enabled); |
|
147 }, |
|
148 |
|
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); |
|
159 |
|
160 return targetPromise |
|
161 .then((target) => { |
|
162 let deferred = promise.defer(); |
|
163 |
|
164 this.controller = new ProfilerController(this.target); |
|
165 this.sidebar = new Sidebar(this.document.querySelector("#profiles-list")); |
|
166 |
|
167 this.sidebar.on("save", (_, uid) => { |
|
168 let profile = this.profiles.get(uid); |
|
169 |
|
170 if (!profile.data) |
|
171 return void Cu.reportError("Can't save profile because there's no data."); |
|
172 |
|
173 this.openFileDialog({ mode: "save", name: profile.name }).then((file) => { |
|
174 if (file) |
|
175 this.saveProfile(file, profile.data); |
|
176 }); |
|
177 }); |
|
178 |
|
179 this.sidebar.on("select", (_, uid) => { |
|
180 let profile = this.profiles.get(uid); |
|
181 this.activeProfile = profile; |
|
182 |
|
183 if (profile.isReady) { |
|
184 return void this.emit("profileSwitched", profile.uid); |
|
185 } |
|
186 |
|
187 profile.once("ready", () => { |
|
188 this.emit("profileSwitched", profile.uid); |
|
189 }); |
|
190 }); |
|
191 |
|
192 this.controller.connect(() => { |
|
193 let btn = this.controls.record; |
|
194 btn.addEventListener("click", () => this.toggleRecording(), false); |
|
195 btn.removeAttribute("disabled"); |
|
196 |
|
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"); |
|
205 |
|
206 // Import queued profiles. |
|
207 for (let [name, data] of this.controller.profiles) { |
|
208 this.importProfile(name, data.data); |
|
209 } |
|
210 |
|
211 this.isReady = true; |
|
212 this.emit("ready"); |
|
213 deferred.resolve(this); |
|
214 }); |
|
215 |
|
216 this.controller.on("profileEnd", (_, data) => { |
|
217 this.importProfile(data.name, data.data); |
|
218 |
|
219 if (this.recordingProfile && !data.fromConsole) |
|
220 this.recordingProfile = null; |
|
221 |
|
222 this.emit("stopped"); |
|
223 }); |
|
224 |
|
225 return deferred.promise; |
|
226 }) |
|
227 .then(null, (reason) => |
|
228 Cu.reportError("ProfilePanel open failed: " + reason.message)); |
|
229 }, |
|
230 |
|
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 } |
|
247 |
|
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 }); |
|
256 |
|
257 this.profiles.set(uid, profile); |
|
258 this.sidebar.addProfile(profile); |
|
259 this.emit("profileCreated", uid); |
|
260 |
|
261 return profile; |
|
262 }, |
|
263 |
|
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")); |
|
280 |
|
281 this.sidebar.setProfileState(profile, PROFILE_COMPLETED); |
|
282 if (!this.sidebar.selectedItem) |
|
283 this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile); |
|
284 |
|
285 return profile; |
|
286 }, |
|
287 |
|
288 /** |
|
289 * Starts or stops profile recording. |
|
290 */ |
|
291 toggleRecording: function () { |
|
292 let profile = this.recordingProfile; |
|
293 |
|
294 if (!profile) { |
|
295 profile = this.createProfile(); |
|
296 |
|
297 this.startProfiling(profile.name, () => { |
|
298 profile.isStarted = true; |
|
299 |
|
300 this.sidebar.setProfileState(profile, PROFILE_RUNNING); |
|
301 this.recordingProfile = profile; |
|
302 this.emit("started"); |
|
303 }); |
|
304 |
|
305 return; |
|
306 } |
|
307 |
|
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")); |
|
313 |
|
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 }, |
|
321 |
|
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 } |
|
334 |
|
335 onStart(); |
|
336 this.emit("started"); |
|
337 }); |
|
338 }, |
|
339 |
|
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 } |
|
353 |
|
354 if (!isActive) { |
|
355 return; |
|
356 } |
|
357 |
|
358 this.controller.stop(name, (err, data) => { |
|
359 if (err) { |
|
360 Cu.reportError("ProfilerController.stop: " + err.message); |
|
361 return; |
|
362 } |
|
363 |
|
364 onStop(data); |
|
365 this.emit("stopped", data); |
|
366 }); |
|
367 }); |
|
368 }, |
|
369 |
|
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 } |
|
380 |
|
381 for (let [ uid, profile ] of this.profiles) { |
|
382 if (profile.name === name) { |
|
383 return profile; |
|
384 } |
|
385 } |
|
386 |
|
387 return null; |
|
388 }, |
|
389 |
|
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 } |
|
400 |
|
401 return this.profiles.get(uid) || null; |
|
402 }, |
|
403 |
|
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; |
|
412 |
|
413 if (!this.profiles) { |
|
414 return; |
|
415 } |
|
416 |
|
417 while (uid >= 0) { |
|
418 if (this.profiles.has(uid)) { |
|
419 cb(this.profiles.get(uid)); |
|
420 } |
|
421 |
|
422 uid -= 1; |
|
423 } |
|
424 }, |
|
425 |
|
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 } |
|
440 |
|
441 this.eachProfile((profile) => { |
|
442 profile.message({ |
|
443 uid: target, |
|
444 isCurrent: target === profile.uid, |
|
445 task: data.task |
|
446 }); |
|
447 }); |
|
448 }, |
|
449 |
|
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(); |
|
463 |
|
464 if (isChrome) { |
|
465 return void win.gViewSourceUtils.viewSource(uri, null, doc, line); |
|
466 } |
|
467 |
|
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 } |
|
475 |
|
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 }); |
|
488 |
|
489 return deferred.promise; |
|
490 }, |
|
491 |
|
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(); |
|
502 |
|
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"); |
|
508 |
|
509 fp.init(this.window, title, save ? picker.modeSave : picker.modeOpen); |
|
510 fp.appendFilter("JSON", "*.json"); |
|
511 fp.appendFilters(picker.filterText | picker.filterAll); |
|
512 |
|
513 if (save) |
|
514 fp.defaultString = (name || "profile") + ".json"; |
|
515 |
|
516 fp.open((result) => { |
|
517 deferred.resolve(result === picker.returnCancel ? null : fp.file); |
|
518 }); |
|
519 |
|
520 return deferred.promise; |
|
521 }, |
|
522 |
|
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" }; |
|
535 |
|
536 return OS.File.writeAtomic(file.path, buffer, opts); |
|
537 }, |
|
538 |
|
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"; |
|
549 |
|
550 NetUtil.asyncFetch(ch, (input, status) => { |
|
551 if (!components.isSuccessCode(status)) throw new Error(status); |
|
552 |
|
553 let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
554 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
555 conv.charset = "UTF-8"; |
|
556 |
|
557 let data = NetUtil.readInputStreamToString(input, input.available()); |
|
558 data = conv.ConvertToUnicode(data); |
|
559 this.importProfile(file.leafName, JSON.parse(data).profile, { external: true }); |
|
560 |
|
561 deferred.resolve(); |
|
562 }); |
|
563 |
|
564 return deferred.promise; |
|
565 }, |
|
566 |
|
567 /** |
|
568 * Cleanup. |
|
569 */ |
|
570 destroy: function PP_destroy() { |
|
571 if (this.profiles) { |
|
572 let uid = this._uid; |
|
573 |
|
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 } |
|
582 |
|
583 if (this.controller) { |
|
584 this.controller.destroy(); |
|
585 } |
|
586 |
|
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; |
|
595 |
|
596 this.emit("destroyed"); |
|
597 } |
|
598 }; |
|
599 |
|
600 module.exports = ProfilerPanel; |