b2g/chrome/content/devtools.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial