зеркало из https://github.com/mozilla/gecko-dev.git
1001 строка
29 KiB
JavaScript
1001 строка
29 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 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;
|
|
})
|
|
};
|