summaryrefslogtreecommitdiffstats
path: root/toolkit/components/perfmonitoring/PerformanceStats.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/perfmonitoring/PerformanceStats.jsm')
-rw-r--r--toolkit/components/perfmonitoring/PerformanceStats.jsm1000
1 files changed, 1000 insertions, 0 deletions
diff --git a/toolkit/components/perfmonitoring/PerformanceStats.jsm b/toolkit/components/perfmonitoring/PerformanceStats.jsm
new file mode 100644
index 000000000..20f27a51b
--- /dev/null
+++ b/toolkit/components/perfmonitoring/PerformanceStats.jsm
@@ -0,0 +1,1000 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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.EXPORTED_SYMBOLS = ["PerformanceStats"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+/**
+ * API for querying and examining performance data.
+ *
+ * This API exposes data from several probes implemented by the JavaScript VM.
+ * See `PerformanceStats.getMonitor()` for information on how to monitor data
+ * from one or more probes and `PerformanceData` for the information obtained
+ * from the probes.
+ *
+ * Data is collected by "Performance Group". Typically, a Performance Group
+ * is an add-on, or a frame, or the internals of the application.
+ *
+ * Generally, if you have the choice between PerformanceStats and PerformanceWatcher,
+ * you should favor PerformanceWatcher.
+ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/ObjectUtils.jsm", this);
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
+ "resource://gre/modules/Timer.jsm");
+
+// The nsIPerformanceStatsService provides lower-level
+// access to SpiderMonkey and the probes.
+XPCOMUtils.defineLazyServiceGetter(this, "performanceStatsService",
+ "@mozilla.org/toolkit/performance-stats-service;1",
+ Ci.nsIPerformanceStatsService);
+
+// The finalizer lets us automatically release (and when possible deactivate)
+// probes when a monitor is garbage-collected.
+XPCOMUtils.defineLazyServiceGetter(this, "finalizer",
+ "@mozilla.org/toolkit/finalizationwitness;1",
+ Ci.nsIFinalizationWitnessService
+);
+
+// The topic used to notify that a PerformanceMonitor has been garbage-collected
+// and that we can release/close the probes it holds.
+const FINALIZATION_TOPIC = "performancemonitor-finalize";
+
+const PROPERTIES_META_IMMUTABLE = ["addonId", "isSystem", "isChildProcess", "groupId", "processId"];
+const PROPERTIES_META = [...PROPERTIES_META_IMMUTABLE, "windowId", "title", "name"];
+
+// How long we wait for children processes to respond.
+const MAX_WAIT_FOR_CHILD_PROCESS_MS = 5000;
+
+var isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+/**
+ * Access to a low-level performance probe.
+ *
+ * Each probe is dedicated to some form of performance monitoring.
+ * As each probe may have a performance impact, a probe is activated
+ * only when a client has requested a PerformanceMonitor for this probe,
+ * and deactivated once all clients are disposed of.
+ */
+function Probe(name, impl) {
+ this._name = name;
+ this._counter = 0;
+ this._impl = impl;
+}
+Probe.prototype = {
+ /**
+ * Acquire the probe on behalf of a client.
+ *
+ * If the probe was inactive, activate it. Note that activating a probe
+ * can incur a memory or performance cost.
+ */
+ acquire: function() {
+ if (this._counter == 0) {
+ this._impl.isActive = true;
+ Process.broadcast("acquire", [this._name]);
+ }
+ this._counter++;
+ },
+
+ /**
+ * Release the probe on behalf of a client.
+ *
+ * If this was the last client for this probe, deactivate it.
+ */
+ release: function() {
+ this._counter--;
+ if (this._counter == 0) {
+ try {
+ this._impl.isActive = false;
+ } catch (ex) {
+ if (ex && typeof ex == "object" && ex.result == Components.results.NS_ERROR_NOT_AVAILABLE) {
+ // The service has already been shutdown. Ignore further shutdown requests.
+ return;
+ }
+ throw ex;
+ }
+ Process.broadcast("release", [this._name]);
+ }
+ },
+
+ /**
+ * Obtain data from this probe, once it is available.
+ *
+ * @param {nsIPerformanceStats} xpcom A xpcom object obtained from
+ * SpiderMonkey. Only the fields updated by the low-level probe
+ * are in a specified state.
+ * @return {object} An object containing the data extracted from this
+ * probe. Actual format depends on the probe.
+ */
+ extract: function(xpcom) {
+ if (!this._impl.isActive) {
+ throw new Error(`Probe is inactive: ${this._name}`);
+ }
+ return this._impl.extract(xpcom);
+ },
+
+ /**
+ * @param {object} a An object returned by `this.extract()`.
+ * @param {object} b An object returned by `this.extract()`.
+ *
+ * @return {true} If `a` and `b` hold identical values.
+ */
+ isEqual: function(a, b) {
+ if (a == null && b == null) {
+ return true;
+ }
+ if (a != null && b != null) {
+ return this._impl.isEqual(a, b);
+ }
+ return false;
+ },
+
+ /**
+ * @param {object} a An object returned by `this.extract()`. May
+ * NOT be `null`.
+ * @param {object} b An object returned by `this.extract()`. May
+ * be `null`.
+ *
+ * @return {object} An object representing `a - b`. If `b` is
+ * `null`, this is `a`.
+ */
+ subtract: function(a, b) {
+ if (a == null) {
+ throw new TypeError();
+ }
+ if (b == null) {
+ return a;
+ }
+ return this._impl.subtract(a, b);
+ },
+
+ importChildCompartments: function(parent, children) {
+ if (!Array.isArray(children)) {
+ throw new TypeError();
+ }
+ if (!parent || !(parent instanceof PerformanceDataLeaf)) {
+ throw new TypeError();
+ }
+ return this._impl.importChildCompartments(parent, children);
+ },
+
+ /**
+ * The name of the probe.
+ */
+ get name() {
+ return this._name;
+ },
+
+ compose: function(stats) {
+ if (!Array.isArray(stats)) {
+ throw new TypeError();
+ }
+ return this._impl.compose(stats);
+ }
+};
+
+// Utility function. Return the position of the last non-0 item in an
+// array, or -1 if there isn't any such item.
+function lastNonZero(array) {
+ for (let i = array.length - 1; i >= 0; --i) {
+ if (array[i] != 0) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * The actual Probes implemented by SpiderMonkey.
+ */
+var Probes = {
+ /**
+ * A probe measuring jank.
+ *
+ * Data provided by this probe uses the following format:
+ *
+ * @field {number} totalCPUTime The total amount of time spent using the
+ * CPU for this performance group, in µs.
+ * @field {number} totalSystemTime The total amount of time spent in the
+ * kernel for this performance group, in µs.
+ * @field {Array<number>} durations An array containing at each position `i`
+ * the number of times execution of this component has lasted at least `2^i`
+ * milliseconds.
+ * @field {number} longestDuration The index of the highest non-0 value in
+ * `durations`.
+ */
+ jank: new Probe("jank", {
+ set isActive(x) {
+ performanceStatsService.isMonitoringJank = x;
+ },
+ get isActive() {
+ return performanceStatsService.isMonitoringJank;
+ },
+ extract: function(xpcom) {
+ let durations = xpcom.getDurations();
+ return {
+ totalUserTime: xpcom.totalUserTime,
+ totalSystemTime: xpcom.totalSystemTime,
+ totalCPUTime: xpcom.totalUserTime + xpcom.totalSystemTime,
+ durations: durations,
+ longestDuration: lastNonZero(durations)
+ }
+ },
+ isEqual: function(a, b) {
+ // invariant: `a` and `b` are both non-null
+ if (a.totalUserTime != b.totalUserTime) {
+ return false;
+ }
+ if (a.totalSystemTime != b.totalSystemTime) {
+ return false;
+ }
+ for (let i = 0; i < a.durations.length; ++i) {
+ if (a.durations[i] != b.durations[i]) {
+ return false;
+ }
+ }
+ return true;
+ },
+ subtract: function(a, b) {
+ // invariant: `a` and `b` are both non-null
+ let result = {
+ totalUserTime: a.totalUserTime - b.totalUserTime,
+ totalSystemTime: a.totalSystemTime - b.totalSystemTime,
+ totalCPUTime: a.totalCPUTime - b.totalCPUTime,
+ durations: [],
+ longestDuration: -1,
+ };
+ for (let i = 0; i < a.durations.length; ++i) {
+ result.durations[i] = a.durations[i] - b.durations[i];
+ }
+ result.longestDuration = lastNonZero(result.durations);
+ return result;
+ },
+ importChildCompartments: function() { /* nothing to do */ },
+ compose: function(stats) {
+ let result = {
+ totalUserTime: 0,
+ totalSystemTime: 0,
+ totalCPUTime: 0,
+ durations: [],
+ longestDuration: -1
+ };
+ for (let stat of stats) {
+ result.totalUserTime += stat.totalUserTime;
+ result.totalSystemTime += stat.totalSystemTime;
+ result.totalCPUTime += stat.totalCPUTime;
+ for (let i = 0; i < stat.durations.length; ++i) {
+ result.durations[i] += stat.durations[i];
+ }
+ result.longestDuration = Math.max(result.longestDuration, stat.longestDuration);
+ }
+ return result;
+ }
+ }),
+
+ /**
+ * A probe measuring CPOW activity.
+ *
+ * Data provided by this probe uses the following format:
+ *
+ * @field {number} totalCPOWTime The amount of wallclock time
+ * spent executing blocking cross-process calls, in µs.
+ */
+ cpow: new Probe("cpow", {
+ set isActive(x) {
+ performanceStatsService.isMonitoringCPOW = x;
+ },
+ get isActive() {
+ return performanceStatsService.isMonitoringCPOW;
+ },
+ extract: function(xpcom) {
+ return {
+ totalCPOWTime: xpcom.totalCPOWTime
+ };
+ },
+ isEqual: function(a, b) {
+ return a.totalCPOWTime == b.totalCPOWTime;
+ },
+ subtract: function(a, b) {
+ return {
+ totalCPOWTime: a.totalCPOWTime - b.totalCPOWTime
+ };
+ },
+ importChildCompartments: function() { /* nothing to do */ },
+ compose: function(stats) {
+ let totalCPOWTime = 0;
+ for (let stat of stats) {
+ totalCPOWTime += stat.totalCPOWTime;
+ }
+ return { totalCPOWTime };
+ },
+ }),
+
+ /**
+ * A probe measuring activations, i.e. the number
+ * of times code execution has entered a given
+ * PerformanceGroup.
+ *
+ * Note that this probe is always active.
+ *
+ * Data provided by this probe uses the following format:
+ * @type {number} ticks The number of times execution has entered
+ * this performance group.
+ */
+ ticks: new Probe("ticks", {
+ set isActive(x) { /* this probe cannot be deactivated */ },
+ get isActive() { return true; },
+ extract: function(xpcom) {
+ return {
+ ticks: xpcom.ticks
+ };
+ },
+ isEqual: function(a, b) {
+ return a.ticks == b.ticks;
+ },
+ subtract: function(a, b) {
+ return {
+ ticks: a.ticks - b.ticks
+ };
+ },
+ importChildCompartments: function() { /* nothing to do */ },
+ compose: function(stats) {
+ let ticks = 0;
+ for (let stat of stats) {
+ ticks += stat.ticks;
+ }
+ return { ticks };
+ },
+ }),
+
+ compartments: new Probe("compartments", {
+ set isActive(x) {
+ performanceStatsService.isMonitoringPerCompartment = x;
+ },
+ get isActive() {
+ return performanceStatsService.isMonitoringPerCompartment;
+ },
+ extract: function(xpcom) {
+ return null;
+ },
+ isEqual: function(a, b) {
+ return true;
+ },
+ subtract: function(a, b) {
+ return true;
+ },
+ importChildCompartments: function(parent, children) {
+ parent.children = children;
+ },
+ compose: function(stats) {
+ return null;
+ },
+ }),
+};
+
+/**
+ * A monitor for a set of probes.
+ *
+ * Keeping probes active when they are unused is often a bad
+ * idea for performance reasons. Upon destruction, or whenever
+ * a client calls `dispose`, this monitor releases the probes,
+ * which may let the system deactivate them.
+ */
+function PerformanceMonitor(probes) {
+ this._probes = probes;
+
+ // Activate low-level features as needed
+ for (let probe of probes) {
+ probe.acquire();
+ }
+
+ // A finalization witness. At some point after the garbage-collection of
+ // `this` object, a notification of `FINALIZATION_TOPIC` will be triggered
+ // with `id` as message.
+ this._id = PerformanceMonitor.makeId();
+ this._finalizer = finalizer.make(FINALIZATION_TOPIC, this._id)
+ PerformanceMonitor._monitors.set(this._id, probes);
+}
+PerformanceMonitor.prototype = {
+ /**
+ * The names of probes activated in this monitor.
+ */
+ get probeNames() {
+ return this._probes.map(probe => probe.name);
+ },
+
+ /**
+ * Return asynchronously a snapshot with the data
+ * for each probe monitored by this PerformanceMonitor.
+ *
+ * All numeric values are non-negative and can only increase. Depending on
+ * the probe and the underlying operating system, probes may not be available
+ * immediately and may miss some activity.
+ *
+ * Clients should NOT expect that the first call to `promiseSnapshot()`
+ * will return a `Snapshot` in which all values are 0. For most uses,
+ * the appropriate scenario is to perform a first call to `promiseSnapshot()`
+ * to obtain a baseline, and then watch evolution of the values by calling
+ * `promiseSnapshot()` and `subtract()`.
+ *
+ * On the other hand, numeric values are also monotonic across several instances
+ * of a PerformanceMonitor with the same probes.
+ * let a = PerformanceStats.getMonitor(someProbes);
+ * let snapshot1 = yield a.promiseSnapshot();
+ *
+ * // ...
+ * let b = PerformanceStats.getMonitor(someProbes); // Same list of probes
+ * let snapshot2 = yield b.promiseSnapshot();
+ *
+ * // all values of `snapshot2` are greater or equal to values of `snapshot1`.
+ *
+ * @param {object} options If provided, an object that may contain the following
+ * fields:
+ * {Array<string>} probeNames The subset of probes to use for this snapshot.
+ * These probes must be a subset of the probes active in the monitor.
+ *
+ * @return {Promise}
+ * @resolve {Snapshot}
+ */
+ _checkBeforeSnapshot: function(options) {
+ if (!this._finalizer) {
+ throw new Error("dispose() has already been called, this PerformanceMonitor is not usable anymore");
+ }
+ let probes;
+ if (options && options.probeNames || undefined) {
+ if (!Array.isArray(options.probeNames)) {
+ throw new TypeError();
+ }
+ // Make sure that we only request probes that we have
+ for (let probeName of options.probeNames) {
+ let probe = this._probes.find(probe => probe.name == probeName);
+ if (!probe) {
+ throw new TypeError(`I need probe ${probeName} but I only have ${this.probeNames}`);
+ }
+ if (!probes) {
+ probes = [];
+ }
+ probes.push(probe);
+ }
+ } else {
+ probes = this._probes;
+ }
+ return probes;
+ },
+ promiseContentSnapshot: function(options = null) {
+ this._checkBeforeSnapshot(options);
+ return (new ProcessSnapshot(performanceStatsService.getSnapshot()));
+ },
+ promiseSnapshot: function(options = null) {
+ let probes = this._checkBeforeSnapshot(options);
+ return Task.spawn(function*() {
+ let childProcesses = yield Process.broadcastAndCollect("collect", {probeNames: probes.map(p => p.name)});
+ let xpcom = performanceStatsService.getSnapshot();
+ return new ApplicationSnapshot({
+ xpcom,
+ childProcesses,
+ probes,
+ date: Cu.now()
+ });
+ });
+ },
+
+ /**
+ * Release the probes used by this monitor.
+ *
+ * Releasing probes as soon as they are unused is a good idea, as some probes
+ * cost CPU and/or memory.
+ */
+ dispose: function() {
+ if (!this._finalizer) {
+ return;
+ }
+ this._finalizer.forget();
+ PerformanceMonitor.dispose(this._id);
+
+ // As a safeguard against double-release, reset everything to `null`
+ this._probes = null;
+ this._id = null;
+ this._finalizer = null;
+ }
+};
+/**
+ * @type {Map<string, Array<string>>} A map from id (as produced by `makeId`)
+ * to list of probes. Used to deallocate a list of probes during finalization.
+ */
+PerformanceMonitor._monitors = new Map();
+
+/**
+ * Create a `PerformanceMonitor` for a list of probes, register it for
+ * finalization.
+ */
+PerformanceMonitor.make = function(probeNames) {
+ // Sanity checks
+ if (!Array.isArray(probeNames)) {
+ throw new TypeError("Expected an array, got " + probes);
+ }
+ let probes = [];
+ for (let probeName of probeNames) {
+ if (!(probeName in Probes)) {
+ throw new TypeError("Probe not implemented: " + probeName);
+ }
+ probes.push(Probes[probeName]);
+ }
+
+ return (new PerformanceMonitor(probes));
+};
+
+/**
+ * Implementation of `dispose`.
+ *
+ * The actual implementation of `dispose` is as a method of `PerformanceMonitor`,
+ * rather than `PerformanceMonitor.prototype`, to avoid needing a strong reference
+ * to instances of `PerformanceMonitor`, which would defeat the purpose of
+ * finalization.
+ */
+PerformanceMonitor.dispose = function(id) {
+ let probes = PerformanceMonitor._monitors.get(id);
+ if (!probes) {
+ throw new TypeError("`dispose()` has already been called on this monitor");
+ }
+
+ PerformanceMonitor._monitors.delete(id);
+ for (let probe of probes) {
+ probe.release();
+ }
+}
+
+// Generate a unique id for each PerformanceMonitor. Used during
+// finalization.
+PerformanceMonitor._counter = 0;
+PerformanceMonitor.makeId = function() {
+ return "PerformanceMonitor-" + (this._counter++);
+}
+
+// Once a `PerformanceMonitor` has been garbage-collected,
+// release the probes unless `dispose()` has already been called.
+Services.obs.addObserver(function(subject, topic, value) {
+ PerformanceMonitor.dispose(value);
+}, FINALIZATION_TOPIC, false);
+
+// Public API
+this.PerformanceStats = {
+ /**
+ * Create a monitor for observing a set of performance probes.
+ */
+ getMonitor: function(probes) {
+ return PerformanceMonitor.make(probes);
+ }
+};
+
+
+/**
+ * Information on a single performance group.
+ *
+ * This offers the following fields:
+ *
+ * @field {string} name The name of the performance group:
+ * - for the process itself, "<process>";
+ * - for platform code, "<platform>";
+ * - for an add-on, the identifier of the addon (e.g. "myaddon@foo.bar");
+ * - for a webpage, the url of the page.
+ *
+ * @field {string} addonId The identifier of the addon (e.g. "myaddon@foo.bar").
+ *
+ * @field {string|null} title The title of the webpage to which this code
+ * belongs. Note that this is the title of the entire webpage (i.e. the tab),
+ * even if the code is executed in an iframe. Also note that this title may
+ * change over time.
+ *
+ * @field {number} windowId The outer window ID of the top-level nsIDOMWindow
+ * to which this code belongs. May be 0 if the code doesn't belong to any
+ * nsIDOMWindow.
+ *
+ * @field {boolean} isSystem `true` if the component is a system component (i.e.
+ * an add-on or platform-code), `false` otherwise (i.e. a webpage).
+ *
+ * @field {object|undefined} activations See the documentation of probe "ticks".
+ * `undefined` if this probe is not active.
+ *
+ * @field {object|undefined} jank See the documentation of probe "jank".
+ * `undefined` if this probe is not active.
+ *
+ * @field {object|undefined} cpow See the documentation of probe "cpow".
+ * `undefined` if this probe is not active.
+ */
+function PerformanceDataLeaf({xpcom, json, probes}) {
+ if (xpcom && json) {
+ throw new TypeError("Cannot import both xpcom and json data");
+ }
+ let source = xpcom || json;
+ for (let k of PROPERTIES_META) {
+ this[k] = source[k];
+ }
+ if (xpcom) {
+ for (let probe of probes) {
+ this[probe.name] = probe.extract(xpcom);
+ }
+ this.isChildProcess = false;
+ } else {
+ for (let probe of probes) {
+ this[probe.name] = json[probe.name];
+ }
+ this.isChildProcess = true;
+ }
+ this.owner = null;
+}
+PerformanceDataLeaf.prototype = {
+ /**
+ * Compare two instances of `PerformanceData`
+ *
+ * @return `true` if `this` and `to` have equal values in all fields.
+ */
+ equals: function(to) {
+ if (!(to instanceof PerformanceDataLeaf)) {
+ throw new TypeError();
+ }
+ for (let probeName of Object.keys(Probes)) {
+ let probe = Probes[probeName];
+ if (!probe.isEqual(this[probeName], to[probeName])) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Compute the delta between two instances of `PerformanceData`.
+ *
+ * @param {PerformanceData|null} to. If `null`, assumed an instance of
+ * `PerformanceData` in which all numeric values are 0.
+ *
+ * @return {PerformanceDiff} The performance usage between `to` and `this`.
+ */
+ subtract: function(to = null) {
+ return (new PerformanceDiffLeaf(this, to));
+ }
+};
+
+function PerformanceData(timestamp) {
+ this._parent = null;
+ this._content = new Map();
+ this._all = [];
+ this._timestamp = timestamp;
+}
+PerformanceData.prototype = {
+ addChild: function(stat) {
+ if (!(stat instanceof PerformanceDataLeaf)) {
+ throw new TypeError(); // FIXME
+ }
+ if (!stat.isChildProcess) {
+ throw new TypeError(); // FIXME
+ }
+ this._content.set(stat.groupId, stat);
+ this._all.push(stat);
+ stat.owner = this;
+ },
+ setParent: function(stat) {
+ if (!(stat instanceof PerformanceDataLeaf)) {
+ throw new TypeError(); // FIXME
+ }
+ if (stat.isChildProcess) {
+ throw new TypeError(); // FIXME
+ }
+ this._parent = stat;
+ this._all.push(stat);
+ stat.owner = this;
+ },
+ equals: function(to) {
+ if (this._parent && !to._parent) {
+ return false;
+ }
+ if (!this._parent && to._parent) {
+ return false;
+ }
+ if (this._content.size != to._content.size) {
+ return false;
+ }
+ if (this._parent && !this._parent.equals(to._parent)) {
+ return false;
+ }
+ for (let [k, v] of this._content) {
+ let v2 = to._content.get(k);
+ if (!v2) {
+ return false;
+ }
+ if (!v.equals(v2)) {
+ return false;
+ }
+ }
+ return true;
+ },
+ subtract: function(to = null) {
+ return (new PerformanceDiff(this, to));
+ },
+ get addonId() {
+ return this._all[0].addonId;
+ },
+ get title() {
+ return this._all[0].title;
+ }
+};
+
+function PerformanceDiff(current, old = null) {
+ this.addonId = current.addonId;
+ this.title = current.title;
+ this.windowId = current.windowId;
+ this.deltaT = old ? current._timestamp - old._timestamp : Infinity;
+ this._all = [];
+
+ // Handle the parent, if any.
+ if (current._parent) {
+ this._parent = old?current._parent.subtract(old._parent):current._parent;
+ this._all.push(this._parent);
+ this._parent.owner = this;
+ } else {
+ this._parent = null;
+ }
+
+ // Handle the children, if any.
+ this._content = new Map();
+ for (let [k, stat] of current._content) {
+ let diff = stat.subtract(old ? old._content.get(k) : null);
+ this._content.set(k, diff);
+ this._all.push(diff);
+ diff.owner = this;
+ }
+
+ // Now consolidate data
+ for (let k of Object.keys(Probes)) {
+ if (!(k in this._all[0])) {
+ // The stats don't contain data from this probe.
+ continue;
+ }
+ let data = this._all.map(item => item[k]);
+ let probe = Probes[k];
+ this[k] = probe.compose(data);
+ }
+}
+PerformanceDiff.prototype = {
+ toString: function() {
+ return `[PerformanceDiff] ${this.key}`;
+ },
+ get windowIds() {
+ return this._all.map(item => item.windowId).filter(x => !!x);
+ },
+ get groupIds() {
+ return this._all.map(item => item.groupId);
+ },
+ get key() {
+ if (this.addonId) {
+ return this.addonId;
+ }
+ if (this._parent) {
+ return this._parent.windowId;
+ }
+ return this._all[0].groupId;
+ },
+ get names() {
+ return this._all.map(item => item.name);
+ },
+ get processes() {
+ return this._all.map(item => ({ isChildProcess: item.isChildProcess, processId: item.processId}));
+ }
+};
+
+/**
+ * The delta between two instances of `PerformanceDataLeaf`.
+ *
+ * Used to monitor resource usage between two timestamps.
+ */
+function PerformanceDiffLeaf(current, old = null) {
+ for (let k of PROPERTIES_META) {
+ this[k] = current[k];
+ }
+
+ for (let probeName of Object.keys(Probes)) {
+ let other = null;
+ if (old && probeName in old) {
+ other = old[probeName];
+ }
+
+ if (probeName in current) {
+ this[probeName] = Probes[probeName].subtract(current[probeName], other);
+ }
+ }
+}
+
+/**
+ * A snapshot of a single process.
+ */
+function ProcessSnapshot({xpcom, probes}) {
+ this.componentsData = [];
+
+ let subgroups = new Map();
+ let enumeration = xpcom.getComponentsData().enumerate();
+ while (enumeration.hasMoreElements()) {
+ let xpcom = enumeration.getNext().QueryInterface(Ci.nsIPerformanceStats);
+ let stat = (new PerformanceDataLeaf({xpcom, probes}));
+
+ if (!xpcom.parentId) {
+ this.componentsData.push(stat);
+ } else {
+ let siblings = subgroups.get(xpcom.parentId);
+ if (!siblings) {
+ subgroups.set(xpcom.parentId, (siblings = []));
+ }
+ siblings.push(stat);
+ }
+ }
+
+ for (let group of this.componentsData) {
+ for (let probe of probes) {
+ probe.importChildCompartments(group, subgroups.get(group.groupId) || []);
+ }
+ }
+
+ this.processData = (new PerformanceDataLeaf({xpcom: xpcom.getProcessData(), probes}));
+}
+
+/**
+ * A snapshot of the performance usage of the application.
+ *
+ * @param {nsIPerformanceSnapshot} xpcom The data acquired from this process.
+ * @param {Array<Object>} childProcesses The data acquired from children processes.
+ * @param {Array<Probe>} probes The active probes.
+ */
+function ApplicationSnapshot({xpcom, childProcesses, probes, date}) {
+ ProcessSnapshot.call(this, {xpcom, probes});
+
+ this.addons = new Map();
+ this.webpages = new Map();
+ this.date = date;
+
+ // Child processes
+ for (let {componentsData} of (childProcesses || [])) {
+ // We are only interested in `componentsData` for the time being.
+ for (let json of componentsData) {
+ let leaf = (new PerformanceDataLeaf({json, probes}));
+ this.componentsData.push(leaf);
+ }
+ }
+
+ for (let leaf of this.componentsData) {
+ let key, map;
+ if (leaf.addonId) {
+ key = leaf.addonId;
+ map = this.addons;
+ } else if (leaf.windowId) {
+ key = leaf.windowId;
+ map = this.webpages;
+ } else {
+ continue;
+ }
+
+ let combined = map.get(key);
+ if (!combined) {
+ combined = new PerformanceData(date);
+ map.set(key, combined);
+ }
+ if (leaf.isChildProcess) {
+ combined.addChild(leaf);
+ } else {
+ combined.setParent(leaf);
+ }
+ }
+}
+
+/**
+ * Communication with other processes
+ */
+var Process = {
+ // a counter used to match responses to requests
+ _idcounter: 0,
+ _loader: null,
+ /**
+ * If we are in a child process, return `null`.
+ * Otherwise, return the global parent process message manager
+ * and load the script to connect to children processes.
+ */
+ get loader() {
+ if (isContent) {
+ return null;
+ }
+ if (this._loader) {
+ return this._loader;
+ }
+ Services.ppmm.loadProcessScript("resource://gre/modules/PerformanceStats-content.js",
+ true/* including future processes*/);
+ return this._loader = Services.ppmm;
+ },
+
+ /**
+ * Broadcast a message to all children processes.
+ *
+ * NOOP if we are in a child process.
+ */
+ broadcast: function(topic, payload) {
+ if (!this.loader) {
+ return;
+ }
+ this.loader.broadcastAsyncMessage("performance-stats-service-" + topic, {payload});
+ },
+
+ /**
+ * Brodcast a message to all children processes and wait for answer.
+ *
+ * NOOP if we are in a child process, or if we have no children processes,
+ * in which case we return `undefined`.
+ *
+ * @return {undefined} If we have no children processes, in particular
+ * if we are in a child process.
+ * @return {Promise<Array<Object>>} If we have children processes, an
+ * array of objects with a structure similar to PerformanceData. Note
+ * that the array may be empty if no child process responded.
+ */
+ broadcastAndCollect: Task.async(function*(topic, payload) {
+ if (!this.loader || this.loader.childCount == 1) {
+ return undefined;
+ }
+ const TOPIC = "performance-stats-service-" + topic;
+ let id = this._idcounter++;
+
+ // The number of responses we are expecting. Note that we may
+ // not receive all responses if a process is too long to respond.
+ let expecting = this.loader.childCount;
+
+ // The responses we have collected, in arbitrary order.
+ let collected = [];
+ let deferred = PromiseUtils.defer();
+
+ let observer = function({data, target}) {
+ if (data.id != id) {
+ // Collision between two collections,
+ // ignore the other one.
+ return;
+ }
+ if (data.data) {
+ collected.push(data.data)
+ }
+ if (--expecting > 0) {
+ // We are still waiting for at least one response.
+ return;
+ }
+ deferred.resolve();
+ };
+ this.loader.addMessageListener(TOPIC, observer);
+ this.loader.broadcastAsyncMessage(
+ TOPIC,
+ {id, payload}
+ );
+
+ // Processes can die/freeze/be busy loading a page..., so don't expect
+ // that they will always respond.
+ let timeout = setTimeout(() => {
+ if (expecting == 0) {
+ return;
+ }
+ deferred.resolve();
+ }, MAX_WAIT_FOR_CHILD_PROCESS_MS);
+
+ deferred.promise.then(() => {
+ clearTimeout(timeout);
+ });
+
+ yield deferred.promise;
+ this.loader.removeMessageListener(TOPIC, observer);
+
+ return collected;
+ })
+};