// -*- 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; }) };