gecko/devtools/server/actors/performance.js

296 lines
9.4 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");
const protocol = require("devtools/server/protocol");
const { Task } = require("resource://gre/modules/Task.jsm");
const { Actor, custom, method, RetVal, Arg, Option, types, preEvent } = protocol;
const { actorBridge } = require("devtools/server/actors/common");
const { PerformanceRecordingActor, PerformanceRecordingFront } = require("devtools/server/actors/performance-recording");
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "extend", "sdk/util/object", true);
loader.lazyRequireGetter(this, "PerformanceRecorder",
"devtools/server/performance/recorder", true);
loader.lazyRequireGetter(this, "PerformanceIO",
"devtools/client/performance/modules/io");
loader.lazyRequireGetter(this, "normalizePerformanceFeatures",
"devtools/shared/performance/recording-utils", true);
loader.lazyRequireGetter(this, "LegacyPerformanceFront",
"devtools/client/performance/legacy/front", true);
loader.lazyRequireGetter(this, "getSystemInfo",
"devtools/shared/system", true);
const PIPE_TO_FRONT_EVENTS = new Set([
"recording-started", "recording-stopping", "recording-stopped",
"profiler-status", "timeline-data", "console-profile-start"
]);
const RECORDING_STATE_CHANGE_EVENTS = new Set([
"recording-started", "recording-stopping", "recording-stopped"
]);
/**
* This actor wraps the Performance module at devtools/shared/shared/performance.js
* and provides RDP definitions.
*
* @see devtools/shared/shared/performance.js for documentation.
*/
var PerformanceActor = exports.PerformanceActor = protocol.ActorClass({
typeName: "performance",
traits: {
features: {
withMarkers: true,
withTicks: true,
withMemory: true,
withFrames: true,
withGCEvents: true,
withDocLoadingEvents: true,
withAllocations: true,
withJITOptimizations: true,
},
},
/**
* The set of events the PerformanceActor emits over RDP.
*/
events: {
"recording-started": {
recording: Arg(0, "performance-recording"),
},
"recording-stopping": {
recording: Arg(0, "performance-recording"),
},
"recording-stopped": {
recording: Arg(0, "performance-recording"),
data: Arg(1, "json"),
},
"profiler-status": {
data: Arg(0, "json"),
},
"console-profile-start": {},
"timeline-data": {
name: Arg(0, "string"),
data: Arg(1, "json"),
recordings: Arg(2, "array:performance-recording"),
},
},
initialize: function (conn, tabActor) {
Actor.prototype.initialize.call(this, conn);
this._onRecorderEvent = this._onRecorderEvent.bind(this);
this.bridge = new PerformanceRecorder(conn, tabActor);
events.on(this.bridge, "*", this._onRecorderEvent);
},
/**
* `disconnect` method required to call destroy, since this
* actor is not managed by a parent actor.
*/
disconnect: function() {
this.destroy();
},
destroy: function () {
events.off(this.bridge, "*", this._onRecorderEvent);
this.bridge.destroy();
protocol.Actor.prototype.destroy.call(this);
},
connect: method(function (config) {
this.bridge.connect({ systemClient: config.systemClient });
return { traits: this.traits };
}, {
request: { options: Arg(0, "nullable:json") },
response: RetVal("json")
}),
canCurrentlyRecord: method(function() {
return this.bridge.canCurrentlyRecord();
}, {
response: { value: RetVal("json") }
}),
startRecording: method(Task.async(function *(options={}) {
if (!this.bridge.canCurrentlyRecord().success) {
return null;
}
let normalizedOptions = normalizePerformanceFeatures(options, this.traits.features);
let recording = yield this.bridge.startRecording(normalizedOptions);
this.manage(recording);
return recording;
}), {
request: {
options: Arg(0, "nullable:json"),
},
response: {
recording: RetVal("nullable:performance-recording")
}
}),
stopRecording: actorBridge("stopRecording", {
request: {
options: Arg(0, "performance-recording"),
},
response: {
recording: RetVal("performance-recording")
}
}),
isRecording: actorBridge("isRecording", {
response: { isRecording: RetVal("boolean") }
}),
getRecordings: actorBridge("getRecordings", {
response: { recordings: RetVal("array:performance-recording") }
}),
getConfiguration: actorBridge("getConfiguration", {
response: { config: RetVal("json") }
}),
setProfilerStatusInterval: actorBridge("setProfilerStatusInterval", {
request: { interval: Arg(0, "number") },
response: { oneway: true }
}),
/**
* Filter which events get piped to the front.
*/
_onRecorderEvent: function (eventName, ...data) {
// If this is a recording state change, call
// a method on the related PerformanceRecordingActor so it can
// update its internal state.
if (RECORDING_STATE_CHANGE_EVENTS.has(eventName)) {
let recording = data[0];
let extraData = data[1];
recording._setState(eventName, extraData);
}
if (PIPE_TO_FRONT_EVENTS.has(eventName)) {
events.emit(this, eventName, ...data);
}
},
});
exports.createPerformanceFront = function createPerformanceFront (target) {
// If we force legacy mode, or the server does not have a performance actor (< Fx42),
// use our LegacyPerformanceFront which will handle
// the communication over RDP to other underlying actors.
if (target.TEST_PERFORMANCE_LEGACY_FRONT || !target.form.performanceActor) {
return new LegacyPerformanceFront(target);
}
// If our server has a PerformanceActor implementation, set this
// up like a normal front.
return new PerformanceFront(target.client, target.form);
};
const PerformanceFront = exports.PerformanceFront = protocol.FrontClass(PerformanceActor, {
initialize: function (client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
this.actorID = form.performanceActor;
this.manage(this);
},
destroy: function () {
protocol.Front.prototype.destroy.call(this);
},
/**
* Conenct to the server, and handle once-off tasks like storing traits
* or system info.
*/
connect: custom(Task.async(function *() {
let systemClient = yield getSystemInfo();
let { traits } = yield this._connect({ systemClient });
this._traits = traits;
return this._traits;
}), {
impl: "_connect"
}),
get traits() {
if (!this._traits) {
Cu.reportError("Cannot access traits of PerformanceFront before calling `connect()`.");
}
return this._traits;
},
/**
* Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much
* of this recording's lifetime remains without being overwritten.
*
* @param {PerformanceRecording} recording
* @return {number?}
*/
getBufferUsageForRecording: function (recording) {
if (!recording.isRecording()) {
return void 0;
}
let { position: currentPosition, totalSize, generation: currentGeneration } = this._currentBufferStatus;
let { position: origPosition, generation: origGeneration } = recording.getStartingBufferStatus();
let normalizedCurrent = (totalSize * (currentGeneration - origGeneration)) + currentPosition;
let percent = (normalizedCurrent - origPosition) / totalSize;
// Clamp between 0 and 1; can get negative percentage values when a new
// recording starts and the currentBufferStatus has not yet been updated. Rather
// than fetching another status update, just clamp to 0, and this will be updated
// on the next profiler-status event.
return percent > 1 ? 1 : percent < 0 ? 0 : percent;
},
/**
* Loads a recording from a file.
*
* @param {nsILocalFile} file
* The file to import the data from.
* @return {Promise<PerformanceRecordingFront>}
*/
importRecording: function (file) {
return PerformanceIO.loadRecordingFromFile(file).then(recordingData => {
let model = new PerformanceRecordingFront();
model._imported = true;
model._label = recordingData.label || "";
model._duration = recordingData.duration;
model._markers = recordingData.markers;
model._frames = recordingData.frames;
model._memory = recordingData.memory;
model._ticks = recordingData.ticks;
model._allocations = recordingData.allocations;
model._profile = recordingData.profile;
model._configuration = recordingData.configuration || {};
model._systemHost = recordingData.systemHost;
model._systemClient = recordingData.systemClient;
return model;
});
},
/**
* Store profiler status when the position has been update so we can
* calculate recording's buffer percentage usage after emitting the event.
*/
_onProfilerStatus: preEvent("profiler-status", function (data) {
this._currentBufferStatus = data;
}),
/**
* For all PerformanceRecordings that are recording, and needing realtime markers,
* apply the timeline data to the front PerformanceRecording (so we only have one event
* for each timeline data chunk as they could be shared amongst several recordings).
*/
_onTimelineEvent: preEvent("timeline-data", function (type, data, recordings) {
for (let recording of recordings) {
recording._addTimelineData(type, data);
}
}),
});