1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/b2g/chrome/content/devtools.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,596 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +'use strict'; 1.9 + 1.10 +const DEVELOPER_HUD_LOG_PREFIX = 'DeveloperHUD'; 1.11 + 1.12 +XPCOMUtils.defineLazyGetter(this, 'devtools', function() { 1.13 + const {devtools} = Cu.import('resource://gre/modules/devtools/Loader.jsm', {}); 1.14 + return devtools; 1.15 +}); 1.16 + 1.17 +XPCOMUtils.defineLazyGetter(this, 'DebuggerClient', function() { 1.18 + return Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient; 1.19 +}); 1.20 + 1.21 +XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() { 1.22 + return devtools.require('devtools/toolkit/webconsole/utils').Utils; 1.23 +}); 1.24 + 1.25 +XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() { 1.26 + return devtools.require('devtools/server/actors/eventlooplag').EventLoopLagFront; 1.27 +}); 1.28 + 1.29 +XPCOMUtils.defineLazyGetter(this, 'MemoryFront', function() { 1.30 + return devtools.require('devtools/server/actors/memory').MemoryFront; 1.31 +}); 1.32 + 1.33 + 1.34 +/** 1.35 + * The Developer HUD is an on-device developer tool that displays widgets, 1.36 + * showing visual debug information about apps. Each widget corresponds to a 1.37 + * metric as tracked by a metric watcher (e.g. consoleWatcher). 1.38 + */ 1.39 +let developerHUD = { 1.40 + 1.41 + _targets: new Map(), 1.42 + _frames: new Map(), 1.43 + _client: null, 1.44 + _conn: null, 1.45 + _watchers: [], 1.46 + _logging: true, 1.47 + 1.48 + /** 1.49 + * This method registers a metric watcher that will watch one or more metrics 1.50 + * on app frames that are being tracked. A watcher must implement the 1.51 + * `trackTarget(target)` and `untrackTarget(target)` methods, register 1.52 + * observed metrics with `target.register(metric)`, and keep them up-to-date 1.53 + * with `target.update(metric, message)` when necessary. 1.54 + */ 1.55 + registerWatcher: function dwp_registerWatcher(watcher) { 1.56 + this._watchers.unshift(watcher); 1.57 + }, 1.58 + 1.59 + init: function dwp_init() { 1.60 + if (this._client) 1.61 + return; 1.62 + 1.63 + if (!DebuggerServer.initialized) { 1.64 + RemoteDebugger.start(); 1.65 + } 1.66 + 1.67 + // We instantiate a local debugger connection so that watchers can use our 1.68 + // DebuggerClient to send requests to tab actors (e.g. the consoleActor). 1.69 + // Note the special usage of the private _serverConnection, which we need 1.70 + // to call connectToChild and set up child process actors on a frame we 1.71 + // intend to track. These actors will use the connection to communicate with 1.72 + // our DebuggerServer in the parent process. 1.73 + let transport = DebuggerServer.connectPipe(); 1.74 + this._conn = transport._serverConnection; 1.75 + this._client = new DebuggerClient(transport); 1.76 + 1.77 + for (let w of this._watchers) { 1.78 + if (w.init) { 1.79 + w.init(this._client); 1.80 + } 1.81 + } 1.82 + 1.83 + Services.obs.addObserver(this, 'remote-browser-shown', false); 1.84 + Services.obs.addObserver(this, 'inprocess-browser-shown', false); 1.85 + Services.obs.addObserver(this, 'message-manager-disconnect', false); 1.86 + 1.87 + let systemapp = document.querySelector('#systemapp'); 1.88 + this.trackFrame(systemapp); 1.89 + 1.90 + let frames = systemapp.contentWindow.document.querySelectorAll('iframe[mozapp]'); 1.91 + for (let frame of frames) { 1.92 + this.trackFrame(frame); 1.93 + } 1.94 + 1.95 + SettingsListener.observe('hud.logging', this._logging, enabled => { 1.96 + this._logging = enabled; 1.97 + }); 1.98 + }, 1.99 + 1.100 + uninit: function dwp_uninit() { 1.101 + if (!this._client) 1.102 + return; 1.103 + 1.104 + for (let frame of this._targets.keys()) { 1.105 + this.untrackFrame(frame); 1.106 + } 1.107 + 1.108 + Services.obs.removeObserver(this, 'remote-browser-shown'); 1.109 + Services.obs.removeObserver(this, 'inprocess-browser-shown'); 1.110 + Services.obs.removeObserver(this, 'message-manager-disconnect'); 1.111 + 1.112 + this._client.close(); 1.113 + delete this._client; 1.114 + }, 1.115 + 1.116 + /** 1.117 + * This method will ask all registered watchers to track and update metrics 1.118 + * on an app frame. 1.119 + */ 1.120 + trackFrame: function dwp_trackFrame(frame) { 1.121 + if (this._targets.has(frame)) 1.122 + return; 1.123 + 1.124 + DebuggerServer.connectToChild(this._conn, frame).then(actor => { 1.125 + let target = new Target(frame, actor); 1.126 + this._targets.set(frame, target); 1.127 + 1.128 + for (let w of this._watchers) { 1.129 + w.trackTarget(target); 1.130 + } 1.131 + }); 1.132 + }, 1.133 + 1.134 + untrackFrame: function dwp_untrackFrame(frame) { 1.135 + let target = this._targets.get(frame); 1.136 + if (target) { 1.137 + for (let w of this._watchers) { 1.138 + w.untrackTarget(target); 1.139 + } 1.140 + 1.141 + target.destroy(); 1.142 + this._targets.delete(frame); 1.143 + } 1.144 + }, 1.145 + 1.146 + observe: function dwp_observe(subject, topic, data) { 1.147 + if (!this._client) 1.148 + return; 1.149 + 1.150 + let frame; 1.151 + 1.152 + switch(topic) { 1.153 + 1.154 + // listen for frame creation in OOP (device) as well as in parent process (b2g desktop) 1.155 + case 'remote-browser-shown': 1.156 + case 'inprocess-browser-shown': 1.157 + let frameLoader = subject; 1.158 + // get a ref to the app <iframe> 1.159 + frameLoader.QueryInterface(Ci.nsIFrameLoader); 1.160 + // Ignore notifications that aren't from a BrowserOrApp 1.161 + if (!frameLoader.ownerIsBrowserOrAppFrame) { 1.162 + return; 1.163 + } 1.164 + frame = frameLoader.ownerElement; 1.165 + if (!frame.appManifestURL) // Ignore all frames but app frames 1.166 + return; 1.167 + this.trackFrame(frame); 1.168 + this._frames.set(frameLoader.messageManager, frame); 1.169 + break; 1.170 + 1.171 + // Every time an iframe is destroyed, its message manager also is 1.172 + case 'message-manager-disconnect': 1.173 + let mm = subject; 1.174 + frame = this._frames.get(mm); 1.175 + if (!frame) 1.176 + return; 1.177 + this.untrackFrame(frame); 1.178 + this._frames.delete(mm); 1.179 + break; 1.180 + } 1.181 + }, 1.182 + 1.183 + log: function dwp_log(message) { 1.184 + if (this._logging) { 1.185 + dump(DEVELOPER_HUD_LOG_PREFIX + ': ' + message + '\n'); 1.186 + } 1.187 + } 1.188 + 1.189 +}; 1.190 + 1.191 + 1.192 +/** 1.193 + * A Target object represents all there is to know about a Firefox OS app frame 1.194 + * that is being tracked, e.g. a pointer to the frame, current values of watched 1.195 + * metrics, and how to notify the front-end when metrics have changed. 1.196 + */ 1.197 +function Target(frame, actor) { 1.198 + this.frame = frame; 1.199 + this.actor = actor; 1.200 + this.metrics = new Map(); 1.201 +} 1.202 + 1.203 +Target.prototype = { 1.204 + 1.205 + /** 1.206 + * Register a metric that can later be updated. Does not update the front-end. 1.207 + */ 1.208 + register: function target_register(metric) { 1.209 + this.metrics.set(metric, 0); 1.210 + }, 1.211 + 1.212 + /** 1.213 + * Modify one of a target's metrics, and send out an event to notify relevant 1.214 + * parties (e.g. the developer HUD, automated tests, etc). 1.215 + */ 1.216 + update: function target_update(metric, message) { 1.217 + if (!metric.name) { 1.218 + throw new Error('Missing metric.name'); 1.219 + } 1.220 + 1.221 + if (!metric.value) { 1.222 + metric.value = 0; 1.223 + } 1.224 + 1.225 + let metrics = this.metrics; 1.226 + if (metrics) { 1.227 + metrics.set(metric.name, metric.value); 1.228 + } 1.229 + 1.230 + let data = { 1.231 + metrics: [], // FIXME(Bug 982066) Remove this field. 1.232 + manifest: this.frame.appManifestURL, 1.233 + metric: metric, 1.234 + message: message 1.235 + }; 1.236 + 1.237 + // FIXME(Bug 982066) Remove this loop. 1.238 + if (metrics && metrics.size > 0) { 1.239 + for (let name of metrics.keys()) { 1.240 + data.metrics.push({name: name, value: metrics.get(name)}); 1.241 + } 1.242 + } 1.243 + 1.244 + if (message) { 1.245 + developerHUD.log('[' + data.manifest + '] ' + data.message); 1.246 + } 1.247 + this._send(data); 1.248 + }, 1.249 + 1.250 + /** 1.251 + * Nicer way to call update() when the metric value is a number that needs 1.252 + * to be incremented. 1.253 + */ 1.254 + bump: function target_bump(metric, message) { 1.255 + metric.value = (this.metrics.get(metric.name) || 0) + 1; 1.256 + this.update(metric, message); 1.257 + }, 1.258 + 1.259 + /** 1.260 + * Void a metric value and make sure it isn't displayed on the front-end 1.261 + * anymore. 1.262 + */ 1.263 + clear: function target_clear(metric) { 1.264 + metric.value = 0; 1.265 + this.update(metric); 1.266 + }, 1.267 + 1.268 + /** 1.269 + * Tear everything down, including the front-end by sending a message without 1.270 + * widgets. 1.271 + */ 1.272 + destroy: function target_destroy() { 1.273 + delete this.metrics; 1.274 + this._send({}); 1.275 + }, 1.276 + 1.277 + _send: function target_send(data) { 1.278 + shell.sendEvent(this.frame, 'developer-hud-update', Cu.cloneInto(data, this.frame)); 1.279 + } 1.280 + 1.281 +}; 1.282 + 1.283 + 1.284 +/** 1.285 + * The Console Watcher tracks the following metrics in apps: reflows, warnings, 1.286 + * and errors, with security errors reported separately. 1.287 + */ 1.288 +let consoleWatcher = { 1.289 + 1.290 + _client: null, 1.291 + _targets: new Map(), 1.292 + _watching: { 1.293 + reflows: false, 1.294 + warnings: false, 1.295 + errors: false, 1.296 + security: false 1.297 + }, 1.298 + _security: [ 1.299 + 'Mixed Content Blocker', 1.300 + 'Mixed Content Message', 1.301 + 'CSP', 1.302 + 'Invalid HSTS Headers', 1.303 + 'Insecure Password Field', 1.304 + 'SSL', 1.305 + 'CORS' 1.306 + ], 1.307 + 1.308 + init: function cw_init(client) { 1.309 + this._client = client; 1.310 + this.consoleListener = this.consoleListener.bind(this); 1.311 + 1.312 + let watching = this._watching; 1.313 + 1.314 + for (let key in watching) { 1.315 + let metric = key; 1.316 + SettingsListener.observe('hud.' + metric, watching[metric], watch => { 1.317 + // Watch or unwatch the metric. 1.318 + if (watching[metric] = watch) { 1.319 + return; 1.320 + } 1.321 + 1.322 + // If unwatched, remove any existing widgets for that metric. 1.323 + for (let target of this._targets.values()) { 1.324 + target.clear({name: metric}); 1.325 + } 1.326 + }); 1.327 + } 1.328 + 1.329 + client.addListener('logMessage', this.consoleListener); 1.330 + client.addListener('pageError', this.consoleListener); 1.331 + client.addListener('consoleAPICall', this.consoleListener); 1.332 + client.addListener('reflowActivity', this.consoleListener); 1.333 + }, 1.334 + 1.335 + trackTarget: function cw_trackTarget(target) { 1.336 + target.register('reflows'); 1.337 + target.register('warnings'); 1.338 + target.register('errors'); 1.339 + target.register('security'); 1.340 + 1.341 + this._client.request({ 1.342 + to: target.actor.consoleActor, 1.343 + type: 'startListeners', 1.344 + listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity'] 1.345 + }, (res) => { 1.346 + this._targets.set(target.actor.consoleActor, target); 1.347 + }); 1.348 + }, 1.349 + 1.350 + untrackTarget: function cw_untrackTarget(target) { 1.351 + this._client.request({ 1.352 + to: target.actor.consoleActor, 1.353 + type: 'stopListeners', 1.354 + listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity'] 1.355 + }, (res) => { }); 1.356 + 1.357 + this._targets.delete(target.actor.consoleActor); 1.358 + }, 1.359 + 1.360 + consoleListener: function cw_consoleListener(type, packet) { 1.361 + let target = this._targets.get(packet.from); 1.362 + let metric = {}; 1.363 + let output = ''; 1.364 + 1.365 + switch (packet.type) { 1.366 + 1.367 + case 'pageError': 1.368 + let pageError = packet.pageError; 1.369 + 1.370 + if (pageError.warning || pageError.strict) { 1.371 + metric.name = 'warnings'; 1.372 + output += 'warning ('; 1.373 + } else { 1.374 + metric.name = 'errors'; 1.375 + output += 'error ('; 1.376 + } 1.377 + 1.378 + if (this._security.indexOf(pageError.category) > -1) { 1.379 + metric.name = 'security'; 1.380 + } 1.381 + 1.382 + let {errorMessage, sourceName, category, lineNumber, columnNumber} = pageError; 1.383 + output += category + '): "' + (errorMessage.initial || errorMessage) + 1.384 + '" in ' + sourceName + ':' + lineNumber + ':' + columnNumber; 1.385 + break; 1.386 + 1.387 + case 'consoleAPICall': 1.388 + switch (packet.message.level) { 1.389 + 1.390 + case 'error': 1.391 + metric.name = 'errors'; 1.392 + output += 'error (console)'; 1.393 + break; 1.394 + 1.395 + case 'warn': 1.396 + metric.name = 'warnings'; 1.397 + output += 'warning (console)'; 1.398 + break; 1.399 + 1.400 + default: 1.401 + return; 1.402 + } 1.403 + break; 1.404 + 1.405 + case 'reflowActivity': 1.406 + metric.name = 'reflows'; 1.407 + 1.408 + let {start, end, sourceURL, interruptible} = packet; 1.409 + metric.interruptible = interruptible; 1.410 + let duration = Math.round((end - start) * 100) / 100; 1.411 + output += 'reflow: ' + duration + 'ms'; 1.412 + if (sourceURL) { 1.413 + output += ' ' + this.formatSourceURL(packet); 1.414 + } 1.415 + break; 1.416 + 1.417 + default: 1.418 + return; 1.419 + } 1.420 + 1.421 + if (!this._watching[metric.name]) { 1.422 + return; 1.423 + } 1.424 + 1.425 + target.bump(metric, output); 1.426 + }, 1.427 + 1.428 + formatSourceURL: function cw_formatSourceURL(packet) { 1.429 + // Abbreviate source URL 1.430 + let source = WebConsoleUtils.abbreviateSourceURL(packet.sourceURL); 1.431 + 1.432 + // Add function name and line number 1.433 + let {functionName, sourceLine} = packet; 1.434 + source = 'in ' + (functionName || '<anonymousFunction>') + 1.435 + ', ' + source + ':' + sourceLine; 1.436 + 1.437 + return source; 1.438 + } 1.439 +}; 1.440 +developerHUD.registerWatcher(consoleWatcher); 1.441 + 1.442 + 1.443 +let eventLoopLagWatcher = { 1.444 + _client: null, 1.445 + _fronts: new Map(), 1.446 + _active: false, 1.447 + 1.448 + init: function(client) { 1.449 + this._client = client; 1.450 + 1.451 + SettingsListener.observe('hud.jank', false, this.settingsListener.bind(this)); 1.452 + }, 1.453 + 1.454 + settingsListener: function(value) { 1.455 + if (this._active == value) { 1.456 + return; 1.457 + } 1.458 + this._active = value; 1.459 + 1.460 + // Toggle the state of existing fronts. 1.461 + let fronts = this._fronts; 1.462 + for (let target of fronts.keys()) { 1.463 + if (value) { 1.464 + fronts.get(target).start(); 1.465 + } else { 1.466 + fronts.get(target).stop(); 1.467 + target.clear({name: 'jank'}); 1.468 + } 1.469 + } 1.470 + }, 1.471 + 1.472 + trackTarget: function(target) { 1.473 + target.register('jank'); 1.474 + 1.475 + let front = new EventLoopLagFront(this._client, target.actor); 1.476 + this._fronts.set(target, front); 1.477 + 1.478 + front.on('event-loop-lag', time => { 1.479 + target.update({name: 'jank', value: time}, 'jank: ' + time + 'ms'); 1.480 + }); 1.481 + 1.482 + if (this._active) { 1.483 + front.start(); 1.484 + } 1.485 + }, 1.486 + 1.487 + untrackTarget: function(target) { 1.488 + let fronts = this._fronts; 1.489 + if (fronts.has(target)) { 1.490 + fronts.get(target).destroy(); 1.491 + fronts.delete(target); 1.492 + } 1.493 + } 1.494 +}; 1.495 +developerHUD.registerWatcher(eventLoopLagWatcher); 1.496 + 1.497 + 1.498 +/** 1.499 + * The Memory Watcher uses devtools actors to track memory usage. 1.500 + */ 1.501 +let memoryWatcher = { 1.502 + 1.503 + _client: null, 1.504 + _fronts: new Map(), 1.505 + _timers: new Map(), 1.506 + _watching: { 1.507 + jsobjects: false, 1.508 + jsstrings: false, 1.509 + jsother: false, 1.510 + dom: false, 1.511 + style: false, 1.512 + other: false 1.513 + }, 1.514 + _active: false, 1.515 + 1.516 + init: function mw_init(client) { 1.517 + this._client = client; 1.518 + let watching = this._watching; 1.519 + 1.520 + for (let key in watching) { 1.521 + let category = key; 1.522 + SettingsListener.observe('hud.' + category, false, watch => { 1.523 + watching[category] = watch; 1.524 + }); 1.525 + } 1.526 + 1.527 + SettingsListener.observe('hud.appmemory', false, enabled => { 1.528 + if (this._active = enabled) { 1.529 + for (let target of this._fronts.keys()) { 1.530 + this.measure(target); 1.531 + } 1.532 + } else { 1.533 + for (let target of this._fronts.keys()) { 1.534 + clearTimeout(this._timers.get(target)); 1.535 + target.clear({name: 'memory'}); 1.536 + } 1.537 + } 1.538 + }); 1.539 + }, 1.540 + 1.541 + measure: function mw_measure(target) { 1.542 + 1.543 + // TODO Also track USS (bug #976024). 1.544 + 1.545 + let watch = this._watching; 1.546 + let front = this._fronts.get(target); 1.547 + 1.548 + front.measure().then((data) => { 1.549 + 1.550 + let total = 0; 1.551 + if (watch.jsobjects) { 1.552 + total += parseInt(data.jsObjectsSize); 1.553 + } 1.554 + if (watch.jsstrings) { 1.555 + total += parseInt(data.jsStringsSize); 1.556 + } 1.557 + if (watch.jsother) { 1.558 + total += parseInt(data.jsOtherSize); 1.559 + } 1.560 + if (watch.dom) { 1.561 + total += parseInt(data.domSize); 1.562 + } 1.563 + if (watch.style) { 1.564 + total += parseInt(data.styleSize); 1.565 + } 1.566 + if (watch.other) { 1.567 + total += parseInt(data.otherSize); 1.568 + } 1.569 + // TODO Also count images size (bug #976007). 1.570 + 1.571 + target.update({name: 'memory', value: total}); 1.572 + let duration = parseInt(data.jsMilliseconds) + parseInt(data.nonJSMilliseconds); 1.573 + let timer = setTimeout(() => this.measure(target), 100 * duration); 1.574 + this._timers.set(target, timer); 1.575 + }, (err) => { 1.576 + console.error(err); 1.577 + }); 1.578 + }, 1.579 + 1.580 + trackTarget: function mw_trackTarget(target) { 1.581 + target.register('uss'); 1.582 + target.register('memory'); 1.583 + this._fronts.set(target, MemoryFront(this._client, target.actor)); 1.584 + if (this._active) { 1.585 + this.measure(target); 1.586 + } 1.587 + }, 1.588 + 1.589 + untrackTarget: function mw_untrackTarget(target) { 1.590 + let front = this._fronts.get(target); 1.591 + if (front) { 1.592 + front.destroy(); 1.593 + clearTimeout(this._timers.get(target)); 1.594 + this._fronts.delete(target); 1.595 + this._timers.delete(target); 1.596 + } 1.597 + } 1.598 +}; 1.599 +developerHUD.registerWatcher(memoryWatcher);