diff options
Diffstat (limited to 'toolkit/components/perfmonitoring/PerformanceStats.jsm')
-rw-r--r-- | toolkit/components/perfmonitoring/PerformanceStats.jsm | 1000 |
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; + }) +}; |