|
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 DEVELOPER_HUD_LOG_PREFIX = 'DeveloperHUD'; |
|
8 |
|
9 XPCOMUtils.defineLazyGetter(this, 'devtools', function() { |
|
10 const {devtools} = Cu.import('resource://gre/modules/devtools/Loader.jsm', {}); |
|
11 return devtools; |
|
12 }); |
|
13 |
|
14 XPCOMUtils.defineLazyGetter(this, 'DebuggerClient', function() { |
|
15 return Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient; |
|
16 }); |
|
17 |
|
18 XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() { |
|
19 return devtools.require('devtools/toolkit/webconsole/utils').Utils; |
|
20 }); |
|
21 |
|
22 XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() { |
|
23 return devtools.require('devtools/server/actors/eventlooplag').EventLoopLagFront; |
|
24 }); |
|
25 |
|
26 XPCOMUtils.defineLazyGetter(this, 'MemoryFront', function() { |
|
27 return devtools.require('devtools/server/actors/memory').MemoryFront; |
|
28 }); |
|
29 |
|
30 |
|
31 /** |
|
32 * The Developer HUD is an on-device developer tool that displays widgets, |
|
33 * showing visual debug information about apps. Each widget corresponds to a |
|
34 * metric as tracked by a metric watcher (e.g. consoleWatcher). |
|
35 */ |
|
36 let developerHUD = { |
|
37 |
|
38 _targets: new Map(), |
|
39 _frames: new Map(), |
|
40 _client: null, |
|
41 _conn: null, |
|
42 _watchers: [], |
|
43 _logging: true, |
|
44 |
|
45 /** |
|
46 * This method registers a metric watcher that will watch one or more metrics |
|
47 * on app frames that are being tracked. A watcher must implement the |
|
48 * `trackTarget(target)` and `untrackTarget(target)` methods, register |
|
49 * observed metrics with `target.register(metric)`, and keep them up-to-date |
|
50 * with `target.update(metric, message)` when necessary. |
|
51 */ |
|
52 registerWatcher: function dwp_registerWatcher(watcher) { |
|
53 this._watchers.unshift(watcher); |
|
54 }, |
|
55 |
|
56 init: function dwp_init() { |
|
57 if (this._client) |
|
58 return; |
|
59 |
|
60 if (!DebuggerServer.initialized) { |
|
61 RemoteDebugger.start(); |
|
62 } |
|
63 |
|
64 // We instantiate a local debugger connection so that watchers can use our |
|
65 // DebuggerClient to send requests to tab actors (e.g. the consoleActor). |
|
66 // Note the special usage of the private _serverConnection, which we need |
|
67 // to call connectToChild and set up child process actors on a frame we |
|
68 // intend to track. These actors will use the connection to communicate with |
|
69 // our DebuggerServer in the parent process. |
|
70 let transport = DebuggerServer.connectPipe(); |
|
71 this._conn = transport._serverConnection; |
|
72 this._client = new DebuggerClient(transport); |
|
73 |
|
74 for (let w of this._watchers) { |
|
75 if (w.init) { |
|
76 w.init(this._client); |
|
77 } |
|
78 } |
|
79 |
|
80 Services.obs.addObserver(this, 'remote-browser-shown', false); |
|
81 Services.obs.addObserver(this, 'inprocess-browser-shown', false); |
|
82 Services.obs.addObserver(this, 'message-manager-disconnect', false); |
|
83 |
|
84 let systemapp = document.querySelector('#systemapp'); |
|
85 this.trackFrame(systemapp); |
|
86 |
|
87 let frames = systemapp.contentWindow.document.querySelectorAll('iframe[mozapp]'); |
|
88 for (let frame of frames) { |
|
89 this.trackFrame(frame); |
|
90 } |
|
91 |
|
92 SettingsListener.observe('hud.logging', this._logging, enabled => { |
|
93 this._logging = enabled; |
|
94 }); |
|
95 }, |
|
96 |
|
97 uninit: function dwp_uninit() { |
|
98 if (!this._client) |
|
99 return; |
|
100 |
|
101 for (let frame of this._targets.keys()) { |
|
102 this.untrackFrame(frame); |
|
103 } |
|
104 |
|
105 Services.obs.removeObserver(this, 'remote-browser-shown'); |
|
106 Services.obs.removeObserver(this, 'inprocess-browser-shown'); |
|
107 Services.obs.removeObserver(this, 'message-manager-disconnect'); |
|
108 |
|
109 this._client.close(); |
|
110 delete this._client; |
|
111 }, |
|
112 |
|
113 /** |
|
114 * This method will ask all registered watchers to track and update metrics |
|
115 * on an app frame. |
|
116 */ |
|
117 trackFrame: function dwp_trackFrame(frame) { |
|
118 if (this._targets.has(frame)) |
|
119 return; |
|
120 |
|
121 DebuggerServer.connectToChild(this._conn, frame).then(actor => { |
|
122 let target = new Target(frame, actor); |
|
123 this._targets.set(frame, target); |
|
124 |
|
125 for (let w of this._watchers) { |
|
126 w.trackTarget(target); |
|
127 } |
|
128 }); |
|
129 }, |
|
130 |
|
131 untrackFrame: function dwp_untrackFrame(frame) { |
|
132 let target = this._targets.get(frame); |
|
133 if (target) { |
|
134 for (let w of this._watchers) { |
|
135 w.untrackTarget(target); |
|
136 } |
|
137 |
|
138 target.destroy(); |
|
139 this._targets.delete(frame); |
|
140 } |
|
141 }, |
|
142 |
|
143 observe: function dwp_observe(subject, topic, data) { |
|
144 if (!this._client) |
|
145 return; |
|
146 |
|
147 let frame; |
|
148 |
|
149 switch(topic) { |
|
150 |
|
151 // listen for frame creation in OOP (device) as well as in parent process (b2g desktop) |
|
152 case 'remote-browser-shown': |
|
153 case 'inprocess-browser-shown': |
|
154 let frameLoader = subject; |
|
155 // get a ref to the app <iframe> |
|
156 frameLoader.QueryInterface(Ci.nsIFrameLoader); |
|
157 // Ignore notifications that aren't from a BrowserOrApp |
|
158 if (!frameLoader.ownerIsBrowserOrAppFrame) { |
|
159 return; |
|
160 } |
|
161 frame = frameLoader.ownerElement; |
|
162 if (!frame.appManifestURL) // Ignore all frames but app frames |
|
163 return; |
|
164 this.trackFrame(frame); |
|
165 this._frames.set(frameLoader.messageManager, frame); |
|
166 break; |
|
167 |
|
168 // Every time an iframe is destroyed, its message manager also is |
|
169 case 'message-manager-disconnect': |
|
170 let mm = subject; |
|
171 frame = this._frames.get(mm); |
|
172 if (!frame) |
|
173 return; |
|
174 this.untrackFrame(frame); |
|
175 this._frames.delete(mm); |
|
176 break; |
|
177 } |
|
178 }, |
|
179 |
|
180 log: function dwp_log(message) { |
|
181 if (this._logging) { |
|
182 dump(DEVELOPER_HUD_LOG_PREFIX + ': ' + message + '\n'); |
|
183 } |
|
184 } |
|
185 |
|
186 }; |
|
187 |
|
188 |
|
189 /** |
|
190 * A Target object represents all there is to know about a Firefox OS app frame |
|
191 * that is being tracked, e.g. a pointer to the frame, current values of watched |
|
192 * metrics, and how to notify the front-end when metrics have changed. |
|
193 */ |
|
194 function Target(frame, actor) { |
|
195 this.frame = frame; |
|
196 this.actor = actor; |
|
197 this.metrics = new Map(); |
|
198 } |
|
199 |
|
200 Target.prototype = { |
|
201 |
|
202 /** |
|
203 * Register a metric that can later be updated. Does not update the front-end. |
|
204 */ |
|
205 register: function target_register(metric) { |
|
206 this.metrics.set(metric, 0); |
|
207 }, |
|
208 |
|
209 /** |
|
210 * Modify one of a target's metrics, and send out an event to notify relevant |
|
211 * parties (e.g. the developer HUD, automated tests, etc). |
|
212 */ |
|
213 update: function target_update(metric, message) { |
|
214 if (!metric.name) { |
|
215 throw new Error('Missing metric.name'); |
|
216 } |
|
217 |
|
218 if (!metric.value) { |
|
219 metric.value = 0; |
|
220 } |
|
221 |
|
222 let metrics = this.metrics; |
|
223 if (metrics) { |
|
224 metrics.set(metric.name, metric.value); |
|
225 } |
|
226 |
|
227 let data = { |
|
228 metrics: [], // FIXME(Bug 982066) Remove this field. |
|
229 manifest: this.frame.appManifestURL, |
|
230 metric: metric, |
|
231 message: message |
|
232 }; |
|
233 |
|
234 // FIXME(Bug 982066) Remove this loop. |
|
235 if (metrics && metrics.size > 0) { |
|
236 for (let name of metrics.keys()) { |
|
237 data.metrics.push({name: name, value: metrics.get(name)}); |
|
238 } |
|
239 } |
|
240 |
|
241 if (message) { |
|
242 developerHUD.log('[' + data.manifest + '] ' + data.message); |
|
243 } |
|
244 this._send(data); |
|
245 }, |
|
246 |
|
247 /** |
|
248 * Nicer way to call update() when the metric value is a number that needs |
|
249 * to be incremented. |
|
250 */ |
|
251 bump: function target_bump(metric, message) { |
|
252 metric.value = (this.metrics.get(metric.name) || 0) + 1; |
|
253 this.update(metric, message); |
|
254 }, |
|
255 |
|
256 /** |
|
257 * Void a metric value and make sure it isn't displayed on the front-end |
|
258 * anymore. |
|
259 */ |
|
260 clear: function target_clear(metric) { |
|
261 metric.value = 0; |
|
262 this.update(metric); |
|
263 }, |
|
264 |
|
265 /** |
|
266 * Tear everything down, including the front-end by sending a message without |
|
267 * widgets. |
|
268 */ |
|
269 destroy: function target_destroy() { |
|
270 delete this.metrics; |
|
271 this._send({}); |
|
272 }, |
|
273 |
|
274 _send: function target_send(data) { |
|
275 shell.sendEvent(this.frame, 'developer-hud-update', Cu.cloneInto(data, this.frame)); |
|
276 } |
|
277 |
|
278 }; |
|
279 |
|
280 |
|
281 /** |
|
282 * The Console Watcher tracks the following metrics in apps: reflows, warnings, |
|
283 * and errors, with security errors reported separately. |
|
284 */ |
|
285 let consoleWatcher = { |
|
286 |
|
287 _client: null, |
|
288 _targets: new Map(), |
|
289 _watching: { |
|
290 reflows: false, |
|
291 warnings: false, |
|
292 errors: false, |
|
293 security: false |
|
294 }, |
|
295 _security: [ |
|
296 'Mixed Content Blocker', |
|
297 'Mixed Content Message', |
|
298 'CSP', |
|
299 'Invalid HSTS Headers', |
|
300 'Insecure Password Field', |
|
301 'SSL', |
|
302 'CORS' |
|
303 ], |
|
304 |
|
305 init: function cw_init(client) { |
|
306 this._client = client; |
|
307 this.consoleListener = this.consoleListener.bind(this); |
|
308 |
|
309 let watching = this._watching; |
|
310 |
|
311 for (let key in watching) { |
|
312 let metric = key; |
|
313 SettingsListener.observe('hud.' + metric, watching[metric], watch => { |
|
314 // Watch or unwatch the metric. |
|
315 if (watching[metric] = watch) { |
|
316 return; |
|
317 } |
|
318 |
|
319 // If unwatched, remove any existing widgets for that metric. |
|
320 for (let target of this._targets.values()) { |
|
321 target.clear({name: metric}); |
|
322 } |
|
323 }); |
|
324 } |
|
325 |
|
326 client.addListener('logMessage', this.consoleListener); |
|
327 client.addListener('pageError', this.consoleListener); |
|
328 client.addListener('consoleAPICall', this.consoleListener); |
|
329 client.addListener('reflowActivity', this.consoleListener); |
|
330 }, |
|
331 |
|
332 trackTarget: function cw_trackTarget(target) { |
|
333 target.register('reflows'); |
|
334 target.register('warnings'); |
|
335 target.register('errors'); |
|
336 target.register('security'); |
|
337 |
|
338 this._client.request({ |
|
339 to: target.actor.consoleActor, |
|
340 type: 'startListeners', |
|
341 listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity'] |
|
342 }, (res) => { |
|
343 this._targets.set(target.actor.consoleActor, target); |
|
344 }); |
|
345 }, |
|
346 |
|
347 untrackTarget: function cw_untrackTarget(target) { |
|
348 this._client.request({ |
|
349 to: target.actor.consoleActor, |
|
350 type: 'stopListeners', |
|
351 listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity'] |
|
352 }, (res) => { }); |
|
353 |
|
354 this._targets.delete(target.actor.consoleActor); |
|
355 }, |
|
356 |
|
357 consoleListener: function cw_consoleListener(type, packet) { |
|
358 let target = this._targets.get(packet.from); |
|
359 let metric = {}; |
|
360 let output = ''; |
|
361 |
|
362 switch (packet.type) { |
|
363 |
|
364 case 'pageError': |
|
365 let pageError = packet.pageError; |
|
366 |
|
367 if (pageError.warning || pageError.strict) { |
|
368 metric.name = 'warnings'; |
|
369 output += 'warning ('; |
|
370 } else { |
|
371 metric.name = 'errors'; |
|
372 output += 'error ('; |
|
373 } |
|
374 |
|
375 if (this._security.indexOf(pageError.category) > -1) { |
|
376 metric.name = 'security'; |
|
377 } |
|
378 |
|
379 let {errorMessage, sourceName, category, lineNumber, columnNumber} = pageError; |
|
380 output += category + '): "' + (errorMessage.initial || errorMessage) + |
|
381 '" in ' + sourceName + ':' + lineNumber + ':' + columnNumber; |
|
382 break; |
|
383 |
|
384 case 'consoleAPICall': |
|
385 switch (packet.message.level) { |
|
386 |
|
387 case 'error': |
|
388 metric.name = 'errors'; |
|
389 output += 'error (console)'; |
|
390 break; |
|
391 |
|
392 case 'warn': |
|
393 metric.name = 'warnings'; |
|
394 output += 'warning (console)'; |
|
395 break; |
|
396 |
|
397 default: |
|
398 return; |
|
399 } |
|
400 break; |
|
401 |
|
402 case 'reflowActivity': |
|
403 metric.name = 'reflows'; |
|
404 |
|
405 let {start, end, sourceURL, interruptible} = packet; |
|
406 metric.interruptible = interruptible; |
|
407 let duration = Math.round((end - start) * 100) / 100; |
|
408 output += 'reflow: ' + duration + 'ms'; |
|
409 if (sourceURL) { |
|
410 output += ' ' + this.formatSourceURL(packet); |
|
411 } |
|
412 break; |
|
413 |
|
414 default: |
|
415 return; |
|
416 } |
|
417 |
|
418 if (!this._watching[metric.name]) { |
|
419 return; |
|
420 } |
|
421 |
|
422 target.bump(metric, output); |
|
423 }, |
|
424 |
|
425 formatSourceURL: function cw_formatSourceURL(packet) { |
|
426 // Abbreviate source URL |
|
427 let source = WebConsoleUtils.abbreviateSourceURL(packet.sourceURL); |
|
428 |
|
429 // Add function name and line number |
|
430 let {functionName, sourceLine} = packet; |
|
431 source = 'in ' + (functionName || '<anonymousFunction>') + |
|
432 ', ' + source + ':' + sourceLine; |
|
433 |
|
434 return source; |
|
435 } |
|
436 }; |
|
437 developerHUD.registerWatcher(consoleWatcher); |
|
438 |
|
439 |
|
440 let eventLoopLagWatcher = { |
|
441 _client: null, |
|
442 _fronts: new Map(), |
|
443 _active: false, |
|
444 |
|
445 init: function(client) { |
|
446 this._client = client; |
|
447 |
|
448 SettingsListener.observe('hud.jank', false, this.settingsListener.bind(this)); |
|
449 }, |
|
450 |
|
451 settingsListener: function(value) { |
|
452 if (this._active == value) { |
|
453 return; |
|
454 } |
|
455 this._active = value; |
|
456 |
|
457 // Toggle the state of existing fronts. |
|
458 let fronts = this._fronts; |
|
459 for (let target of fronts.keys()) { |
|
460 if (value) { |
|
461 fronts.get(target).start(); |
|
462 } else { |
|
463 fronts.get(target).stop(); |
|
464 target.clear({name: 'jank'}); |
|
465 } |
|
466 } |
|
467 }, |
|
468 |
|
469 trackTarget: function(target) { |
|
470 target.register('jank'); |
|
471 |
|
472 let front = new EventLoopLagFront(this._client, target.actor); |
|
473 this._fronts.set(target, front); |
|
474 |
|
475 front.on('event-loop-lag', time => { |
|
476 target.update({name: 'jank', value: time}, 'jank: ' + time + 'ms'); |
|
477 }); |
|
478 |
|
479 if (this._active) { |
|
480 front.start(); |
|
481 } |
|
482 }, |
|
483 |
|
484 untrackTarget: function(target) { |
|
485 let fronts = this._fronts; |
|
486 if (fronts.has(target)) { |
|
487 fronts.get(target).destroy(); |
|
488 fronts.delete(target); |
|
489 } |
|
490 } |
|
491 }; |
|
492 developerHUD.registerWatcher(eventLoopLagWatcher); |
|
493 |
|
494 |
|
495 /** |
|
496 * The Memory Watcher uses devtools actors to track memory usage. |
|
497 */ |
|
498 let memoryWatcher = { |
|
499 |
|
500 _client: null, |
|
501 _fronts: new Map(), |
|
502 _timers: new Map(), |
|
503 _watching: { |
|
504 jsobjects: false, |
|
505 jsstrings: false, |
|
506 jsother: false, |
|
507 dom: false, |
|
508 style: false, |
|
509 other: false |
|
510 }, |
|
511 _active: false, |
|
512 |
|
513 init: function mw_init(client) { |
|
514 this._client = client; |
|
515 let watching = this._watching; |
|
516 |
|
517 for (let key in watching) { |
|
518 let category = key; |
|
519 SettingsListener.observe('hud.' + category, false, watch => { |
|
520 watching[category] = watch; |
|
521 }); |
|
522 } |
|
523 |
|
524 SettingsListener.observe('hud.appmemory', false, enabled => { |
|
525 if (this._active = enabled) { |
|
526 for (let target of this._fronts.keys()) { |
|
527 this.measure(target); |
|
528 } |
|
529 } else { |
|
530 for (let target of this._fronts.keys()) { |
|
531 clearTimeout(this._timers.get(target)); |
|
532 target.clear({name: 'memory'}); |
|
533 } |
|
534 } |
|
535 }); |
|
536 }, |
|
537 |
|
538 measure: function mw_measure(target) { |
|
539 |
|
540 // TODO Also track USS (bug #976024). |
|
541 |
|
542 let watch = this._watching; |
|
543 let front = this._fronts.get(target); |
|
544 |
|
545 front.measure().then((data) => { |
|
546 |
|
547 let total = 0; |
|
548 if (watch.jsobjects) { |
|
549 total += parseInt(data.jsObjectsSize); |
|
550 } |
|
551 if (watch.jsstrings) { |
|
552 total += parseInt(data.jsStringsSize); |
|
553 } |
|
554 if (watch.jsother) { |
|
555 total += parseInt(data.jsOtherSize); |
|
556 } |
|
557 if (watch.dom) { |
|
558 total += parseInt(data.domSize); |
|
559 } |
|
560 if (watch.style) { |
|
561 total += parseInt(data.styleSize); |
|
562 } |
|
563 if (watch.other) { |
|
564 total += parseInt(data.otherSize); |
|
565 } |
|
566 // TODO Also count images size (bug #976007). |
|
567 |
|
568 target.update({name: 'memory', value: total}); |
|
569 let duration = parseInt(data.jsMilliseconds) + parseInt(data.nonJSMilliseconds); |
|
570 let timer = setTimeout(() => this.measure(target), 100 * duration); |
|
571 this._timers.set(target, timer); |
|
572 }, (err) => { |
|
573 console.error(err); |
|
574 }); |
|
575 }, |
|
576 |
|
577 trackTarget: function mw_trackTarget(target) { |
|
578 target.register('uss'); |
|
579 target.register('memory'); |
|
580 this._fronts.set(target, MemoryFront(this._client, target.actor)); |
|
581 if (this._active) { |
|
582 this.measure(target); |
|
583 } |
|
584 }, |
|
585 |
|
586 untrackTarget: function mw_untrackTarget(target) { |
|
587 let front = this._fronts.get(target); |
|
588 if (front) { |
|
589 front.destroy(); |
|
590 clearTimeout(this._timers.get(target)); |
|
591 this._fronts.delete(target); |
|
592 this._timers.delete(target); |
|
593 } |
|
594 } |
|
595 }; |
|
596 developerHUD.registerWatcher(memoryWatcher); |