mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-12 14:20:14 +02:00
This removes all the code for add-on performance watching from the perfmonitoring component. This should mean that for add-on compartments, we no longer trigger jank or CPOW monitoring in the JS engine. This should result in minor performance improvements. As a result, about:performance no longer reports on add-on performance (but still reports on web page performance). It also removes the AddonWatchers.jsm module and the related Nightly- only UI (disabled in the parent commit) and strings. This UI wasn't ready for release, there wasn't sufficient data it was creating value for users, and there was some evidence that it didn't always correctly identify the cause of performance issues, thus potentially leading to user confusion or annoyance. Removing it therefore seemed the right thing to do. MozReview-Commit-ID: LsRwuaUtq6L --HG-- extra : rebase_source : 92d4b775a7a7cbb5793e74eea471be81be974dda
986 lines
28 KiB
JavaScript
986 lines
28 KiB
JavaScript
// -*- 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 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 = ["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() {
|
|
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() {
|
|
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(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(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(a, b) {
|
|
if (a == null) {
|
|
throw new TypeError();
|
|
}
|
|
if (b == null) {
|
|
return a;
|
|
}
|
|
return this._impl.subtract(a, b);
|
|
},
|
|
|
|
importChildCompartments(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(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(xpcom) {
|
|
let durations = xpcom.getDurations();
|
|
return {
|
|
totalUserTime: xpcom.totalUserTime,
|
|
totalSystemTime: xpcom.totalSystemTime,
|
|
totalCPUTime: xpcom.totalUserTime + xpcom.totalSystemTime,
|
|
durations,
|
|
longestDuration: lastNonZero(durations)
|
|
}
|
|
},
|
|
isEqual(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(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() { /* nothing to do */ },
|
|
compose(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(xpcom) {
|
|
return {
|
|
totalCPOWTime: xpcom.totalCPOWTime
|
|
};
|
|
},
|
|
isEqual(a, b) {
|
|
return a.totalCPOWTime == b.totalCPOWTime;
|
|
},
|
|
subtract(a, b) {
|
|
return {
|
|
totalCPOWTime: a.totalCPOWTime - b.totalCPOWTime
|
|
};
|
|
},
|
|
importChildCompartments() { /* nothing to do */ },
|
|
compose(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(xpcom) {
|
|
return {
|
|
ticks: xpcom.ticks
|
|
};
|
|
},
|
|
isEqual(a, b) {
|
|
return a.ticks == b.ticks;
|
|
},
|
|
subtract(a, b) {
|
|
return {
|
|
ticks: a.ticks - b.ticks
|
|
};
|
|
},
|
|
importChildCompartments() { /* nothing to do */ },
|
|
compose(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(xpcom) {
|
|
return null;
|
|
},
|
|
isEqual(a, b) {
|
|
return true;
|
|
},
|
|
subtract(a, b) {
|
|
return true;
|
|
},
|
|
importChildCompartments(parent, children) {
|
|
parent.children = children;
|
|
},
|
|
compose(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(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(options = null) {
|
|
this._checkBeforeSnapshot(options);
|
|
return (new ProcessSnapshot(performanceStatsService.getSnapshot()));
|
|
},
|
|
promiseSnapshot(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() {
|
|
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(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 a webpage, the url of the page.
|
|
*
|
|
* @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(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(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(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(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(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(to = null) {
|
|
return (new PerformanceDiff(this, to));
|
|
},
|
|
get title() {
|
|
return this._all[0].title;
|
|
}
|
|
};
|
|
|
|
function PerformanceDiff(current, old = null) {
|
|
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() {
|
|
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._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.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.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(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;
|
|
})
|
|
};
|