b2g/chrome/content/devtools.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:afea21cc9654
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);

mercurial