summaryrefslogtreecommitdiffstats
path: root/b2g/chrome/content/devtools
diff options
context:
space:
mode:
Diffstat (limited to 'b2g/chrome/content/devtools')
-rw-r--r--b2g/chrome/content/devtools/adb.js233
-rw-r--r--b2g/chrome/content/devtools/debugger.js397
-rw-r--r--b2g/chrome/content/devtools/hud.js1017
3 files changed, 1647 insertions, 0 deletions
diff --git a/b2g/chrome/content/devtools/adb.js b/b2g/chrome/content/devtools/adb.js
new file mode 100644
index 000000000..cebc6696b
--- /dev/null
+++ b/b2g/chrome/content/devtools/adb.js
@@ -0,0 +1,233 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This file is only loaded on Gonk to manage ADB state
+
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+
+const DEBUG = false;
+var debug = function(str) {
+ dump("AdbController: " + str + "\n");
+}
+
+var AdbController = {
+ locked: undefined,
+ remoteDebuggerEnabled: undefined,
+ lockEnabled: undefined,
+ disableAdbTimer: null,
+ disableAdbTimeoutHours: 12,
+ umsActive: false,
+
+ setLockscreenEnabled: function(value) {
+ this.lockEnabled = value;
+ DEBUG && debug("setLockscreenEnabled = " + this.lockEnabled);
+ this.updateState();
+ },
+
+ setLockscreenState: function(value) {
+ this.locked = value;
+ DEBUG && debug("setLockscreenState = " + this.locked);
+ this.updateState();
+ },
+
+ setRemoteDebuggerState: function(value) {
+ this.remoteDebuggerEnabled = value;
+ DEBUG && debug("setRemoteDebuggerState = " + this.remoteDebuggerEnabled);
+ this.updateState();
+ },
+
+ startDisableAdbTimer: function() {
+ if (this.disableAdbTimer) {
+ this.disableAdbTimer.cancel();
+ } else {
+ this.disableAdbTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ try {
+ this.disableAdbTimeoutHours =
+ Services.prefs.getIntPref("b2g.adb.timeout-hours");
+ } catch (e) {
+ // This happens if the pref doesn't exist, in which case
+ // disableAdbTimeoutHours will still be set to the default.
+ }
+ }
+ if (this.disableAdbTimeoutHours <= 0) {
+ DEBUG && debug("Timer to disable ADB not started due to zero timeout");
+ return;
+ }
+
+ DEBUG && debug("Starting timer to disable ADB in " +
+ this.disableAdbTimeoutHours + " hours");
+ let timeoutMilliseconds = this.disableAdbTimeoutHours * 60 * 60 * 1000;
+ this.disableAdbTimer.initWithCallback(this, timeoutMilliseconds,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ stopDisableAdbTimer: function() {
+ DEBUG && debug("Stopping timer to disable ADB");
+ if (this.disableAdbTimer) {
+ this.disableAdbTimer.cancel();
+ this.disableAdbTimer = null;
+ }
+ },
+
+ notify: function(aTimer) {
+ if (aTimer == this.disableAdbTimer) {
+ this.disableAdbTimer = null;
+ // The following dump will be the last thing that shows up in logcat,
+ // and will at least give the user a clue about why logcat was
+ // disconnected, if the user happens to be using logcat.
+ debug("ADB timer expired - disabling ADB\n");
+ navigator.mozSettings.createLock().set(
+ {'debugger.remote-mode': 'disabled'});
+ }
+ },
+
+ updateState: function() {
+ this.umsActive = false;
+ },
+
+ updateStateInternal: function() {
+ DEBUG && debug("updateStateInternal: called");
+
+ if (this.remoteDebuggerEnabled === undefined ||
+ this.lockEnabled === undefined ||
+ this.locked === undefined) {
+ // Part of initializing the settings database will cause the observers
+ // to trigger. We want to wait until both have been initialized before
+ // we start changing ther adb state. Without this then we can wind up
+ // toggling adb off and back on again (or on and back off again).
+ //
+ // For completeness, one scenario which toggles adb is using the unagi.
+ // The unagi has adb enabled by default (prior to b2g starting). If you
+ // have the phone lock disabled and remote debugging enabled, then we'll
+ // receive an unlock event and an rde event. However at the time we
+ // receive the unlock event we haven't yet received the rde event, so
+ // we turn adb off momentarily, which disconnects a logcat that might
+ // be running. Changing the defaults (in AdbController) just moves the
+ // problem to a different phone, which has adb disabled by default and
+ // we wind up turning on adb for a short period when we shouldn't.
+ //
+ // By waiting until both values are properly initialized, we avoid
+ // turning adb on or off accidentally.
+ DEBUG && debug("updateState: Waiting for all vars to be initialized");
+ return;
+ }
+
+ // Check if we have a remote debugging session going on. If so, we won't
+ // disable adb even if the screen is locked.
+ let isDebugging = USBRemoteDebugger.isDebugging;
+ DEBUG && debug("isDebugging=" + isDebugging);
+
+ // If USB Mass Storage, USB tethering, or a debug session is active,
+ // then we don't want to disable adb in an automatic fashion (i.e.
+ // when the screen locks or due to timeout).
+ let sysUsbConfig = libcutils.property_get("sys.usb.config").split(",");
+ let usbFuncActive = this.umsActive || isDebugging;
+ usbFuncActive |= (sysUsbConfig.indexOf("rndis") >= 0);
+ usbFuncActive |= (sysUsbConfig.indexOf("mtp") >= 0);
+
+ let enableAdb = this.remoteDebuggerEnabled &&
+ (!(this.lockEnabled && this.locked) || usbFuncActive);
+
+ let useDisableAdbTimer = true;
+ try {
+ if (Services.prefs.getBoolPref("marionette.defaultPrefs.enabled")) {
+ // Marionette is enabled. Marionette requires that adb be on (and also
+ // requires that remote debugging be off). The fact that marionette
+ // is enabled also implies that we're doing a non-production build, so
+ // we want adb enabled all of the time.
+ enableAdb = true;
+ useDisableAdbTimer = false;
+ }
+ } catch (e) {
+ // This means that the pref doesn't exist. Which is fine. We just leave
+ // enableAdb alone.
+ }
+
+ // Check wakelock to prevent adb from disconnecting when phone is locked
+ let lockFile = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);
+ lockFile.initWithPath('/sys/power/wake_lock');
+ if(lockFile.exists()) {
+ let foStream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ let coStream = Cc["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Ci.nsIConverterInputStream);
+ let str = {};
+ foStream.init(lockFile, FileUtils.MODE_RDONLY, 0, 0);
+ coStream.init(foStream, "UTF-8", 0, 0);
+ coStream.readString(-1, str);
+ coStream.close();
+ foStream.close();
+ let wakeLockContents = str.value.replace(/\n/, "");
+ let wakeLockList = wakeLockContents.split(" ");
+ if (wakeLockList.indexOf("adb") >= 0) {
+ enableAdb = true;
+ useDisableAdbTimer = false;
+ DEBUG && debug("Keeping ADB enabled as ADB wakelock is present.");
+ } else {
+ DEBUG && debug("ADB wakelock not found.");
+ }
+ } else {
+ DEBUG && debug("Wake_lock file not found.");
+ }
+
+ DEBUG && debug("updateState: enableAdb = " + enableAdb +
+ " remoteDebuggerEnabled = " + this.remoteDebuggerEnabled +
+ " lockEnabled = " + this.lockEnabled +
+ " locked = " + this.locked +
+ " usbFuncActive = " + usbFuncActive);
+
+ // Configure adb.
+ let currentConfig = libcutils.property_get("persist.sys.usb.config");
+ let configFuncs = currentConfig.split(",");
+ if (currentConfig == "" || currentConfig == "none") {
+ // We want to treat none like the empty string.
+ // "".split(",") yields [""] and not []
+ configFuncs = [];
+ }
+ let adbIndex = configFuncs.indexOf("adb");
+
+ if (enableAdb) {
+ // Add adb to the list of functions, if not already present
+ if (adbIndex < 0) {
+ configFuncs.push("adb");
+ }
+ } else {
+ // Remove adb from the list of functions, if present
+ if (adbIndex >= 0) {
+ configFuncs.splice(adbIndex, 1);
+ }
+ }
+ let newConfig = configFuncs.join(",");
+ if (newConfig == "") {
+ // Convert the empty string back into none, since that's what init.rc
+ // needs.
+ newConfig = "none";
+ }
+ if (newConfig != currentConfig) {
+ DEBUG && debug("updateState: currentConfig = " + currentConfig);
+ DEBUG && debug("updateState: newConfig = " + newConfig);
+ try {
+ libcutils.property_set("persist.sys.usb.config", newConfig);
+ } catch(e) {
+ Cu.reportError("Error configuring adb: " + e);
+ }
+ }
+ if (useDisableAdbTimer) {
+ if (enableAdb && !usbFuncActive) {
+ this.startDisableAdbTimer();
+ } else {
+ this.stopDisableAdbTimer();
+ }
+ }
+ }
+};
+
+SettingsListener.observe("lockscreen.locked", false,
+ AdbController.setLockscreenState.bind(AdbController));
+SettingsListener.observe("lockscreen.enabled", false,
+ AdbController.setLockscreenEnabled.bind(AdbController));
diff --git a/b2g/chrome/content/devtools/debugger.js b/b2g/chrome/content/devtools/debugger.js
new file mode 100644
index 000000000..11987a839
--- /dev/null
+++ b/b2g/chrome/content/devtools/debugger.js
@@ -0,0 +1,397 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "devtools", function() {
+ const { devtools } =
+ Cu.import("resource://devtools/shared/Loader.jsm", {});
+ return devtools;
+});
+
+XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function() {
+ const { DebuggerServer } = devtools.require("devtools/server/main");
+ return DebuggerServer;
+});
+
+XPCOMUtils.defineLazyGetter(this, "B2GTabList", function() {
+ const { B2GTabList } =
+ devtools.require("resource://gre/modules/DebuggerActors.js");
+ return B2GTabList;
+});
+
+// Load the discovery module eagerly, so that it can set a device name at
+// startup. This does not cause discovery to start listening for packets, as
+// that only happens once DevTools is enabled.
+devtools.require("devtools/shared/discovery/discovery");
+
+var RemoteDebugger = {
+ _listening: false,
+
+ /**
+ * Prompt the user to accept or decline the incoming connection.
+ *
+ * @param session object
+ * The session object will contain at least the following fields:
+ * {
+ * authentication,
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * }
+ * }
+ * Specific authentication modes may include additional fields. Check
+ * the different |allowConnection| methods in
+ * devtools/shared/security/auth.js.
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ allowConnection(session) {
+ if (this._promptingForAllow) {
+ // Don't stack connection prompts if one is already open
+ return DebuggerServer.AuthenticationResult.DENY;
+ }
+ this._listen();
+
+ this._promptingForAllow = new Promise(resolve => {
+ this._handleAllowResult = detail => {
+ this._handleAllowResult = null;
+ this._promptingForAllow = null;
+ // Newer Gaia supplies |authResult|, which is one of the
+ // AuthenticationResult values.
+ if (detail.authResult) {
+ resolve(detail.authResult);
+ } else if (detail.value) {
+ resolve(DebuggerServer.AuthenticationResult.ALLOW);
+ } else {
+ resolve(DebuggerServer.AuthenticationResult.DENY);
+ }
+ };
+
+ shell.sendChromeEvent({
+ type: "remote-debugger-prompt",
+ session
+ });
+ });
+
+ return this._promptingForAllow;
+ },
+
+ /**
+ * During OOB_CERT authentication, the user must transfer some data through some
+ * out of band mechanism from the client to the server to authenticate the
+ * devices.
+ *
+ * This implementation instructs Gaia to continually capture images which are
+ * passed back here and run through a QR decoder.
+ *
+ * @return An object containing:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * A promise that will be resolved to the above is also allowed.
+ */
+ receiveOOB() {
+ if (this._receivingOOB) {
+ return this._receivingOOB;
+ }
+ this._listen();
+
+ const QR = devtools.require("devtools/shared/qrcode/index");
+ this._receivingOOB = new Promise((resolve, reject) => {
+ this._handleAuthEvent = detail => {
+ debug(detail.action);
+ if (detail.action === "abort") {
+ this._handleAuthEvent = null;
+ this._receivingOOB = null;
+ reject();
+ return;
+ }
+
+ if (detail.action !== "capture") {
+ return;
+ }
+
+ let url = detail.url;
+ QR.decodeFromURI(url).then(data => {
+ debug("Got auth data: " + data);
+ let oob = JSON.parse(data);
+
+ shell.sendChromeEvent({
+ type: "devtools-auth",
+ action: "stop"
+ });
+
+ this._handleAuthEvent = null;
+ this._receivingOOB = null;
+ resolve(oob);
+ }).catch(() => {
+ debug("No auth data, requesting new capture");
+ shell.sendChromeEvent({
+ type: "devtools-auth",
+ action: "capture"
+ });
+ });
+ };
+
+ // Show QR scanning dialog, get an initial capture
+ shell.sendChromeEvent({
+ type: "devtools-auth",
+ action: "start"
+ });
+ });
+
+ return this._receivingOOB;
+ },
+
+ _listen: function() {
+ if (this._listening) {
+ return;
+ }
+
+ this.handleEvent = this.handleEvent.bind(this);
+ let content = shell.contentBrowser.contentWindow;
+ content.addEventListener("mozContentEvent", this, false, true);
+ this._listening = true;
+ },
+
+ handleEvent: function(event) {
+ let detail = event.detail;
+ if (detail.type === "remote-debugger-prompt" && this._handleAllowResult) {
+ this._handleAllowResult(detail);
+ }
+ if (detail.type === "devtools-auth" && this._handleAuthEvent) {
+ this._handleAuthEvent(detail);
+ }
+ },
+
+ initServer: function() {
+ if (DebuggerServer.initialized) {
+ return;
+ }
+
+ // Ask for remote connections.
+ DebuggerServer.init();
+
+ // /!\ Be careful when adding a new actor, especially global actors.
+ // Any new global actor will be exposed and returned by the root actor.
+
+ // Add Firefox-specific actors, but prevent tab actors to be loaded in
+ // the parent process, unless we enable certified apps debugging.
+ let restrictPrivileges = Services.prefs.getBoolPref("devtools.debugger.forbid-certified-apps");
+ DebuggerServer.addBrowserActors("navigator:browser", restrictPrivileges);
+
+ // Allow debugging of chrome for any process
+ if (!restrictPrivileges) {
+ DebuggerServer.allowChromeProcess = true;
+ }
+
+ /**
+ * Construct a root actor appropriate for use in a server running in B2G.
+ * The returned root actor respects the factories registered with
+ * DebuggerServer.addGlobalActor only if certified apps debugging is on,
+ * otherwise we used an explicit limited list of global actors
+ *
+ * * @param connection DebuggerServerConnection
+ * The conection to the client.
+ */
+ DebuggerServer.createRootActor = function createRootActor(connection)
+ {
+ let parameters = {
+ tabList: new B2GTabList(connection),
+ // Use an explicit global actor list to prevent exposing
+ // unexpected actors
+ globalActorFactories: restrictPrivileges ? {
+ webappsActor: DebuggerServer.globalActorFactories.webappsActor,
+ deviceActor: DebuggerServer.globalActorFactories.deviceActor,
+ settingsActor: DebuggerServer.globalActorFactories.settingsActor
+ } : DebuggerServer.globalActorFactories
+ };
+ let { RootActor } = devtools.require("devtools/server/actors/root");
+ let root = new RootActor(connection, parameters);
+ root.applicationType = "operating-system";
+ return root;
+ };
+
+ if (isGonk) {
+ DebuggerServer.on("connectionchange", function() {
+ AdbController.updateState();
+ });
+ }
+ }
+};
+
+RemoteDebugger.allowConnection =
+ RemoteDebugger.allowConnection.bind(RemoteDebugger);
+RemoteDebugger.receiveOOB =
+ RemoteDebugger.receiveOOB.bind(RemoteDebugger);
+
+var USBRemoteDebugger = {
+
+ get isDebugging() {
+ if (!this._listener) {
+ return false;
+ }
+
+ return DebuggerServer._connections &&
+ Object.keys(DebuggerServer._connections).length > 0;
+ },
+
+ start: function() {
+ if (this._listener) {
+ return;
+ }
+
+ RemoteDebugger.initServer();
+
+ let portOrPath =
+ Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
+ "/data/local/debugger-socket";
+
+ try {
+ debug("Starting USB debugger on " + portOrPath);
+ let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+ let authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = RemoteDebugger.allowConnection;
+ this._listener = DebuggerServer.createListener();
+ this._listener.portOrPath = portOrPath;
+ this._listener.authenticator = authenticator;
+ this._listener.open();
+ // Temporary event, until bug 942756 lands and offers a way to know
+ // when the server is up and running.
+ Services.obs.notifyObservers(null, "debugger-server-started", null);
+ } catch (e) {
+ debug("Unable to start USB debugger server: " + e);
+ }
+ },
+
+ stop: function() {
+ if (!this._listener) {
+ return;
+ }
+
+ try {
+ this._listener.close();
+ this._listener = null;
+ } catch (e) {
+ debug("Unable to stop USB debugger server: " + e);
+ }
+ }
+
+};
+
+var WiFiRemoteDebugger = {
+
+ start: function() {
+ if (this._listener) {
+ return;
+ }
+
+ RemoteDebugger.initServer();
+
+ try {
+ debug("Starting WiFi debugger");
+ let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
+ let authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = RemoteDebugger.allowConnection;
+ authenticator.receiveOOB = RemoteDebugger.receiveOOB;
+ this._listener = DebuggerServer.createListener();
+ this._listener.portOrPath = -1 /* any available port */;
+ this._listener.authenticator = authenticator;
+ this._listener.discoverable = true;
+ this._listener.encryption = true;
+ this._listener.open();
+ let port = this._listener.port;
+ debug("Started WiFi debugger on " + port);
+ } catch (e) {
+ debug("Unable to start WiFi debugger server: " + e);
+ }
+ },
+
+ stop: function() {
+ if (!this._listener) {
+ return;
+ }
+
+ try {
+ this._listener.close();
+ this._listener = null;
+ } catch (e) {
+ debug("Unable to stop WiFi debugger server: " + e);
+ }
+ }
+
+};
+
+(function() {
+ // Track these separately here so we can determine the correct value for the
+ // pref "devtools.debugger.remote-enabled", which is true when either mode of
+ // using DevTools is enabled.
+ let devtoolsUSB = false;
+ let devtoolsWiFi = false;
+
+ // Keep the old setting to not break people that won't have updated
+ // gaia and gecko.
+ SettingsListener.observe("devtools.debugger.remote-enabled", false,
+ function(value) {
+ devtoolsUSB = value;
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
+ devtoolsUSB || devtoolsWiFi);
+ // This preference is consulted during startup
+ Services.prefs.savePrefFile(null);
+ try {
+ value ? USBRemoteDebugger.start() : USBRemoteDebugger.stop();
+ } catch(e) {
+ dump("Error while initializing USB devtools: " +
+ e + "\n" + e.stack + "\n");
+ }
+ });
+
+ SettingsListener.observe("debugger.remote-mode", "disabled", function(value) {
+ if (["disabled", "adb-only", "adb-devtools"].indexOf(value) == -1) {
+ dump("Illegal value for debugger.remote-mode: " + value + "\n");
+ return;
+ }
+
+ devtoolsUSB = value == "adb-devtools";
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
+ devtoolsUSB || devtoolsWiFi);
+ // This preference is consulted during startup
+ Services.prefs.savePrefFile(null);
+
+ try {
+ (value == "adb-devtools") ? USBRemoteDebugger.start()
+ : USBRemoteDebugger.stop();
+ } catch(e) {
+ dump("Error while initializing USB devtools: " +
+ e + "\n" + e.stack + "\n");
+ }
+
+ isGonk && AdbController.setRemoteDebuggerState(value != "disabled");
+ });
+
+ SettingsListener.observe("devtools.remote.wifi.enabled", false,
+ function(value) {
+ devtoolsWiFi = value;
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
+ devtoolsUSB || devtoolsWiFi);
+ // Allow remote debugging on non-local interfaces when WiFi debug is enabled
+ // TODO: Bug 1034411: Lock down to WiFi interface, instead of all interfaces
+ Services.prefs.setBoolPref("devtools.debugger.force-local", !value);
+ // This preference is consulted during startup
+ Services.prefs.savePrefFile(null);
+
+ try {
+ value ? WiFiRemoteDebugger.start() : WiFiRemoteDebugger.stop();
+ } catch(e) {
+ dump("Error while initializing WiFi devtools: " +
+ e + "\n" + e.stack + "\n");
+ }
+ });
+})();
diff --git a/b2g/chrome/content/devtools/hud.js b/b2g/chrome/content/devtools/hud.js
new file mode 100644
index 000000000..64e9d553d
--- /dev/null
+++ b/b2g/chrome/content/devtools/hud.js
@@ -0,0 +1,1017 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+// settings.js loads this file when the HUD setting is enabled.
+
+const DEVELOPER_HUD_LOG_PREFIX = 'DeveloperHUD';
+const CUSTOM_HISTOGRAM_PREFIX = 'DEVTOOLS_HUD_CUSTOM_';
+const APPNAME_IDX = 3;
+const HISTNAME_IDX = 4;
+
+XPCOMUtils.defineLazyGetter(this, 'devtools', function() {
+ const {devtools} = Cu.import('resource://devtools/shared/Loader.jsm', {});
+ return devtools;
+});
+
+XPCOMUtils.defineLazyGetter(this, 'DebuggerClient', function() {
+ return devtools.require('devtools/shared/client/main').DebuggerClient;
+});
+
+XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() {
+ return devtools.require('devtools/shared/webconsole/utils').Utils;
+});
+
+XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() {
+ return devtools.require('devtools/shared/fronts/eventlooplag').EventLoopLagFront;
+});
+
+XPCOMUtils.defineLazyGetter(this, 'PerformanceEntriesFront', function() {
+ return devtools.require('devtools/server/actors/performance-entries').PerformanceEntriesFront;
+});
+
+XPCOMUtils.defineLazyGetter(this, 'MemoryFront', function() {
+ return devtools.require('devtools/server/actors/memory').MemoryFront;
+});
+
+Cu.import('resource://gre/modules/Frames.jsm');
+
+var _telemetryDebug = false;
+
+function telemetryDebug(...args) {
+ if (_telemetryDebug) {
+ args.unshift('[AdvancedTelemetry]');
+ console.log(...args);
+ }
+}
+
+/**
+ * The Developer HUD is an on-device developer tool that displays widgets,
+ * showing visual debug information about apps. Each widget corresponds to a
+ * metric as tracked by a metric watcher (e.g. consoleWatcher).
+ */
+var developerHUD = {
+
+ _targets: new Map(),
+ _histograms: new Set(),
+ _customHistograms: new Set(),
+ _client: null,
+ _conn: null,
+ _watchers: [],
+ _logging: true,
+ _telemetry: false,
+
+ /**
+ * This method registers a metric watcher that will watch one or more metrics
+ * on app frames that are being tracked. A watcher must implement the
+ * `trackTarget(target)` and `untrackTarget(target)` methods, register
+ * observed metrics with `target.register(metric)`, and keep them up-to-date
+ * with `target.update(metric, message)` when necessary.
+ */
+ registerWatcher(watcher) {
+ this._watchers.unshift(watcher);
+ },
+
+ init() {
+ if (this._client) {
+ return;
+ }
+
+ if (!DebuggerServer.initialized) {
+ RemoteDebugger.initServer();
+ }
+
+ // We instantiate a local debugger connection so that watchers can use our
+ // DebuggerClient to send requests to tab actors (e.g. the consoleActor).
+ // Note the special usage of the private _serverConnection, which we need
+ // to call connectToChild and set up child process actors on a frame we
+ // intend to track. These actors will use the connection to communicate with
+ // our DebuggerServer in the parent process.
+ let transport = DebuggerServer.connectPipe();
+ this._conn = transport._serverConnection;
+ this._client = new DebuggerClient(transport);
+
+ for (let w of this._watchers) {
+ if (w.init) {
+ w.init(this._client);
+ }
+ }
+
+ Frames.addObserver(this);
+
+ let appFrames = Frames.list().filter(frame => frame.getAttribute('mozapp'));
+ for (let frame of appFrames) {
+ this.trackFrame(frame);
+ }
+
+ SettingsListener.observe('hud.logging', this._logging, enabled => {
+ this._logging = enabled;
+ });
+
+ SettingsListener.observe('hud.telemetry.logging', _telemetryDebug, enabled => {
+ _telemetryDebug = enabled;
+ });
+
+ SettingsListener.observe('metrics.selectedMetrics.level', "", level => {
+ this._telemetry = (level === 'Enhanced');
+ });
+ },
+
+ uninit() {
+ if (!this._client) {
+ return;
+ }
+
+ for (let frame of this._targets.keys()) {
+ this.untrackFrame(frame);
+ }
+
+ Frames.removeObserver(this);
+
+ this._client.close();
+ delete this._client;
+ },
+
+ /**
+ * This method will ask all registered watchers to track and update metrics
+ * on an app frame.
+ */
+ trackFrame(frame) {
+ if (this._targets.has(frame)) {
+ return;
+ }
+
+ DebuggerServer.connectToChild(this._conn, frame).then(actor => {
+ let target = new Target(frame, actor);
+ this._targets.set(frame, target);
+
+ for (let w of this._watchers) {
+ w.trackTarget(target);
+ }
+ });
+ },
+
+ untrackFrame(frame) {
+ let target = this._targets.get(frame);
+ if (target) {
+ for (let w of this._watchers) {
+ w.untrackTarget(target);
+ }
+
+ target.destroy();
+ this._targets.delete(frame);
+ }
+ },
+
+ onFrameCreated(frame, isFirstAppFrame) {
+ let mozapp = frame.getAttribute('mozapp');
+ if (!mozapp) {
+ return;
+ }
+ this.trackFrame(frame);
+ },
+
+ onFrameDestroyed(frame, isLastAppFrame) {
+ let mozapp = frame.getAttribute('mozapp');
+ if (!mozapp) {
+ return;
+ }
+ this.untrackFrame(frame);
+ },
+
+ log(message) {
+ if (this._logging) {
+ dump(DEVELOPER_HUD_LOG_PREFIX + ': ' + message + '\n');
+ }
+ }
+
+};
+
+
+/**
+ * A Target object represents all there is to know about a Firefox OS app frame
+ * that is being tracked, e.g. a pointer to the frame, current values of watched
+ * metrics, and how to notify the front-end when metrics have changed.
+ */
+function Target(frame, actor) {
+ this.frame = frame;
+ this.actor = actor;
+ this.metrics = new Map();
+ this._appName = null;
+}
+
+Target.prototype = {
+
+ get manifest() {
+ return this.frame.appManifestURL;
+ },
+
+ get appName() {
+
+ if (this._appName) {
+ return this._appName;
+ }
+
+ let manifest = this.manifest;
+ if (!manifest) {
+ let msg = DEVELOPER_HUD_LOG_PREFIX + ': Unable to determine app for telemetry metric. src: ' +
+ this.frame.src;
+ console.error(msg);
+ return null;
+ }
+
+ // "communications" apps are a special case
+ if (manifest.indexOf('communications') === -1) {
+ let start = manifest.indexOf('/') + 2;
+ let end = manifest.indexOf('.', start);
+ this._appName = manifest.substring(start, end).toLowerCase();
+ } else {
+ let src = this.frame.src;
+ if (src) {
+ // e.g., `app://communications.gaiamobile.org/contacts/index.html`
+ let parts = src.split('/');
+ let APP = 3;
+ let EXPECTED_PARTS_LENGTH = 5;
+ if (parts.length === EXPECTED_PARTS_LENGTH) {
+ this._appName = parts[APP];
+ }
+ }
+ }
+
+ return this._appName;
+ },
+
+ /**
+ * Register a metric that can later be updated. Does not update the front-end.
+ */
+ register(metric) {
+ this.metrics.set(metric, 0);
+ },
+
+ /**
+ * Modify one of a target's metrics, and send out an event to notify relevant
+ * parties (e.g. the developer HUD, automated tests, etc).
+ */
+ update(metric, message) {
+ if (!metric.name) {
+ throw new Error('Missing metric.name');
+ }
+
+ if (!metric.value) {
+ metric.value = 0;
+ }
+
+ let metrics = this.metrics;
+ if (metrics) {
+ metrics.set(metric.name, metric.value);
+ }
+
+ let data = {
+ metrics: [], // FIXME(Bug 982066) Remove this field.
+ manifest: this.manifest,
+ metric: metric,
+ message: message
+ };
+
+ // FIXME(Bug 982066) Remove this loop.
+ if (metrics && metrics.size > 0) {
+ for (let name of metrics.keys()) {
+ data.metrics.push({name: name, value: metrics.get(name)});
+ }
+ }
+
+ if (message) {
+ developerHUD.log('[' + data.manifest + '] ' + data.message);
+ }
+
+ this._send(data);
+ },
+
+ /**
+ * Nicer way to call update() when the metric value is a number that needs
+ * to be incremented.
+ */
+ bump(metric, message) {
+ metric.value = (this.metrics.get(metric.name) || 0) + 1;
+ this.update(metric, message);
+ },
+
+ /**
+ * Void a metric value and make sure it isn't displayed on the front-end
+ * anymore.
+ */
+ clear(metric) {
+ metric.value = 0;
+ this.update(metric);
+ },
+
+ /**
+ * Tear everything down, including the front-end by sending a message without
+ * widgets.
+ */
+ destroy() {
+ delete this.metrics;
+ this._send({metric: {skipTelemetry: true}});
+ },
+
+ _send(data) {
+ let frame = this.frame;
+
+ shell.sendEvent(frame, 'developer-hud-update', Cu.cloneInto(data, frame));
+ this._logHistogram(data.metric);
+ },
+
+ _getAddonHistogram(item) {
+ let appName = this._getAddonHistogramName(item, APPNAME_IDX);
+ let histName = this._getAddonHistogramName(item, HISTNAME_IDX);
+
+ return Services.telemetry.getAddonHistogram(appName, CUSTOM_HISTOGRAM_PREFIX
+ + histName);
+ },
+
+ _getAddonHistogramName(item, index) {
+ let array = item.split('_');
+ return array[index].toUpperCase();
+ },
+
+ _clearTelemetryData() {
+ developerHUD._histograms.forEach(function(item) {
+ Services.telemetry.getKeyedHistogramById(item).clear();
+ });
+
+ developerHUD._customHistograms.forEach(item => {
+ this._getAddonHistogram(item).clear();
+ });
+ },
+
+ _sendTelemetryData() {
+ if (!developerHUD._telemetry) {
+ return;
+ }
+ telemetryDebug('calling sendTelemetryData');
+ let frame = this.frame;
+ let payload = {
+ keyedHistograms: {},
+ addonHistograms: {}
+ };
+ // Package the hud histograms.
+ developerHUD._histograms.forEach(function(item) {
+ payload.keyedHistograms[item] =
+ Services.telemetry.getKeyedHistogramById(item).snapshot();
+ });
+
+ // Package the registered hud custom histograms
+ developerHUD._customHistograms.forEach(item => {
+ let appName = this._getAddonHistogramName(item, APPNAME_IDX);
+ let histName = CUSTOM_HISTOGRAM_PREFIX +
+ this._getAddonHistogramName(item, HISTNAME_IDX);
+ let addonHist = Services.telemetry.getAddonHistogram(appName, histName).snapshot();
+ if (!(appName in payload.addonHistograms)) {
+ payload.addonHistograms[appName] = {};
+ }
+ // Do not include histograms with sum of 0.
+ if (addonHist.sum > 0) {
+ payload.addonHistograms[appName][histName] = addonHist;
+ }
+ });
+ shell.sendEvent(frame, 'advanced-telemetry-update', Cu.cloneInto(payload, frame));
+ },
+
+ _logHistogram(metric) {
+ if (!developerHUD._telemetry || metric.skipTelemetry) {
+ return;
+ }
+
+ metric.appName = this.appName;
+ if (!metric.appName) {
+ return;
+ }
+
+ let metricName = metric.name.toUpperCase();
+ let metricAppName = metric.appName.toUpperCase();
+ if (!metric.custom) {
+ let keyedMetricName = 'DEVTOOLS_HUD_' + metricName;
+ try {
+ let keyed = Services.telemetry.getKeyedHistogramById(keyedMetricName);
+ if (keyed) {
+ keyed.add(metric.appName, parseInt(metric.value, 10));
+ developerHUD._histograms.add(keyedMetricName);
+ telemetryDebug(keyedMetricName, metric.value, metric.appName);
+ }
+ } catch(err) {
+ console.error('Histogram error is metricname added to histograms.json:'
+ + keyedMetricName);
+ }
+ } else {
+ let histogramName = CUSTOM_HISTOGRAM_PREFIX + metricAppName + '_'
+ + metricName;
+ // This is a call to add a value to an existing histogram.
+ if (typeof metric.value !== 'undefined') {
+ Services.telemetry.getAddonHistogram(metricAppName,
+ CUSTOM_HISTOGRAM_PREFIX + metricName).add(parseInt(metric.value, 10));
+ telemetryDebug(histogramName, metric.value);
+ return;
+ }
+
+ // The histogram already exists and are not adding data to it.
+ if (developerHUD._customHistograms.has(histogramName)) {
+ return;
+ }
+
+ // This is a call to create a new histogram.
+ try {
+ let metricType = parseInt(metric.type, 10);
+ if (metricType === Services.telemetry.HISTOGRAM_COUNT) {
+ Services.telemetry.registerAddonHistogram(metricAppName,
+ CUSTOM_HISTOGRAM_PREFIX + metricName, metricType);
+ } else {
+ Services.telemetry.registerAddonHistogram(metricAppName,
+ CUSTOM_HISTOGRAM_PREFIX + metricName, metricType, metric.min,
+ metric.max, metric.buckets);
+ }
+ developerHUD._customHistograms.add(histogramName);
+ } catch (err) {
+ console.error('Histogram error: ' + err);
+ }
+ }
+ }
+};
+
+
+/**
+ * The Console Watcher tracks the following metrics in apps: reflows, warnings,
+ * and errors, with security errors reported separately.
+ */
+var consoleWatcher = {
+
+ _client: null,
+ _targets: new Map(),
+ _watching: {
+ reflows: false,
+ warnings: false,
+ errors: false,
+ security: false
+ },
+ _security: [
+ 'Mixed Content Blocker',
+ 'Mixed Content Message',
+ 'CSP',
+ 'Invalid HSTS Headers',
+ 'Invalid HPKP Headers',
+ 'Insecure Password Field',
+ 'SSL',
+ 'CORS'
+ ],
+ _reflowThreshold: 0,
+
+ init(client) {
+ this._client = client;
+ this.consoleListener = this.consoleListener.bind(this);
+
+ let watching = this._watching;
+
+ for (let key in watching) {
+ let metric = key;
+ SettingsListener.observe('hud.' + metric, watching[metric], watch => {
+ // Watch or unwatch the metric.
+ if (watching[metric] = watch) {
+ return;
+ }
+
+ // If unwatched, remove any existing widgets for that metric.
+ for (let target of this._targets.values()) {
+ target.clear({name: metric});
+ }
+ });
+ }
+
+ SettingsListener.observe('hud.reflows.duration', this._reflowThreshold, threshold => {
+ this._reflowThreshold = threshold;
+ });
+
+ client.addListener('logMessage', this.consoleListener);
+ client.addListener('pageError', this.consoleListener);
+ client.addListener('consoleAPICall', this.consoleListener);
+ client.addListener('reflowActivity', this.consoleListener);
+ },
+
+ trackTarget(target) {
+ target.register('reflows');
+ target.register('warnings');
+ target.register('errors');
+ target.register('security');
+
+ this._client.request({
+ to: target.actor.consoleActor,
+ type: 'startListeners',
+ listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity']
+ }, (res) => {
+ this._targets.set(target.actor.consoleActor, target);
+ });
+ },
+
+ untrackTarget(target) {
+ this._client.request({
+ to: target.actor.consoleActor,
+ type: 'stopListeners',
+ listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity']
+ }, (res) => { });
+
+ this._targets.delete(target.actor.consoleActor);
+ },
+
+ consoleListener(type, packet) {
+ let target = this._targets.get(packet.from);
+ let metric = {};
+ let output = '';
+
+ switch (packet.type) {
+
+ case 'pageError':
+ let pageError = packet.pageError;
+
+ if (pageError.warning || pageError.strict) {
+ metric.name = 'warnings';
+ output += 'Warning (';
+ } else {
+ metric.name = 'errors';
+ output += 'Error (';
+ }
+
+ if (this._security.indexOf(pageError.category) > -1) {
+ metric.name = 'security';
+
+ // Telemetry sends the security error category not the
+ // count of security errors.
+ target._logHistogram({
+ name: 'security_category',
+ value: pageError.category
+ });
+
+ // Indicate that the 'hud' security metric (the count of security
+ // errors) should not be sent as a telemetry metric since the
+ // security error category is being sent instead.
+ metric.skipTelemetry = true;
+ }
+
+ let {errorMessage, sourceName, category, lineNumber, columnNumber} = pageError;
+ output += category + '): "' + (errorMessage.initial || errorMessage) +
+ '" in ' + sourceName + ':' + lineNumber + ':' + columnNumber;
+ break;
+
+ case 'consoleAPICall':
+ switch (packet.message.level) {
+
+ case 'error':
+ metric.name = 'errors';
+ output += 'Error (console)';
+ break;
+
+ case 'warn':
+ metric.name = 'warnings';
+ output += 'Warning (console)';
+ break;
+
+ case 'info':
+ this.handleTelemetryMessage(target, packet);
+
+ // Currently, informational log entries are tracked only by
+ // telemetry. Nonetheless, for consistency, we continue here
+ // and let the function return normally, when it concludes 'info'
+ // entries are not being watched.
+ metric.name = 'info';
+ break;
+
+ default:
+ return;
+ }
+ break;
+
+ case 'reflowActivity':
+ metric.name = 'reflows';
+
+ let {start, end, sourceURL, interruptible} = packet;
+ metric.interruptible = interruptible;
+ let duration = Math.round((end - start) * 100) / 100;
+
+ // Record the reflow if the duration exceeds the threshold.
+ if (duration < this._reflowThreshold) {
+ return;
+ }
+
+ output += 'Reflow: ' + duration + 'ms';
+ if (sourceURL) {
+ output += ' ' + this.formatSourceURL(packet);
+ }
+
+ // Telemetry also records reflow duration.
+ target._logHistogram({
+ name: 'reflow_duration',
+ value: Math.round(duration)
+ });
+ break;
+
+ default:
+ return;
+ }
+
+ if (developerHUD._telemetry) {
+ // Always record telemetry for these metrics.
+ if (metric.name === 'errors' || metric.name === 'warnings' || metric.name === 'reflows') {
+ let value = target.metrics.get(metric.name);
+ metric.value = (value || 0) + 1;
+ target._logHistogram(metric);
+
+ // Telemetry has already been recorded.
+ metric.skipTelemetry = true;
+
+ // If the metric is not being watched, persist the incremented value.
+ // If the metric is being watched, `target.bump` will increment the value
+ // of the metric and will persist the incremented value.
+ if (!this._watching[metric.name]) {
+ target.metrics.set(metric.name, metric.value);
+ }
+ }
+ }
+
+ if (!this._watching[metric.name]) {
+ return;
+ }
+
+ target.bump(metric, output);
+ },
+
+ formatSourceURL(packet) {
+ // Abbreviate source URL
+ let source = WebConsoleUtils.abbreviateSourceURL(packet.sourceURL);
+
+ // Add function name and line number
+ let {functionName, sourceLine} = packet;
+ source = 'in ' + (functionName || '<anonymousFunction>') +
+ ', ' + source + ':' + sourceLine;
+
+ return source;
+ },
+
+ handleTelemetryMessage(target, packet) {
+ if (!developerHUD._telemetry) {
+ return;
+ }
+
+ // If this is a 'telemetry' log entry, create a telemetry metric from
+ // the log content.
+ let separator = '|';
+ let logContent = packet.message.arguments.toString();
+
+ if (logContent.indexOf('telemetry') < 0) {
+ return;
+ }
+
+ let telemetryData = logContent.split(separator);
+
+ // Positions of the components of a telemetry log entry.
+ let TELEMETRY_IDENTIFIER_IDX = 0;
+ let NAME_IDX = 1;
+ let VALUE_IDX = 2;
+ let TYPE_IDX = 2;
+ let MIN_IDX = 3;
+ let MAX_IDX = 4;
+ let BUCKETS_IDX = 5;
+ let MAX_CUSTOM_ARGS = 6;
+ let MIN_CUSTOM_ARGS = 3;
+
+ if (telemetryData[TELEMETRY_IDENTIFIER_IDX] != 'telemetry' ||
+ telemetryData.length < MIN_CUSTOM_ARGS ||
+ telemetryData.length > MAX_CUSTOM_ARGS) {
+ return;
+ }
+
+ let metric = {
+ name: telemetryData[NAME_IDX]
+ };
+
+ if (metric.name === 'MGMT') {
+ metric.value = telemetryData[VALUE_IDX];
+ if (metric.value === 'TIMETOSHIP') {
+ telemetryDebug('Received a Ship event');
+ target._sendTelemetryData();
+ } else if (metric.value === 'CLEARMETRICS') {
+ target._clearTelemetryData();
+ }
+ } else {
+ if (telemetryData.length === MIN_CUSTOM_ARGS) {
+ metric.value = telemetryData[VALUE_IDX];
+ } else if (telemetryData.length === MAX_CUSTOM_ARGS) {
+ metric.type = telemetryData[TYPE_IDX];
+ metric.min = telemetryData[MIN_IDX];
+ metric.max = telemetryData[MAX_IDX];
+ metric.buckets = telemetryData[BUCKETS_IDX];
+ }
+ metric.custom = true;
+ target._logHistogram(metric);
+ }
+ }
+};
+developerHUD.registerWatcher(consoleWatcher);
+
+
+var eventLoopLagWatcher = {
+ _client: null,
+ _fronts: new Map(),
+ _active: false,
+
+ init(client) {
+ this._client = client;
+
+ SettingsListener.observe('hud.jank', false, this.settingsListener.bind(this));
+ },
+
+ settingsListener(value) {
+ if (this._active == value) {
+ return;
+ }
+
+ this._active = value;
+
+ // Toggle the state of existing fronts.
+ let fronts = this._fronts;
+ for (let target of fronts.keys()) {
+ if (value) {
+ fronts.get(target).start();
+ } else {
+ fronts.get(target).stop();
+ target.clear({name: 'jank'});
+ }
+ }
+ },
+
+ trackTarget(target) {
+ target.register('jank');
+
+ let front = new EventLoopLagFront(this._client, target.actor);
+ this._fronts.set(target, front);
+
+ front.on('event-loop-lag', time => {
+ target.update({name: 'jank', value: time}, 'Jank: ' + time + 'ms');
+ });
+
+ if (this._active) {
+ front.start();
+ }
+ },
+
+ untrackTarget(target) {
+ let fronts = this._fronts;
+ if (fronts.has(target)) {
+ fronts.get(target).destroy();
+ fronts.delete(target);
+ }
+ }
+};
+developerHUD.registerWatcher(eventLoopLagWatcher);
+
+/*
+ * The performanceEntriesWatcher determines the delta between the epoch
+ * of an app's launch time and the epoch of the app's performance entry marks.
+ * When it receives an "appLaunch" performance entry mark it records the
+ * name of the app being launched and the epoch of when the launch ocurred.
+ * When it receives subsequent performance entry events for the app being
+ * launched, it records the delta of the performance entry opoch compared
+ * to the app-launch epoch and emits an "app-start-time-<performance mark name>"
+ * event containing the delta.
+ *
+ * Additionally, while recording the "app-start-time" for a performance mark,
+ * USS memory at the time of the performance mark is also recorded.
+ */
+var performanceEntriesWatcher = {
+ _client: null,
+ _fronts: new Map(),
+ _appLaunch: new Map(),
+ _supported: [
+ 'contentInteractive',
+ 'navigationInteractive',
+ 'navigationLoaded',
+ 'visuallyLoaded',
+ 'fullyLoaded',
+ 'mediaEnumerated',
+ 'scanEnd'
+ ],
+
+ init(client) {
+ this._client = client;
+ let setting = 'devtools.telemetry.supported_performance_marks';
+ let defaultValue = this._supported.join(',');
+
+ SettingsListener.observe(setting, defaultValue, supported => {
+ this._supported = supported.split(',');
+ });
+ },
+
+ trackTarget(target) {
+ // The performanceEntries watcher doesn't register a metric because
+ // currently the metrics generated are not displayed in
+ // in the front-end.
+
+ let front = new PerformanceEntriesFront(this._client, target.actor);
+ this._fronts.set(target, front);
+
+ // User timings are always gathered; there is no setting to enable/
+ // disable.
+ front.start();
+
+ front.on('entry', detail => {
+
+ // Only process performance marks.
+ if (detail.type !== 'mark') {
+ return;
+ }
+
+ let name = detail.name;
+ let epoch = detail.epoch;
+
+ // If this is an "app launch" mark, record the app that was
+ // launched and the epoch of when it was launched.
+ if (name.indexOf('appLaunch') !== -1) {
+ let CHARS_UNTIL_APP_NAME = 7; // '@app://'
+ let startPos = name.indexOf('@app') + CHARS_UNTIL_APP_NAME;
+ let endPos = name.indexOf('.');
+ let appName = name.slice(startPos, endPos);
+ this._appLaunch.set(appName, epoch);
+ return;
+ }
+
+ // Only process supported performance marks
+ if (this._supported.indexOf(name) === -1) {
+ return;
+ }
+
+ let origin = detail.origin;
+ origin = origin.slice(0, origin.indexOf('.'));
+
+ let appLaunchTime = this._appLaunch.get(origin);
+
+ // Sanity check: ensure we have an app launch time for the app
+ // corresponding to this performance mark.
+ if (!appLaunchTime) {
+ return;
+ }
+
+ let time = epoch - appLaunchTime;
+ let eventName = 'app_startup_time_' + name;
+
+ // Events based on performance marks are for telemetry only, they are
+ // not displayed in the HUD front end.
+ target._logHistogram({name: eventName, value: time});
+
+ memoryWatcher.front(target).residentUnique().then(value => {
+ // bug 1215277, need 'v2' for app-memory histograms
+ eventName = 'app_memory_' + name + '_v2';
+ target._logHistogram({name: eventName, value: value});
+ }, err => {
+ console.error(err);
+ });
+ });
+ },
+
+ untrackTarget(target) {
+ let fronts = this._fronts;
+ if (fronts.has(target)) {
+ fronts.get(target).destroy();
+ fronts.delete(target);
+ }
+ }
+};
+developerHUD.registerWatcher(performanceEntriesWatcher);
+
+/**
+ * The Memory Watcher uses devtools actors to track memory usage.
+ */
+var memoryWatcher = {
+
+ _client: null,
+ _fronts: new Map(),
+ _timers: new Map(),
+ _watching: {
+ uss: false,
+ appmemory: false,
+ jsobjects: false,
+ jsstrings: false,
+ jsother: false,
+ dom: false,
+ style: false,
+ other: false
+ },
+ _active: false,
+
+ init(client) {
+ this._client = client;
+ let watching = this._watching;
+
+ for (let key in watching) {
+ let category = key;
+ SettingsListener.observe('hud.' + category, false, watch => {
+ watching[category] = watch;
+ this.update();
+ });
+ }
+ },
+
+ update() {
+ let watching = this._watching;
+ let active = watching.appmemory || watching.uss;
+
+ if (this._active) {
+ for (let target of this._fronts.keys()) {
+ if (!watching.appmemory) target.clear({name: 'memory'});
+ if (!watching.uss) target.clear({name: 'uss'});
+ if (!active) clearTimeout(this._timers.get(target));
+ }
+ } else if (active) {
+ for (let target of this._fronts.keys()) {
+ this.measure(target);
+ }
+ }
+ this._active = active;
+ },
+
+ measure(target) {
+ let watch = this._watching;
+ let format = this.formatMemory;
+
+ if (watch.uss) {
+ this.front(target).residentUnique().then(value => {
+ target.update({name: 'uss', value: value}, 'USS: ' + format(value));
+ }, err => {
+ console.error(err);
+ });
+ }
+
+ if (watch.appmemory) {
+ front.measure().then(data => {
+ let total = 0;
+ let details = [];
+
+ function item(name, condition, value) {
+ if (!condition) {
+ return;
+ }
+
+ let v = parseInt(value);
+ total += v;
+ details.push(name + ': ' + format(v));
+ }
+
+ item('JS objects', watch.jsobjects, data.jsObjectsSize);
+ item('JS strings', watch.jsstrings, data.jsStringsSize);
+ item('JS other', watch.jsother, data.jsOtherSize);
+ item('DOM', watch.dom, data.domSize);
+ item('Style', watch.style, data.styleSize);
+ item('Other', watch.other, data.otherSize);
+ // TODO Also count images size (bug #976007).
+
+ target.update({name: 'memory', value: total},
+ 'App Memory: ' + format(total) + ' (' + details.join(', ') + ')');
+ }, err => {
+ console.error(err);
+ });
+ }
+
+ let timer = setTimeout(() => this.measure(target), 2000);
+ this._timers.set(target, timer);
+ },
+
+ formatMemory(bytes) {
+ var prefix = ['','K','M','G','T','P','E','Z','Y'];
+ var i = 0;
+ for (; bytes > 1024 && i < prefix.length; ++i) {
+ bytes /= 1024;
+ }
+ return (Math.round(bytes * 100) / 100) + ' ' + prefix[i] + 'B';
+ },
+
+ trackTarget(target) {
+ target.register('uss');
+ target.register('memory');
+ this._fronts.set(target, MemoryFront(this._client, target.actor));
+ if (this._active) {
+ this.measure(target);
+ }
+ },
+
+ untrackTarget(target) {
+ let front = this._fronts.get(target);
+ if (front) {
+ front.destroy();
+ clearTimeout(this._timers.get(target));
+ this._fronts.delete(target);
+ this._timers.delete(target);
+ }
+ },
+
+ front(target) {
+ return this._fronts.get(target);
+ }
+};
+developerHUD.registerWatcher(memoryWatcher);