gecko-dev/devtools/server/performance/recorder.js

537 строки
16 KiB
JavaScript

/* 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";
const { Cu } = require("chrome");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
loader.lazyRequireGetter(
this,
"Memory",
"devtools/server/performance/memory",
true
);
loader.lazyRequireGetter(
this,
"Timeline",
"devtools/server/performance/timeline",
true
);
loader.lazyRequireGetter(
this,
"Profiler",
"devtools/server/performance/profiler",
true
);
loader.lazyRequireGetter(
this,
"PerformanceRecordingActor",
"devtools/server/actors/performance-recording",
true
);
loader.lazyRequireGetter(
this,
"mapRecordingOptions",
"devtools/shared/performance/recording-utils",
true
);
loader.lazyRequireGetter(this, "getSystemInfo", "devtools/shared/system", true);
const PROFILER_EVENTS = [
"console-api-profiler",
"profiler-started",
"profiler-stopped",
"profiler-status",
];
// Max time in milliseconds for the allocations event to occur, which will
// occur on every GC, or at least as often as DRAIN_ALLOCATIONS_TIMEOUT.
const DRAIN_ALLOCATIONS_TIMEOUT = 2000;
/**
* A connection to underlying actors (profiler, memory, framerate, etc.)
* shared by all tools in a target.
*
* @param Target target
* The target owning this connection.
*/
function PerformanceRecorder(conn, targetActor) {
EventEmitter.decorate(this);
this.conn = conn;
this.targetActor = targetActor;
this._pendingConsoleRecordings = [];
this._recordings = [];
this._onTimelineData = this._onTimelineData.bind(this);
this._onProfilerEvent = this._onProfilerEvent.bind(this);
}
PerformanceRecorder.prototype = {
/**
* Initializes a connection to the profiler and other miscellaneous actors.
* If in the process of opening, or already open, nothing happens.
*
* @param {Object} options.systemClient
* Metadata about the client's system to attach to the recording models.
*
* @return object
* A promise that is resolved once the connection is established.
*/
connect: function(options) {
if (this._connected) {
return;
}
// Sets `this._profiler`, `this._timeline` and `this._memory`.
// Only initialize the timeline and memory fronts if the respective actors
// are available. Older Gecko versions don't have existing implementations,
// in which case all the methods we need can be easily mocked.
this._connectComponents();
this._registerListeners();
this._systemClient = options.systemClient;
this._connected = true;
},
/**
* Destroys this connection.
*/
destroy: function() {
this._unregisterListeners();
this._disconnectComponents();
this._connected = null;
this._profiler = null;
this._timeline = null;
this._memory = null;
this._target = null;
this._client = null;
},
/**
* Initializes fronts and connects to the underlying actors using the facades
* found in ./actors.js.
*/
_connectComponents: function() {
this._profiler = new Profiler(this.targetActor);
this._memory = new Memory(this.targetActor);
this._timeline = new Timeline(this.targetActor);
this._profiler.registerEventNotifications({ events: PROFILER_EVENTS });
},
/**
* Registers listeners on events from the underlying
* actors, so the connection can handle them.
*/
_registerListeners: function() {
this._timeline.on("*", this._onTimelineData);
this._memory.on("*", this._onTimelineData);
this._profiler.on("*", this._onProfilerEvent);
},
/**
* Unregisters listeners on events on the underlying actors.
*/
_unregisterListeners: function() {
this._timeline.off("*", this._onTimelineData);
this._memory.off("*", this._onTimelineData);
this._profiler.off("*", this._onProfilerEvent);
},
/**
* Closes the connections to non-profiler actors.
*/
_disconnectComponents: function() {
this._profiler.unregisterEventNotifications({ events: PROFILER_EVENTS });
this._profiler.destroy();
this._timeline.destroy();
this._memory.destroy();
},
_onProfilerEvent: function(topic, data) {
if (topic === "console-api-profiler") {
if (data.subject.action === "profile") {
this._onConsoleProfileStart(data.details);
} else if (data.subject.action === "profileEnd") {
this._onConsoleProfileEnd(data.details);
}
} else if (topic === "profiler-stopped") {
this._onProfilerUnexpectedlyStopped();
} else if (topic === "profiler-status") {
this.emit("profiler-status", data);
}
},
/**
* Invoked whenever `console.profile` is called.
*
* @param string profileLabel
* The provided string argument if available; undefined otherwise.
* @param number currentTime
* The time (in milliseconds) when the call was made, relative to when
* the nsIProfiler module was started.
*/
async _onConsoleProfileStart({ profileLabel, currentTime }) {
const recordings = this._recordings;
// Abort if a profile with this label already exists.
if (recordings.find(e => e.getLabel() === profileLabel)) {
return;
}
// Immediately emit this so the client can start setting things up,
// expecting a recording very soon.
this.emit("console-profile-start");
await this.startRecording(
Object.assign({}, getPerformanceRecordingPrefs(), {
console: true,
label: profileLabel,
})
);
},
/**
* Invoked whenever `console.profileEnd` is called.
*
* @param string profileLabel
* The provided string argument if available; undefined otherwise.
* @param number currentTime
* The time (in milliseconds) when the call was made, relative to when
* the nsIProfiler module was started.
*/
async _onConsoleProfileEnd(data) {
// If no data, abort; can occur if profiler isn't running and we get a surprise
// call to console.profileEnd()
if (!data) {
return;
}
const { profileLabel } = data;
const pending = this._recordings.filter(
r => r.isConsole() && r.isRecording()
);
if (pending.length === 0) {
return;
}
let model;
// Try to find the corresponding `console.profile` call if
// a label was used in profileEnd(). If no matches, abort.
if (profileLabel) {
model = pending.find(e => e.getLabel() === profileLabel);
} else {
// If no label supplied, pop off the most recent pending console recording
model = pending[pending.length - 1];
}
// If `profileEnd()` was called with a label, and there are no matching
// sessions, abort.
if (!model) {
Cu.reportError(
"console.profileEnd() called with label that does not match a recording."
);
return;
}
await this.stopRecording(model);
},
/**
* TODO handle bug 1144438
*/
_onProfilerUnexpectedlyStopped: function() {
Cu.reportError("Profiler unexpectedly stopped.", arguments);
},
/**
* Called whenever there is timeline data of any of the following types:
* - markers
* - frames
* - memory
* - ticks
* - allocations
*/
_onTimelineData: function(eventName, ...data) {
let eventData = Object.create(null);
switch (eventName) {
case "markers": {
eventData = { markers: data[0], endTime: data[1] };
break;
}
case "ticks": {
eventData = { delta: data[0], timestamps: data[1] };
break;
}
case "memory": {
eventData = { delta: data[0], measurement: data[1] };
break;
}
case "frames": {
eventData = { delta: data[0], frames: data[1] };
break;
}
case "allocations": {
eventData = data[0];
break;
}
}
// Filter by only recordings that are currently recording;
// TODO should filter by recordings that have realtimeMarkers enabled.
const activeRecordings = this._recordings.filter(r => r.isRecording());
if (activeRecordings.length) {
this.emit("timeline-data", eventName, eventData, activeRecordings);
}
},
/**
* Checks whether or not recording is currently supported. At the moment,
* this is only influenced by private browsing mode and the profiler.
*/
canCurrentlyRecord: function() {
let success = true;
const reasons = [];
if (!Profiler.canProfile()) {
success = false;
reasons.push("profiler-unavailable");
}
// Check other factors that will affect the possibility of successfully
// starting a recording here.
return { success, reasons };
},
/**
* Begins a recording session
*
* @param boolean options.withMarkers
* @param boolean options.withTicks
* @param boolean options.withMemory
* @param boolean options.withAllocations
* @param boolean options.allocationsSampleProbability
* @param boolean options.allocationsMaxLogLength
* @param boolean options.bufferSize
* @param boolean options.sampleFrequency
* @param boolean options.console
* @param string options.label
* @param boolean options.realtimeMarkers
* @return object
* A promise that is resolved once recording has started.
*/
async startRecording(options) {
let timelineStart, memoryStart;
const profilerStart = async function() {
const data = await this._profiler.isActive();
if (data.isActive) {
return data;
}
const startData = await this._profiler.start(
mapRecordingOptions("profiler", options)
);
// If no current time is exposed from starting, set it to 0 -- this is an
// older Gecko that does not return its starting time, and uses an epoch based
// on the profiler's start time.
if (startData.currentTime == null) {
startData.currentTime = 0;
}
return startData;
}.bind(this)();
// Timeline will almost always be on if using the DevTools, but using component
// independently could result in no timeline.
if (options.withMarkers || options.withTicks || options.withMemory) {
timelineStart = this._timeline.start(
mapRecordingOptions("timeline", options)
);
}
if (options.withAllocations) {
if (this._memory.getState() === "detached") {
this._memory.attach();
}
const recordingOptions = Object.assign(
mapRecordingOptions("memory", options),
{
drainAllocationsTimeout: DRAIN_ALLOCATIONS_TIMEOUT,
}
);
memoryStart = this._memory.startRecordingAllocations(recordingOptions);
}
const [
profilerStartData,
timelineStartData,
memoryStartData,
] = await Promise.all([profilerStart, timelineStart, memoryStart]);
const data = Object.create(null);
// Filter out start times that are not actually used (0 or undefined), and
// find the earliest time since all sources use same epoch.
const startTimes = [
profilerStartData.currentTime,
memoryStartData,
timelineStartData,
].filter(Boolean);
data.startTime = Math.min(...startTimes);
data.position = profilerStartData.position;
data.generation = profilerStartData.generation;
data.totalSize = profilerStartData.totalSize;
data.systemClient = this._systemClient;
data.systemHost = await getSystemInfo();
const model = new PerformanceRecordingActor(this.conn, options, data);
this._recordings.push(model);
this.emit("recording-started", model);
return model;
},
/**
* Manually ends the recording session for the corresponding PerformanceRecording.
*
* @param PerformanceRecording model
* The corresponding PerformanceRecording that belongs to the recording
* session wished to stop.
* @return PerformanceRecording
* Returns the same model, populated with the profiling data.
*/
async stopRecording(model) {
// If model isn't in the Recorder's internal store,
// then do nothing, like if this was a console.profileEnd
// from a different target.
if (!this._recordings.includes(model)) {
return model;
}
// Flag the recording as no longer recording, so that `model.isRecording()`
// is false. Do this before we fetch all the data, and then subsequently
// the recording can be considered "completed".
this.emit("recording-stopping", model);
// Currently there are two ways profiles stop recording. Either manually in the
// performance tool, or via console.profileEnd. Once a recording is done,
// we want to deliver the model to the performance tool (either as a return
// from the PerformanceFront or via `console-profile-stop` event) and then
// remove it from the internal store.
//
// In the case where a console.profile is generated via the console (so the tools are
// open), we initialize the Performance tool so it can listen to those events.
this._recordings.splice(this._recordings.indexOf(model), 1);
const startTime = model._startTime;
const profilerData = this._profiler.getProfile({ startTime });
// Only if there are no more sessions recording do we stop
// the underlying memory and timeline actors. If we're still recording,
// juse use Date.now() for the memory and timeline end times, as those
// are only used in tests.
if (!this.isRecording()) {
// Check to see if memory is recording, so we only stop recording
// if necessary (otherwise if the memory component is not attached, this will fail)
if (this._memory.isRecordingAllocations()) {
this._memory.stopRecordingAllocations();
}
this._timeline.stop();
}
const recordingData = {
// Data available only at the end of a recording.
profile: profilerData.profile,
// End times for all the actors.
duration: profilerData.currentTime - startTime,
};
this.emit("recording-stopped", model, recordingData);
return model;
},
/**
* Checks all currently stored recording handles and returns a boolean
* if there is a session currently being recorded.
*
* @return Boolean
*/
isRecording: function() {
return this._recordings.some(h => h.isRecording());
},
/**
* Returns all current recordings.
*/
getRecordings: function() {
return this._recordings;
},
/**
* Sets how often the "profiler-status" event should be emitted.
* Used in tests.
*/
setProfilerStatusInterval: function(n) {
this._profiler.setProfilerStatusInterval(n);
},
/**
* Returns the configurations set on underlying components, used in tests.
* Returns an object with `probability`, `maxLogLength` for allocations, and
* `features`, `threadFilters`, `entries` and `interval` for profiler.
*
* @return {object}
*/
getConfiguration: function() {
let allocationSettings = Object.create(null);
if (this._memory.getState() === "attached") {
allocationSettings = this._memory.getAllocationsSettings();
}
return Object.assign(
{},
allocationSettings,
this._profiler.getStartOptions()
);
},
toString: () => "[object PerformanceRecorder]",
};
/**
* Creates an object of configurations based off of
* preferences for a PerformanceRecording.
*/
function getPerformanceRecordingPrefs() {
return {
withMarkers: true,
withMemory: Services.prefs.getBoolPref(
"devtools.performance.ui.enable-memory"
),
withTicks: Services.prefs.getBoolPref(
"devtools.performance.ui.enable-framerate"
),
withAllocations: Services.prefs.getBoolPref(
"devtools.performance.ui.enable-allocations"
),
allocationsSampleProbability: +Services.prefs.getCharPref(
"devtools.performance.memory.sample-probability"
),
allocationsMaxLogLength: Services.prefs.getIntPref(
"devtools.performance.memory.max-log-length"
),
};
}
exports.PerformanceRecorder = PerformanceRecorder;