|
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 var isJSM = typeof require !== "function"; |
|
8 |
|
9 // This code is needed because, for whatever reason, mochitest can't |
|
10 // find any requirejs module so we have to load it old school way. :( |
|
11 |
|
12 if (isJSM) { |
|
13 var Cu = this["Components"].utils; |
|
14 let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils; |
|
15 this["loader"] = { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils) }; |
|
16 this["require"] = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; |
|
17 } else { |
|
18 var { Cu } = require("chrome"); |
|
19 } |
|
20 |
|
21 const { L10N_BUNDLE } = require("devtools/profiler/consts"); |
|
22 |
|
23 var EventEmitter = require("devtools/toolkit/event-emitter"); |
|
24 |
|
25 Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); |
|
26 Cu.import("resource://gre/modules/devtools/Console.jsm"); |
|
27 Cu.import("resource://gre/modules/AddonManager.jsm"); |
|
28 Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); |
|
29 |
|
30 loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE)); |
|
31 |
|
32 loader.lazyGetter(this, "gDevTools", |
|
33 () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools); |
|
34 |
|
35 loader.lazyGetter(this, "DebuggerServer", |
|
36 () => Cu.import("resource:///modules/devtools/dbg-server.jsm", {}).DebuggerServer); |
|
37 |
|
38 /** |
|
39 * Data structure that contains information that has |
|
40 * to be shared between separate ProfilerController |
|
41 * instances. |
|
42 */ |
|
43 const sharedData = { |
|
44 data: new WeakMap(), |
|
45 controllers: new WeakMap(), |
|
46 }; |
|
47 |
|
48 /** |
|
49 * Makes a structure representing an individual profile. |
|
50 */ |
|
51 function makeProfile(name, def={}) { |
|
52 if (def.timeStarted == null) |
|
53 def.timeStarted = null; |
|
54 |
|
55 if (def.timeEnded == null) |
|
56 def.timeEnded = null; |
|
57 |
|
58 return { |
|
59 name: name, |
|
60 timeStarted: def.timeStarted, |
|
61 timeEnded: def.timeEnded, |
|
62 fromConsole: def.fromConsole || false |
|
63 }; |
|
64 } |
|
65 |
|
66 // Three functions below all operate with sharedData |
|
67 // structure defined above. They should be self-explanatory. |
|
68 |
|
69 function addTarget(target) { |
|
70 sharedData.data.set(target, new Map()); |
|
71 } |
|
72 |
|
73 function getProfiles(target) { |
|
74 return sharedData.data.get(target); |
|
75 } |
|
76 |
|
77 /** |
|
78 * Object to control the JavaScript Profiler over the remote |
|
79 * debugging protocol. |
|
80 * |
|
81 * @param Target target |
|
82 * A target object as defined in Target.jsm |
|
83 */ |
|
84 function ProfilerController(target) { |
|
85 if (sharedData.controllers.has(target)) { |
|
86 return sharedData.controllers.get(target); |
|
87 } |
|
88 |
|
89 this.target = target; |
|
90 this.client = target.client; |
|
91 this.isConnected = false; |
|
92 this.consoleProfiles = []; |
|
93 this.reservedNames = {}; |
|
94 |
|
95 addTarget(target); |
|
96 |
|
97 // Chrome debugging targets have already obtained a reference |
|
98 // to the profiler actor. |
|
99 if (target.chrome) { |
|
100 this.isConnected = true; |
|
101 this.actor = target.form.profilerActor; |
|
102 } |
|
103 |
|
104 sharedData.controllers.set(target, this); |
|
105 EventEmitter.decorate(this); |
|
106 }; |
|
107 |
|
108 ProfilerController.prototype = { |
|
109 target: null, |
|
110 client: null, |
|
111 isConnected: null, |
|
112 consoleProfiles: null, |
|
113 reservedNames: null, |
|
114 |
|
115 /** |
|
116 * Return a map of profile results for the current target. |
|
117 * |
|
118 * @return Map |
|
119 */ |
|
120 get profiles() { |
|
121 return getProfiles(this.target); |
|
122 }, |
|
123 |
|
124 /** |
|
125 * Checks whether the profile is currently recording. |
|
126 * |
|
127 * @param object profile |
|
128 * An object made by calling makeProfile function. |
|
129 * @return boolean |
|
130 */ |
|
131 isProfileRecording: function PC_isProfileRecording(profile) { |
|
132 return profile.timeStarted !== null && profile.timeEnded === null; |
|
133 }, |
|
134 |
|
135 getProfileName: function PC_getProfileName() { |
|
136 let num = 1; |
|
137 let name = L10N.getFormatStr("profiler.profileName", [num]); |
|
138 |
|
139 while (this.reservedNames[name]) { |
|
140 num += 1; |
|
141 name = L10N.getFormatStr("profiler.profileName", [num]); |
|
142 } |
|
143 |
|
144 this.reservedNames[name] = true; |
|
145 return name; |
|
146 }, |
|
147 |
|
148 /** |
|
149 * A listener that fires whenever console.profile or console.profileEnd |
|
150 * is called. |
|
151 * |
|
152 * @param string type |
|
153 * Type of a call. Either 'profile' or 'profileEnd'. |
|
154 * @param object data |
|
155 * Event data. |
|
156 */ |
|
157 onConsoleEvent: function (type, data) { |
|
158 let name = data.extra.name; |
|
159 |
|
160 let profileStart = () => { |
|
161 if (name && this.profiles.has(name)) |
|
162 return; |
|
163 |
|
164 // Add profile structure to shared data. |
|
165 let profile = makeProfile(name || this.getProfileName(), { |
|
166 timeStarted: data.extra.currentTime, |
|
167 fromConsole: true |
|
168 }); |
|
169 |
|
170 this.profiles.set(profile.name, profile); |
|
171 this.consoleProfiles.push(profile.name); |
|
172 this.emit("profileStart", profile); |
|
173 }; |
|
174 |
|
175 let profileEnd = () => { |
|
176 if (!name && !this.consoleProfiles.length) |
|
177 return; |
|
178 |
|
179 if (!name) |
|
180 name = this.consoleProfiles.pop(); |
|
181 else |
|
182 this.consoleProfiles.filter((n) => n !== name); |
|
183 |
|
184 if (!this.profiles.has(name)) |
|
185 return; |
|
186 |
|
187 let profile = this.profiles.get(name); |
|
188 if (!this.isProfileRecording(profile)) |
|
189 return; |
|
190 |
|
191 let profileData = data.extra.profile; |
|
192 profileData.threads = profileData.threads.map((thread) => { |
|
193 let samples = thread.samples.filter((sample) => { |
|
194 return sample.time >= profile.timeStarted; |
|
195 }); |
|
196 |
|
197 return { samples: samples }; |
|
198 }); |
|
199 |
|
200 profile.timeEnded = data.extra.currentTime; |
|
201 profile.data = profileData; |
|
202 |
|
203 this.emit("profileEnd", profile); |
|
204 }; |
|
205 |
|
206 if (type === "profile") |
|
207 profileStart(); |
|
208 |
|
209 if (type === "profileEnd") |
|
210 profileEnd(); |
|
211 }, |
|
212 |
|
213 /** |
|
214 * Connects to the client unless we're already connected. |
|
215 * |
|
216 * @param function cb |
|
217 * Function to be called once we're connected. If |
|
218 * the controller is already connected, this function |
|
219 * will be called immediately (synchronously). |
|
220 */ |
|
221 connect: function (cb=function(){}) { |
|
222 if (this.isConnected) { |
|
223 return void cb(); |
|
224 } |
|
225 |
|
226 // Check if we already have a grip to the listTabs response object |
|
227 // and, if we do, use it to get to the profilerActor. Otherwise, |
|
228 // call listTabs. The problem is that if we call listTabs twice |
|
229 // webconsole tests fail (see bug 872826). |
|
230 |
|
231 let register = () => { |
|
232 let data = { events: ["console-api-profiler"] }; |
|
233 |
|
234 // Check if Gecko Profiler Addon [1] is installed and, if it is, |
|
235 // don't register our own console event listeners. Gecko Profiler |
|
236 // Addon takes care of console.profile and console.profileEnd methods |
|
237 // and we don't want to break it. |
|
238 // |
|
239 // [1] - https://github.com/bgirard/Gecko-Profiler-Addon/ |
|
240 |
|
241 AddonManager.getAddonByID("jid0-edalmuivkozlouyij0lpdx548bc@jetpack", (addon) => { |
|
242 if (addon && !addon.userDisabled && !addon.softDisabled) |
|
243 return void cb(); |
|
244 |
|
245 this.request("registerEventNotifications", data, (resp) => { |
|
246 this.client.addListener("eventNotification", (type, resp) => { |
|
247 let toolbox = gDevTools.getToolbox(this.target); |
|
248 if (toolbox == null) |
|
249 return; |
|
250 |
|
251 this.onConsoleEvent(resp.subject.action, resp.data); |
|
252 }); |
|
253 }); |
|
254 |
|
255 cb(); |
|
256 }); |
|
257 }; |
|
258 |
|
259 if (this.target.root) { |
|
260 this.actor = this.target.root.profilerActor; |
|
261 this.isConnected = true; |
|
262 return void register(); |
|
263 } |
|
264 |
|
265 this.client.listTabs((resp) => { |
|
266 this.actor = resp.profilerActor; |
|
267 this.isConnected = true; |
|
268 register(); |
|
269 }); |
|
270 }, |
|
271 |
|
272 /** |
|
273 * Adds actor and type information to data and sends the request over |
|
274 * the remote debugging protocol. |
|
275 * |
|
276 * @param string type |
|
277 * Method to call on the other side |
|
278 * @param object data |
|
279 * Data to send with the request |
|
280 * @param function cb |
|
281 * A callback function |
|
282 */ |
|
283 request: function (type, data, cb) { |
|
284 data.to = this.actor; |
|
285 data.type = type; |
|
286 this.client.request(data, cb); |
|
287 }, |
|
288 |
|
289 /** |
|
290 * Checks whether the profiler is active. |
|
291 * |
|
292 * @param function cb |
|
293 * Function to be called with a response from the |
|
294 * client. It will be called with two arguments: |
|
295 * an error object (may be null) and a boolean |
|
296 * value indicating if the profiler is active or not. |
|
297 */ |
|
298 isActive: function (cb) { |
|
299 this.request("isActive", {}, (resp) => { |
|
300 cb(resp.error, resp.isActive, resp.currentTime); |
|
301 }); |
|
302 }, |
|
303 |
|
304 /** |
|
305 * Creates a new profile and starts the profiler, if needed. |
|
306 * |
|
307 * @param string name |
|
308 * Name of the profile. |
|
309 * @param function cb |
|
310 * Function to be called once the profiler is started |
|
311 * or we get an error. It will be called with a single |
|
312 * argument: an error object (may be null). |
|
313 */ |
|
314 start: function PC_start(name, cb) { |
|
315 if (this.profiles.has(name)) { |
|
316 return; |
|
317 } |
|
318 |
|
319 let profile = makeProfile(name); |
|
320 this.consoleProfiles.push(name); |
|
321 this.profiles.set(name, profile); |
|
322 |
|
323 // If profile is already running, no need to do anything. |
|
324 if (this.isProfileRecording(profile)) { |
|
325 return void cb(); |
|
326 } |
|
327 |
|
328 this.isActive((err, isActive, currentTime) => { |
|
329 if (isActive) { |
|
330 profile.timeStarted = currentTime; |
|
331 return void cb(); |
|
332 } |
|
333 |
|
334 let params = { |
|
335 entries: 1000000, |
|
336 interval: 1, |
|
337 features: ["js"], |
|
338 }; |
|
339 |
|
340 this.request("startProfiler", params, (resp) => { |
|
341 if (resp.error) { |
|
342 return void cb(resp.error); |
|
343 } |
|
344 |
|
345 profile.timeStarted = 0; |
|
346 cb(); |
|
347 }); |
|
348 }); |
|
349 }, |
|
350 |
|
351 /** |
|
352 * Stops the profiler. NOTE, that we don't stop the actual |
|
353 * SPS Profiler here. It will be stopped as soon as all |
|
354 * clients disconnect from the profiler actor. |
|
355 * |
|
356 * @param string name |
|
357 * Name of the profile that needs to be stopped. |
|
358 * @param function cb |
|
359 * Function to be called once the profiler is stopped |
|
360 * or we get an error. It will be called with a single |
|
361 * argument: an error object (may be null). |
|
362 */ |
|
363 stop: function PC_stop(name, cb) { |
|
364 if (!this.profiles.has(name)) { |
|
365 return; |
|
366 } |
|
367 |
|
368 let profile = this.profiles.get(name); |
|
369 if (!this.isProfileRecording(profile)) { |
|
370 return; |
|
371 } |
|
372 |
|
373 this.request("getProfile", {}, (resp) => { |
|
374 if (resp.error) { |
|
375 Cu.reportError("Failed to fetch profile data."); |
|
376 return void cb(resp.error, null); |
|
377 } |
|
378 |
|
379 let data = resp.profile; |
|
380 profile.timeEnded = resp.currentTime; |
|
381 |
|
382 // Filter out all samples that fall out of current |
|
383 // profile's range. |
|
384 |
|
385 data.threads = data.threads.map((thread) => { |
|
386 let samples = thread.samples.filter((sample) => { |
|
387 return sample.time >= profile.timeStarted; |
|
388 }); |
|
389 |
|
390 return { samples: samples }; |
|
391 }); |
|
392 |
|
393 cb(null, data); |
|
394 }); |
|
395 }, |
|
396 |
|
397 /** |
|
398 * Cleanup. |
|
399 */ |
|
400 destroy: function PC_destroy() { |
|
401 this.client = null; |
|
402 this.target = null; |
|
403 this.actor = null; |
|
404 } |
|
405 }; |
|
406 |
|
407 if (isJSM) { |
|
408 var EXPORTED_SYMBOLS = ["ProfilerController"]; |
|
409 } else { |
|
410 module.exports = ProfilerController; |
|
411 } |