Bug 1159389 - Migrate profiler actor to use new form of protocol.js actors, and become a standalone module consumed by actors. r=vp, r=jryans

This commit is contained in:
Jordan Santell 2015-07-13 13:20:00 -04:00
parent 92df0a10fe
commit d1d060018e
23 changed files with 762 additions and 575 deletions

View File

@ -19,6 +19,8 @@ loader.lazyRequireGetter(this, "TimelineFront",
"devtools/server/actors/timeline", true);
loader.lazyRequireGetter(this, "MemoryFront",
"devtools/server/actors/memory", true);
loader.lazyRequireGetter(this, "ProfilerFront",
"devtools/server/actors/profiler", true);
// how often do we pull allocation sites from the memory actor
const ALLOCATION_SITE_POLL_TIMER = 200; // ms
@ -58,7 +60,7 @@ ProfilerFrontFacade.prototype = {
// Connects to the targets underlying real ProfilerFront.
connect: Task.async(function*() {
let target = this._target;
this._actor = yield CompatUtils.getProfiler(target);
this._front = new ProfilerFront(target.client, target.form);
// Fetch and store information about the SPS profiler and
// server profiler.
@ -68,8 +70,7 @@ ProfilerFrontFacade.prototype = {
// Directly register to event notifications when connected
// to hook into `console.profile|profileEnd` calls.
yield this.registerEventNotifications({ events: this.EVENTS });
// TODO bug 1159389, listen directly to actor if supporting new front
target.client.addListener("eventNotification", this._onProfilerEvent);
this.EVENTS.forEach(e => this._front.on(e, this._onProfilerEvent));
}),
/**
@ -79,9 +80,10 @@ ProfilerFrontFacade.prototype = {
if (this._poller) {
yield this._poller.destroy();
}
this.EVENTS.forEach(e => this._front.off(e, this._onProfilerEvent));
yield this.unregisterEventNotifications({ events: this.EVENTS });
// TODO bug 1159389, listen directly to actor if supporting new front
this._target.client.removeListener("eventNotification", this._onProfilerEvent);
yield this._front.destroy();
}),
/**
@ -105,7 +107,14 @@ ProfilerFrontFacade.prototype = {
// nsIPerformance module will be kept recording, because it's the same instance
// for all targets and interacts with the whole platform, so we don't want
// to affect other clients by stopping (or restarting) it.
let { isActive, currentTime, position, generation, totalSize } = yield this.getStatus();
let status = yield this.getStatus();
// This should only occur during teardown
if (!status) {
return;
}
let { isActive, currentTime, position, generation, totalSize } = status;
if (isActive) {
this.emit("profiler-already-active");
@ -121,7 +130,7 @@ ProfilerFrontFacade.prototype = {
let startInfo = yield this.startProfiler(profilerOptions);
let startTime = 0;
if ('currentTime' in startInfo) {
if ("currentTime" in startInfo) {
startTime = startInfo.currentTime;
}
@ -142,7 +151,7 @@ ProfilerFrontFacade.prototype = {
* Wrapper around `profiler.isActive()` to take profiler status data and emit.
*/
getStatus: Task.async(function *() {
let data = yield (CompatUtils.actorCompatibilityBridge("isActive").call(this));
let data = yield CompatUtils.callFrontMethod("isActive").call(this);
// If no data, the last poll for `isActive()` was wrapping up, and the target.client
// is now null, so we no longer have data, so just abort here.
if (!data) {
@ -169,7 +178,7 @@ ProfilerFrontFacade.prototype = {
* Returns profile data from now since `startTime`.
*/
getProfile: Task.async(function *(options) {
let profilerData = yield (CompatUtils.actorCompatibilityBridge("getProfile").call(this, options));
let profilerData = yield CompatUtils.callFrontMethod("getProfile").call(this, options);
// If the backend is not deduped, dedupe it ourselves, as rest of the code
// expects a deduped profile.
if (profilerData.profile.meta.version === 2) {
@ -191,7 +200,9 @@ ProfilerFrontFacade.prototype = {
* @param object response
* The data received from the backend.
*/
_onProfilerEvent: function (_, { topic, subject, details }) {
_onProfilerEvent: function (data) {
let { subject, topic, details } = data;
if (topic === "console-api-profiler") {
if (subject.action === "profile") {
this.emit("console-profile-start", details);
@ -224,7 +235,7 @@ TimelineFrontFacade.prototype = {
connect: Task.async(function*() {
let supported = yield CompatUtils.timelineActorSupported(this._target);
this._actor = supported ?
this._front = supported ?
new TimelineFront(this._target.client, this._target.form) :
new CompatUtils.MockTimelineFront();
@ -234,7 +245,7 @@ TimelineFrontFacade.prototype = {
// exposed event.
this.EVENTS.forEach(type => {
let handler = this[`_on${type}`] = this._onTimelineData.bind(this, type);
this._actor.on(type, handler);
this._front.on(type, handler);
});
}),
@ -243,8 +254,8 @@ TimelineFrontFacade.prototype = {
* destroying the underlying actor.
*/
destroy: Task.async(function *() {
this.EVENTS.forEach(type => this._actor.off(type, this[`_on${type}`]));
yield this._actor.destroy();
this.EVENTS.forEach(type => this._front.off(type, this[`_on${type}`]));
yield this._front.destroy();
}),
/**
@ -271,7 +282,7 @@ function MemoryFrontFacade (target) {
MemoryFrontFacade.prototype = {
connect: Task.async(function*() {
let supported = yield CompatUtils.memoryActorSupported(this._target);
this._actor = supported ?
this._front = supported ?
new MemoryFront(this._target.client, this._target.form) :
new CompatUtils.MockMemoryFront();
@ -285,7 +296,7 @@ MemoryFrontFacade.prototype = {
if (this._poller) {
yield this._poller.destroy();
}
yield this._actor.destroy();
yield this._front.destroy();
}),
/**
@ -374,9 +385,9 @@ MemoryFrontFacade.prototype = {
};
// Bind all the methods that directly proxy to the actor
PROFILER_ACTOR_METHODS.forEach(m => ProfilerFrontFacade.prototype[m] = CompatUtils.actorCompatibilityBridge(m));
TIMELINE_ACTOR_METHODS.forEach(m => TimelineFrontFacade.prototype[m] = CompatUtils.actorCompatibilityBridge(m));
MEMORY_ACTOR_METHODS.forEach(m => MemoryFrontFacade.prototype[m] = CompatUtils.actorCompatibilityBridge(m));
PROFILER_ACTOR_METHODS.forEach(m => ProfilerFrontFacade.prototype[m] = CompatUtils.callFrontMethod(m));
TIMELINE_ACTOR_METHODS.forEach(m => TimelineFrontFacade.prototype[m] = CompatUtils.callFrontMethod(m));
MEMORY_ACTOR_METHODS.forEach(m => MemoryFrontFacade.prototype[m] = CompatUtils.callFrontMethod(m));
exports.ProfilerFront = ProfilerFrontFacade;
exports.TimelineFront = TimelineFrontFacade;

View File

@ -101,51 +101,10 @@ function timelineActorSupported(target) {
}
/**
* Returns a promise resolving to the location of the profiler actor
* within this context.
*
* @param {TabTarget} target
* @return {Promise<ProfilerActor>}
* Returns a function to be used as a method on an "Front" in ./actors.
* Calls the underlying actor's method.
*/
function getProfiler (target) {
let deferred = promise.defer();
// Chrome and content process targets already have obtained a reference
// to the profiler tab actor. Use it immediately.
if (target.form && target.form.profilerActor) {
deferred.resolve(target.form.profilerActor);
}
// Check if we already have a grip to the `listTabs` response object
// and, if we do, use it to get to the profiler actor.
else if (target.root && target.root.profilerActor) {
deferred.resolve(target.root.profilerActor);
}
// Otherwise, call `listTabs`.
else {
target.client.listTabs(({ profilerActor }) => deferred.resolve(profilerActor));
}
return deferred.promise;
}
/**
* Makes a request to an actor that does not have the modern `Front`
* interface.
*/
function legacyRequest (target, actor, method, args) {
let deferred = promise.defer();
let data = args[0] || {};
data.to = actor;
data.type = method;
target.client.request(data, deferred.resolve);
return deferred.promise;
}
/**
* Returns a function to be used as a method on an "Actor" in ./actors.
* Calls the underlying actor's method, supporting the modern `Front`
* interface if possible, otherwise, falling back to using
* `legacyRequest`.
*/
function actorCompatibilityBridge (method) {
function callFrontMethod (method) {
return function () {
// If there's no target or client on this actor facade,
// abort silently -- this occurs in tests when polling occurs
@ -154,19 +113,7 @@ function actorCompatibilityBridge (method) {
if (!this._target || !this._target.client) {
return;
}
// Check to see if this is a modern ActorFront, which has its
// own `request` method. Also, check if its a mock actor, as it mimicks
// the ActorFront interface.
// The profiler actor does not currently support the modern `Front`
// interface, so we have to manually push packets to it.
// TODO bug 1159389, fix up profiler actor to not need this, however
// we will need it for backwards compat
if (this.IS_MOCK || this._actor.request) {
return this._actor[method].apply(this._actor, arguments);
}
else {
return legacyRequest(this._target, this._actor, method, arguments);
}
return this._front[method].apply(this._front, arguments);
};
}
@ -174,5 +121,4 @@ exports.MockMemoryFront = MockMemoryFront;
exports.MockTimelineFront = MockTimelineFront;
exports.memoryActorSupported = memoryActorSupported;
exports.timelineActorSupported = timelineActorSupported;
exports.getProfiler = getProfiler;
exports.actorCompatibilityBridge = actorCompatibilityBridge;
exports.callFrontMethod = callFrontMethod;

View File

@ -45,6 +45,10 @@ function* spawnTest () {
ok(true, "Got expected cycle collection events");
yield front.stopRecording();
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -35,6 +35,9 @@ function* spawnTest () {
ok(markers.every(({causeName}) => typeof causeName === "string"),
"All markers have a causeName.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();

View File

@ -25,6 +25,9 @@ function* spawnTest () {
ok(markers.every(({name}) => name === "Parse HTML"), "All markers found are Parse HTML markers");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();

View File

@ -27,6 +27,9 @@ function* spawnTest () {
ok(markers.some(({restyleHint}) => restyleHint != void 0), "some markers have a restyleHint property");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();

View File

@ -36,6 +36,9 @@ function* spawnTest () {
is(markers[0].causeName, void 0, "Unlabeled timestamps have an empty causeName");
is(markers[1].causeName, "myLabel", "Labeled timestamps have correct causeName");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();

View File

@ -45,6 +45,9 @@ function* spawnTest() {
ok(recording.getDuration() >= 0,
"The profilerEndTime is after profilerStartTime.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -44,6 +44,9 @@ function* spawnTest() {
ok(recording.getDuration() >= 0,
"The profilerEndTime is after profilerStartTime.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -27,6 +27,9 @@ function* spawnTest() {
yield front.stopRecording(model);
is(model.getBufferUsage(), null, "after recording, model should still have `null` for its buffer usage");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -37,8 +37,6 @@ function* spawnTest() {
let secondRecordingProfile = secondRecording.getProfile();
let secondRecordingSamples = secondRecordingProfile.threads[0].samples.data;
isnot(secondRecording._profilerStartTime, 0,
"The profiling start time should not be 0 on the second recording.");
ok(secondRecording.getDuration() >= WAIT_TIME,
"The second recording duration is correct.");

View File

@ -47,6 +47,9 @@ function* spawnTest() {
is((yield front._request("memory", "getState")), "detached",
"Memory actor is detached when stopping recording with allocations.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -30,6 +30,9 @@ function* spawnTest() {
ok(timelineStart1 < timelineStart2, "can start timeline actor twice and get different start times");
ok(memoryStart1 < memoryStart2, "can start memory actor twice and get different start times");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -27,6 +27,9 @@ function* spawnTest() {
ok(stopModel.getProfile(), "recording model has a profile after stopping.");
ok(stopModel.getDuration(), "recording model has a duration after stopping.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -34,6 +34,9 @@ function* spawnTest() {
is(counters.memory.length, 3, "three memory events fired.");
is(counters.ticks.length, 3, "three ticks events fired.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();

View File

@ -38,6 +38,9 @@ function* spawnTest() {
is(model.getBufferUsage(), null, "getBufferUsage() should be null when no longer recording.");
// Destroy the front before removing tab to ensure no
// lingering requests
yield front.destroy();
yield removeTab(target.tab);
finish();
}

View File

@ -1,355 +1,222 @@
/* 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 {Cc, Ci, Cu, Cr} = require("chrome");
const Services = require("Services");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
const { Cu } = require("chrome");
const protocol = require("devtools/server/protocol");
const { custom, method, RetVal, Arg, Option, types } = protocol;
const { Profiler } = require("devtools/toolkit/shared/profiler");
const { actorBridge } = require("devtools/server/actors/common");
let DEFAULT_PROFILER_OPTIONS = {
// When using the DevTools Performance Tools, this will be overridden
// by the pref `devtools.performance.profiler.buffer-size`.
entries: Math.pow(10, 7),
// When using the DevTools Performance Tools, this will be overridden
// by the pref `devtools.performance.profiler.sample-rate-khz`.
interval: 1,
features: ["js"],
threadFilters: ["GeckoMain"]
};
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "extend", "sdk/util/object", true);
/**
* The nsIProfiler is target agnostic and interacts with the whole platform.
* Therefore, special care needs to be given to make sure different actor
* consumers (i.e. "toolboxes") don't interfere with each other.
*/
let gProfilerConsumers = 0;
loader.lazyGetter(this, "nsIProfilerModule", () => {
return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
types.addType("profiler-data", {
// On Fx42+, the profile is only deserialized on the front; older
// servers will get the profiler data as an object from nsIProfiler,
// causing one parse/stringify cycle, then again implicitly in a packet.
read: (v) => {
if (typeof v.profile === "string") {
// Create a new response object since `profile` is read only.
let newValue = Object.create(null);
newValue.profile = JSON.parse(v.profile);
newValue.currentTime = v.currentTime;
return newValue;
}
return v;
}
});
/**
* The profiler actor provides remote access to the built-in nsIProfiler module.
* This actor wraps the Profiler module at toolkit/devtools/shared/profiler.js
* and provides RDP definitions.
*
* @see toolkit/devtools/shared/profiler.js for documentation.
*/
function ProfilerActor() {
gProfilerConsumers++;
this._observedEvents = new Set();
}
let ProfilerActor = exports.ProfilerActor = protocol.ActorClass({
typeName: "profiler",
ProfilerActor.prototype = {
actorPrefix: "profiler",
/**
* The set of events the ProfilerActor emits over RDP.
*/
events: {
"console-api-profiler": {
data: Arg(0, "json"),
},
"profiler-started": {
data: Arg(0, "json"),
},
"profiler-stopped": {
data: Arg(0, "json"),
},
// Only for older geckos, pre-protocol.js ProfilerActor (<Fx42).
// Emitted on other events as a transition from older profiler events
// to newer ones.
"eventNotification": {
subject: Option(0, "json"),
topic: Option(0, "string"),
details: Option(0, "json")
}
},
initialize: function (conn) {
protocol.Actor.prototype.initialize.call(this, conn);
this._onProfilerEvent = this._onProfilerEvent.bind(this);
this.bridge = new Profiler();
events.on(this.bridge, "*", this._onProfilerEvent);
},
/**
* `disconnect` method required to call destroy, since this
* actor is not managed by a parent actor.
*/
disconnect: function() {
for (let event of this._observedEvents) {
Services.obs.removeObserver(this, event);
this.destroy();
},
destroy: function() {
events.off(this.bridge, "*", this._onProfilerEvent);
this.bridge.destroy();
protocol.Actor.prototype.destroy.call(this);
},
startProfiler: actorBridge("start", {
// Write out every property in the request, since we want all these options to be
// on the packet's top-level for backwards compatibility, when the profiler actor
// was not using protocol.js (<Fx42)
request: {
entries: Option(0, "nullable:number"),
interval: Option(0, "nullable:number"),
features: Option(0, "nullable:array:string"),
threadFilters: Option(0, "nullable:array:string"),
},
response: RetVal("json"),
}),
stopProfiler: actorBridge("stop", {
response: RetVal("json"),
}),
getProfile: actorBridge("getProfile", {
request: {
startTime: Option(0, "nullable:number"),
stringify: Option(0, "nullable:boolean")
},
response: RetVal("profiler-data")
}),
getFeatures: actorBridge("getFeatures", {
response: RetVal("json")
}),
getBufferInfo: actorBridge("getBufferInfo", {
response: RetVal("json")
}),
getStartOptions: actorBridge("getStartOptions", {
response: RetVal("json")
}),
isActive: actorBridge("isActive", {
response: RetVal("json")
}),
getSharedLibraryInformation: actorBridge("getSharedLibraryInformation", {
response: RetVal("json")
}),
registerEventNotifications: actorBridge("registerEventNotifications", {
// Explicitly enumerate the arguments
// @see ProfilerActor#startProfiler
request: {
events: Option(0, "nullable:array:string"),
},
response: RetVal("json")
}),
unregisterEventNotifications: actorBridge("unregisterEventNotifications", {
// Explicitly enumerate the arguments
// @see ProfilerActor#startProfiler
request: {
events: Option(0, "nullable:array:string"),
},
response: RetVal("json")
}),
/**
* Pipe events from Profiler module to this actor.
*/
_onProfilerEvent: function (eventName, ...data) {
events.emit(this, eventName, ...data);
},
});
/**
* This can be used on older Profiler implementations, but the methods cannot
* be changed -- you must introduce a new method, and detect the server.
*/
exports.ProfilerFront = protocol.FrontClass(ProfilerActor, {
initialize: function(client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
this.actorID = form.profilerActor;
this.manage(this);
this._onProfilerEvent = this._onProfilerEvent.bind(this);
events.on(this, "*", this._onProfilerEvent);
},
destroy: function () {
events.off(this, "*", this._onProfilerEvent);
protocol.Front.prototype.destroy.call(this);
},
/**
* If using the protocol.js Fronts, then make stringify default,
* since the read/write mechanisms will expose it as an object anyway, but
* this lets other consumers who connect directly (xpcshell tests, Gecko Profiler) to
* have unchanged behaviour.
*/
getProfile: custom(function (options) {
return this._getProfile(extend({ stringify: true }, options));
}, {
impl: "_getProfile"
}),
/**
* Also emit an old `eventNotification` for older consumers of the profiler.
*/
_onProfilerEvent: function (eventName, data) {
// If this event already passed through once, don't repropagate
if (data.relayed) {
return;
}
this._observedEvents = null;
this.onStopProfiler();
data.relayed = true;
gProfilerConsumers--;
checkProfilerConsumers();
},
/**
* Returns an array of feature strings, describing the profiler features
* that are available on this platform. Can be called while the profiler
* is stopped.
*/
onGetFeatures: function() {
return { features: nsIProfilerModule.GetFeatures([]) };
},
/**
* Returns an object with the values of the current status of the
* circular buffer in the profiler, returning `position`, `totalSize`,
* and the current `generation` of the buffer.
*/
onGetBufferInfo: function() {
let position = {}, totalSize = {}, generation = {};
nsIProfilerModule.GetBufferInfo(position, totalSize, generation);
return {
position: position.value,
totalSize: totalSize.value,
generation: generation.value
// If this is `eventNotification`, this is coming from an older Gecko (<Fx42)
// that doesn't use protocol.js style events. Massage it to emit a protocol.js
// style event as well.
if (eventName === "eventNotification") {
events.emit(this, data.topic, data);
}
},
/**
* Returns the configuration used that was originally passed in to start up the
* profiler. Used for tests, and does not account for others using nsIProfiler.
*/
onGetStartOptions: function() {
return this._profilerStartOptions || {};
},
/**
* Starts the nsIProfiler module. Doing so will discard any samples
* that might have been accumulated so far.
*
* @param number entries [optional]
* @param number interval [optional]
* @param array:string features [optional]
* @param array:string threadFilters [description]
*/
onStartProfiler: function(request = {}) {
let options = this._profilerStartOptions = {
entries: request.entries || DEFAULT_PROFILER_OPTIONS.entries,
interval: request.interval || DEFAULT_PROFILER_OPTIONS.interval,
features: request.features || DEFAULT_PROFILER_OPTIONS.features,
threadFilters: request.threadFilters || DEFAULT_PROFILER_OPTIONS.threadFilters,
};
// The start time should be before any samples we might be
// interested in.
let currentTime = nsIProfilerModule.getElapsedTime();
nsIProfilerModule.StartProfiler(
options.entries,
options.interval,
options.features,
options.features.length,
options.threadFilters,
options.threadFilters.length
);
let { position, totalSize, generation } = this.onGetBufferInfo();
return { started: true, position, totalSize, generation, currentTime };
},
/**
* Stops the nsIProfiler module, if no other client is using it.
*/
onStopProfiler: function() {
// Actually stop the profiler only if the last client has stopped profiling.
// Since this is a root actor, and the profiler module interacts with the
// whole platform, we need to avoid a case in which the profiler is stopped
// when there might be other clients still profiling.
if (gProfilerConsumers == 1) {
nsIProfilerModule.StopProfiler();
}
return { started: false };
},
/**
* Verifies whether or not the nsIProfiler module has started.
* If already active, the current time is also returned.
*/
onIsActive: function() {
let isActive = nsIProfilerModule.IsActive();
let elapsedTime = isActive ? nsIProfilerModule.getElapsedTime() : undefined;
let { position, totalSize, generation } = this.onGetBufferInfo();
return { isActive: isActive, currentTime: elapsedTime, position, totalSize, generation };
},
/**
* Returns a stringified JSON object that describes the shared libraries
* which are currently loaded into our process. Can be called while the
* profiler is stopped.
*/
onGetSharedLibraryInformation: function() {
return { sharedLibraryInformation: nsIProfilerModule.getSharedLibraryInformation() };
},
/**
* Returns all the samples accumulated since the profiler was started,
* along with the current time. The data has the following format:
* {
* libs: string,
* meta: {
* interval: number,
* platform: string,
* ...
* },
* threads: [{
* samples: [{
* frames: [{
* line: number,
* location: string,
* category: number
* } ... ],
* name: string
* responsiveness: number
* time: number
* } ... ]
* } ... ]
* }
*
*
* @param number startTime
* Since the circular buffer will only grow as long as the profiler lives,
* the buffer can contain unwanted samples. Pass in a `startTime` to only retrieve
* samples that took place after the `startTime`, with 0 being when the profiler
* just started.
*/
onGetProfile: function(request) {
let startTime = request.startTime || 0;
let profile = nsIProfilerModule.getProfileData(startTime);
return { profile: profile, currentTime: nsIProfilerModule.getElapsedTime() };
},
/**
* Registers for certain event notifications.
* Currently supported events:
* - "console-api-profiler"
* - "profiler-started"
* - "profiler-stopped"
*/
onRegisterEventNotifications: function(request) {
let response = [];
for (let event of request.events) {
if (this._observedEvents.has(event)) {
continue;
}
Services.obs.addObserver(this, event, false);
this._observedEvents.add(event);
response.push(event);
}
return { registered: response };
},
/**
* Unregisters from certain event notifications.
* Currently supported events:
* - "console-api-profiler"
* - "profiler-started"
* - "profiler-stopped"
*/
onUnregisterEventNotifications: function(request) {
let response = [];
for (let event of request.events) {
if (!this._observedEvents.has(event)) {
continue;
}
Services.obs.removeObserver(this, event);
this._observedEvents.delete(event);
response.push(event);
}
return { unregistered: response };
},
/**
* Callback for all observed notifications.
* @param object subject
* @param string topic
* @param object data
*/
observe: DevToolsUtils.makeInfallible(function(subject, topic, data) {
// Create JSON objects suitable for transportation across the RDP,
// by breaking cycles and making a copy of the `subject` and `data` via
// JSON.stringifying those values with a replacer that omits properties
// known to introduce cycles, and then JSON.parsing the result.
// This spends some CPU cycles, but it's simple.
subject = (subject && !Cu.isXrayWrapper(subject) && subject.wrappedJSObject) || subject;
subject = JSON.parse(JSON.stringify(subject, cycleBreaker));
data = (data && !Cu.isXrayWrapper(data) && data.wrappedJSObject) || data;
data = JSON.parse(JSON.stringify(data, cycleBreaker));
// Sends actor, type and other additional information over the remote
// debugging protocol to any profiler clients.
let reply = details => {
this.conn.send({
from: this.actorID,
type: "eventNotification",
subject: subject,
topic: topic,
data: data,
details: details
// Otherwise if a modern protocol.js event, emit it also as `eventNotification`
// for compatibility reasons on the client (like for any add-ons/Gecko Profiler using this
// event) and log a deprecation message if there is a listener.
else {
this.conn.emit("eventNotification", {
subject: data.subject,
topic: data.topic,
data: data.data,
details: data.details
});
};
switch (topic) {
case "console-api-profiler":
return void reply(this._handleConsoleEvent(subject, data));
case "profiler-started":
case "profiler-stopped":
default:
return void reply();
}
}, "ProfilerActor.prototype.observe"),
/**
* Handles `console.profile` and `console.profileEnd` invocations and
* creates an appropriate response sent over the protocol.
* @param object subject
* @param object data
* @return object
*/
_handleConsoleEvent: function(subject, data) {
// An optional label may be specified when calling `console.profile`.
// If that's the case, stringify it and send it over with the response.
let { action, arguments: args } = subject;
let profileLabel = args.length > 0 ? args[0] + "" : undefined;
// If the event was generated from `console.profile` or `console.profileEnd`
// we need to start the profiler right away and then just notify the client.
// Otherwise, we'll lose precious samples.
if (action === "profile" || action === "profileEnd") {
let { isActive, currentTime } = this.onIsActive();
// Start the profiler only if it wasn't already active. Otherwise, any
// samples that might have been accumulated so far will be discarded.
if (!isActive && action === "profile") {
this.onStartProfiler();
return {
profileLabel: profileLabel,
currentTime: 0
};
if (this.conn._getListeners("eventNotification").length) {
Cu.reportError(`
ProfilerActor's "eventNotification" on the DebuggerClient has been deprecated.
Use the ProfilerFront found in "devtools/server/actors/profiler".`);
}
// Otherwise, if inactive and a call to profile end, send
// an empty object because we can't do anything with this.
else if (!isActive) {
return {};
}
// Otherwise, the profiler is already active, so just send
// to the front the current time, label, and the notification
// adds the action as well.
return {
profileLabel: profileLabel,
currentTime: currentTime
};
}
}
};
exports.ProfilerActor = ProfilerActor;
/**
* JSON.stringify callback used in ProfilerActor.prototype.observe.
*/
function cycleBreaker(key, value) {
if (key == "wrappedJSObject") {
return undefined;
}
return value;
}
/**
* Asserts the value sanity of `gProfilerConsumers`.
*/
function checkProfilerConsumers() {
if (gProfilerConsumers < 0) {
let msg = "Somehow the number of started profilers is now negative.";
DevToolsUtils.reportException("ProfilerActor", msg);
}
}
/**
* The request types this actor can handle.
* At the moment there are two known users of the Profiler actor:
* the devtools and the Gecko Profiler addon, which uses the debugger
* protocol to get profiles from Fennec.
*/
ProfilerActor.prototype.requestTypes = {
"getBufferInfo": ProfilerActor.prototype.onGetBufferInfo,
"getFeatures": ProfilerActor.prototype.onGetFeatures,
"startProfiler": ProfilerActor.prototype.onStartProfiler,
"stopProfiler": ProfilerActor.prototype.onStopProfiler,
"isActive": ProfilerActor.prototype.onIsActive,
"getSharedLibraryInformation": ProfilerActor.prototype.onGetSharedLibraryInformation,
"getProfile": ProfilerActor.prototype.onGetProfile,
"registerEventNotifications": ProfilerActor.prototype.onRegisterEventNotifications,
"unregisterEventNotifications": ProfilerActor.prototype.onUnregisterEventNotifications,
"getStartOptions": ProfilerActor.prototype.onGetStartOptions
};
},
});

View File

@ -171,7 +171,7 @@ RootActor.prototype = {
},
// Whether or not `getProfile()` supports specifying a `startTime`
// and `endTime` to filter out samples. Fx40+
profilerDataFilterable: true
profilerDataFilterable: true,
},
/**

View File

@ -8,99 +8,55 @@
*/
const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
const { ProfilerFront } = devtools.require("devtools/server/actors/profiler");
function run_test()
{
get_chrome_actors((client, form) => {
let actor = form.profilerActor;
activate_profiler(client, actor, () => {
test_events(client, actor, () => {
client.close(do_test_finished);
});
});
})
do_test_pending();
function run_test() {
run_next_test();
}
function activate_profiler(client, actor, callback)
{
client.request({ to: actor, type: "startProfiler" }, response => {
do_check_true(response.started);
client.request({ to: actor, type: "isActive" }, response => {
do_check_true(response.isActive);
callback();
});
});
}
add_task(function *() {
let [client, form] = yield getChromeActors();
let front = new ProfilerFront(client, form);
function register_events(client, actor, events, callback)
{
client.request({
to: actor,
type: "registerEventNotifications",
events: events
}, callback);
}
function unregister_events(client, actor, events, callback)
{
client.request({
to: actor,
type: "unregisterEventNotifications",
events: events
}, callback);
}
function emit_and_wait_for_event(client, subject, topic, data, callback)
{
let events = [0, 0, 0, 0];
front.on("console-api-profiler", () => events[0]++);
front.on("profiler-started", () => events[1]++);
front.on("profiler-stopped", () => events[2]++);
client.addListener("eventNotification", (type, response) => {
do_check_eq(type, "eventNotification");
do_check_eq(response.topic, topic);
do_check_eq(typeof response.subject, "object");
delete subject.wrappedJSObject;
do_check_eq(JSON.stringify(response.subject), JSON.stringify(subject));
do_check_eq(response.data, data);
callback();
do_check_true(type === "eventNotification");
events[3]++;
});
// Make sure cyclic objects are handled before sending them over the protocol.
// See ProfilerActor.prototype.observe for more information.
subject.wrappedJSObject = subject;
Services.obs.notifyObservers(subject, topic, data);
}
function test_events(client, actor, callback)
{
register_events(client, actor, ["foo", "bar"], response => {
do_check_eq(typeof response.registered, "object");
do_check_eq(response.registered.length, 2);
do_check_eq(response.registered[0], "foo");
do_check_eq(response.registered[1], "bar");
register_events(client, actor, ["foo"], response => {
do_check_eq(typeof response.registered, "object");
do_check_eq(response.registered.length, 0);
emit_and_wait_for_event(client, { hello: "world" }, "foo", "bar", () => {
unregister_events(client, actor, ["foo", "bar", "baz"], response => {
do_check_eq(typeof response.unregistered, "object");
do_check_eq(response.unregistered.length, 2);
do_check_eq(response.unregistered[0], "foo");
do_check_eq(response.unregistered[1], "bar");
// All events being now unregistered, sending an event shouldn't
// do anything. If it does, the eventNotification listeners added
// above will catch the event and fail on the data.event test.
Services.obs.notifyObservers(null, "foo", null);
Services.obs.notifyObservers(null, "bar", null);
callback();
});
});
});
});
yield front.startProfiler();
yield front.stopProfiler();
// All should be empty without binding events
do_check_true(events[0] === 0);
do_check_true(events[1] === 0);
do_check_true(events[2] === 0);
do_check_true(events[3] === 0);
let ret = yield front.registerEventNotifications({ events: ["console-api-profiler", "profiler-started", "profiler-stopped"] });
do_check_true(ret.registered.length === 3);
yield front.startProfiler();
do_check_true(events[0] === 0);
do_check_true(events[1] === 1);
do_check_true(events[2] === 0);
do_check_true(events[3] === 1, "compatibility events supported for eventNotifications");
yield front.stopProfiler();
do_check_true(events[0] === 0);
do_check_true(events[1] === 1);
do_check_true(events[2] === 1);
do_check_true(events[3] === 2, "compatibility events supported for eventNotifications");
ret = yield front.unregisterEventNotifications({ events: ["console-api-profiler", "profiler-started", "profiler-stopped"] });
do_check_true(ret.registered.length === 3);
});
function getChromeActors () {
let deferred = promise.defer();
get_chrome_actors((client, form) => deferred.resolve([client, form]));
return deferred.promise;
}

View File

@ -1,69 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests the event notification service for the profiler actor, specifically
* for when the profiler is started and stopped.
*/
const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
function run_test()
{
get_chrome_actors((client, form) => {
let actor = form.profilerActor;
activate_profiler(client, actor, () => {
test_events(client, actor, () => {
client.close(do_test_finished);
});
});
})
do_test_pending();
}
function activate_profiler(client, actor, callback)
{
client.request({ to: actor, type: "startProfiler" }, response => {
do_check_true(response.started);
client.request({ to: actor, type: "isActive" }, response => {
do_check_true(response.isActive);
callback();
});
});
}
function register_events(client, actor, events, callback)
{
client.request({
to: actor,
type: "registerEventNotifications",
events: events
}, callback);
}
function wait_for_event(client, topic, callback)
{
client.addListener("eventNotification", (type, response) => {
do_check_eq(type, "eventNotification");
if (response.topic == topic) {
callback();
}
});
}
function test_events(client, actor, callback)
{
register_events(client, actor, ["profiler-started", "profiler-stopped"], () => {
wait_for_event(client, "profiler-started", () => {
wait_for_event(client, "profiler-stopped", () => {
callback();
});
Profiler.StopProfiler();
});
Profiler.StartProfiler(1000000, 1, ["js"], 1);
});
}

View File

@ -232,7 +232,6 @@ reason = bug 820380
[test_profiler_close.js]
[test_profiler_data.js]
[test_profiler_events-01.js]
[test_profiler_events-02.js]
[test_profiler_getbufferinfo.js]
[test_profiler_getfeatures.js]
[test_profiler_getsharedlibraryinformation.js]

View File

@ -10,6 +10,7 @@ EXTRA_JS_MODULES.devtools.shared += [
'async-storage.js',
'framerate.js',
'memory.js',
'profiler.js',
'system.js',
'timeline.js',
'worker-helper.js',

View File

@ -0,0 +1,438 @@
/* 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 { Cc, Ci, Cu } = require("chrome");
const Services = require("Services");
const { Class } = require("sdk/core/heritage");
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/toolkit/DevToolsUtils.js");
// Events piped from system observers to Profiler instances.
const PROFILER_SYSTEM_EVENTS = [
"console-api-profiler",
"profiler-started",
"profiler-stopped"
];
loader.lazyGetter(this, "nsIProfilerModule", () => {
return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
});
let DEFAULT_PROFILER_OPTIONS = {
// When using the DevTools Performance Tools, this will be overridden
// by the pref `devtools.performance.profiler.buffer-size`.
entries: Math.pow(10, 7),
// When using the DevTools Performance Tools, this will be overridden
// by the pref `devtools.performance.profiler.sample-rate-khz`.
interval: 1,
features: ["js"],
threadFilters: ["GeckoMain"]
};
/**
* Main interface for interacting with nsIProfiler
*/
const ProfilerManager = (function () {
let consumers = new Set();
return {
/**
* The nsIProfiler is target agnostic and interacts with the whole platform.
* Therefore, special care needs to be given to make sure different profiler
* consumers (i.e. "toolboxes") don't interfere with each other. Register
* the instance here.
*/
addInstance: function (instance) {
consumers.add(instance);
// Lazily register events
this.registerEventListeners();
},
removeInstance: function (instance) {
consumers.delete(instance);
if (this.length < 0) {
let msg = "Somehow the number of started profilers is now negative.";
DevToolsUtils.reportException("Profiler", msg);
}
if (this.length === 0) {
this.unregisterEventListeners();
this.stop();
}
},
/**
* Starts the nsIProfiler module. Doing so will discard any samples
* that might have been accumulated so far.
*
* @param {number} entries [optional]
* @param {number} interval [optional]
* @param {Array<string>} features [optional]
* @param {Array<string>} threadFilters [description]
*
* @return {object}
*/
start: function (options = {}) {
let config = this._profilerStartOptions = {
entries: options.entries || DEFAULT_PROFILER_OPTIONS.entries,
interval: options.interval || DEFAULT_PROFILER_OPTIONS.interval,
features: options.features || DEFAULT_PROFILER_OPTIONS.features,
threadFilters: options.threadFilters || DEFAULT_PROFILER_OPTIONS.threadFilters,
};
// The start time should be before any samples we might be
// interested in.
let currentTime = nsIProfilerModule.getElapsedTime();
nsIProfilerModule.StartProfiler(
config.entries,
config.interval,
config.features,
config.features.length,
config.threadFilters,
config.threadFilters.length
);
let { position, totalSize, generation } = this.getBufferInfo();
return { started: true, position, totalSize, generation, currentTime };
},
stop: function () {
// Actually stop the profiler only if the last client has stopped profiling.
// Since this is used as a root actor, and the profiler module interacts with the
// whole platform, we need to avoid a case in which the profiler is stopped
// when there might be other clients still profiling.
if (this.length <= 1) {
nsIProfilerModule.StopProfiler();
}
return { started: false };
},
/**
* Returns all the samples accumulated since the profiler was started,
* along with the current time. The data has the following format:
* {
* libs: string,
* meta: {
* interval: number,
* platform: string,
* ...
* },
* threads: [{
* samples: [{
* frames: [{
* line: number,
* location: string,
* category: number
* } ... ],
* name: string
* responsiveness: number
* time: number
* } ... ]
* } ... ]
* }
*
*
* @param number startTime
* Since the circular buffer will only grow as long as the profiler lives,
* the buffer can contain unwanted samples. Pass in a `startTime` to only retrieve
* samples that took place after the `startTime`, with 0 being when the profiler
* just started.
* @param boolean stringify
* Whether or not the returned profile object should be a string or not to save
* JSON parse/stringify cycle if emitting over RDP.
*/
getProfile: function (options) {
let startTime = options.startTime || 0;
let profile = options.stringify ?
nsIProfilerModule.GetProfile(startTime) :
nsIProfilerModule.getProfileData(startTime);
return { profile: profile, currentTime: nsIProfilerModule.getElapsedTime() };
},
/**
* Returns an array of feature strings, describing the profiler features
* that are available on this platform. Can be called while the profiler
* is stopped.
*
* @return {object}
*/
getFeatures: function () {
return { features: nsIProfilerModule.GetFeatures([]) };
},
/**
* Returns an object with the values of the current status of the
* circular buffer in the profiler, returning `position`, `totalSize`,
* and the current `generation` of the buffer.
*
* @return {object}
*/
getBufferInfo: function() {
let position = {}, totalSize = {}, generation = {};
nsIProfilerModule.GetBufferInfo(position, totalSize, generation);
return {
position: position.value,
totalSize: totalSize.value,
generation: generation.value
}
},
/**
* Returns the configuration used that was originally passed in to start up the
* profiler. Used for tests, and does not account for others using nsIProfiler.
*
* @param {object}
*/
getStartOptions: function() {
return this._profilerStartOptions || {};
},
/**
* Verifies whether or not the nsIProfiler module has started.
* If already active, the current time is also returned.
*
* @return {object}
*/
isActive: function() {
let isActive = nsIProfilerModule.IsActive();
let elapsedTime = isActive ? nsIProfilerModule.getElapsedTime() : undefined;
let { position, totalSize, generation } = this.getBufferInfo();
return { isActive: isActive, currentTime: elapsedTime, position, totalSize, generation };
},
/**
* Returns a stringified JSON object that describes the shared libraries
* which are currently loaded into our process. Can be called while the
* profiler is stopped.
*/
getSharedLibraryInformation: function() {
return { sharedLibraryInformation: nsIProfilerModule.getSharedLibraryInformation() };
},
/**
* Number of profiler instances.
*
* @return {number}
*/
get length() {
return consumers.size;
},
/**
* Callback for all observed notifications.
* @param object subject
* @param string topic
* @param object data
*/
observe: sanitizeHandler(function (subject, topic, data) {
let details;
// An optional label may be specified when calling `console.profile`.
// If that's the case, stringify it and send it over with the response.
let { action, arguments: args } = subject || {};
let profileLabel = args && args.length > 0 ? `${args[0]}` : void 0;
let subscribers = Array.from(consumers).filter(c => c.subscribedEvents.has(topic));
// If no consumers are listening, bail out
if (subscribers.length === 0) {
return;
}
// If the event was generated from `console.profile` or `console.profileEnd`
// we need to start the profiler right away and then just notify the client.
// Otherwise, we'll lose precious samples.
if (topic === "console-api-profiler" && (action === "profile" || action === "profileEnd")) {
let { isActive, currentTime } = this.isActive();
// Start the profiler only if it wasn't already active. Otherwise, any
// samples that might have been accumulated so far will be discarded.
if (!isActive && action === "profile") {
this.start();
details = { profileLabel, currentTime: 0 };
}
// Otherwise, if inactive and a call to profile end, do nothing
// and don't emit event.
else if (!isActive) {
return;
}
// Otherwise, the profiler is already active, so just send
// to the front the current time, label, and the notification
// adds the action as well.
details = { profileLabel, currentTime };
}
// Propagate the event to the profiler instances that
// are subscribed to this event.
for (let subscriber of subscribers) {
events.emit(subscriber, topic, { subject, topic, data, details });
}
}, "ProfilerManager.observe"),
/**
* Registers handlers for the following events to be emitted
* on active Profiler instances:
* - "console-api-profiler"
* - "profiler-started"
* - "profiler-stopped"
*
* The ProfilerManager listens to all events, and individual
* consumers filter which events they are interested in.
*/
registerEventListeners: function () {
if (!this._eventsRegistered) {
PROFILER_SYSTEM_EVENTS.forEach(eventName =>
Services.obs.addObserver(this, eventName, false));
this._eventsRegistered = true;
}
},
/**
* Unregisters handlers for all system events.
*/
unregisterEventListeners: function () {
if (this._eventsRegistered) {
PROFILER_SYSTEM_EVENTS.forEach(eventName => Services.obs.removeObserver(this, eventName));
this._eventsRegistered = false;
}
}
};
})();
/**
* The profiler actor provides remote access to the built-in nsIProfiler module.
*/
let Profiler = exports.Profiler = Class({
extends: EventTarget,
initialize: function () {
this.subscribedEvents = new Set();
ProfilerManager.addInstance(this);
},
destroy: function() {
this.subscribedEvents = null;
ProfilerManager.removeInstance(this);
},
/**
* @see ProfilerManager.start
*/
start: function (options) { return ProfilerManager.start(options); },
/**
* @see ProfilerManager.stop
*/
stop: function () { return ProfilerManager.stop(); },
/**
* @see ProfilerManager.getProfile
*/
getProfile: function (request={}) { return ProfilerManager.getProfile(request); },
/**
* @see ProfilerManager.getFeatures
*/
getFeatures: function() { return ProfilerManager.getFeatures(); },
/**
* @see ProfilerManager.getBufferInfo
*/
getBufferInfo: function() { return ProfilerManager.getBufferInfo(); },
/**
* @see ProfilerManager.getStartOptions
*/
getStartOptions: function() { return ProfilerManager.getStartOptions(); },
/**
* @see ProfilerManager.isActive
*/
isActive: function() { return ProfilerManager.isActive(); },
/**
* @see ProfilerManager.isActive
*/
getSharedLibraryInformation: function() { return ProfilerManager.getSharedLibraryInformation(); },
/**
* Subscribes this instance to one of several events defined in
* an events array.
* - "console-api-profiler",
* - "profiler-started",
* - "profiler-stopped"
*
* @param {Array<string>} data.event
* @return {object}
*/
registerEventNotifications: function(data={}) {
let response = [];
(data.events || []).forEach(e => {
if (!this.subscribedEvents.has(e)) {
this.subscribedEvents.add(e);
response.push(e);
}
});
return { registered: response };
},
/**
* Unsubscribes this instance to one of several events defined in
* an events array.
*
* @param {Array<string>} data.event
* @return {object}
*/
unregisterEventNotifications: function(data={}) {
let response = [];
(data.events || []).forEach(e => {
if (this.subscribedEvents.has(e)) {
this.subscribedEvents.delete(e);
response.push(e);
}
});
return { registered: response };
},
});
/**
* JSON.stringify callback used in Profiler.prototype.observe.
*/
function cycleBreaker(key, value) {
if (key == "wrappedJSObject") {
return undefined;
}
return value;
}
/**
* Create JSON objects suitable for transportation across the RDP,
* by breaking cycles and making a copy of the `subject` and `data` via
* JSON.stringifying those values with a replacer that omits properties
* known to introduce cycles, and then JSON.parsing the result.
* This spends some CPU cycles, but it's simple.
*
* @TODO Also wraps it in a `makeInfallible` -- is this still necessary?
*
* @param {function} handler
* @return {function}
*/
function sanitizeHandler (handler, identifier) {
return DevToolsUtils.makeInfallible(function (subject, topic, data) {
subject = (subject && !Cu.isXrayWrapper(subject) && subject.wrappedJSObject) || subject;
subject = JSON.parse(JSON.stringify(subject, cycleBreaker));
data = (data && !Cu.isXrayWrapper(data) && data.wrappedJSObject) || data;
data = JSON.parse(JSON.stringify(data, cycleBreaker));
// Pass in clean data to the underlying handler
return handler.call(this, subject, topic, data);
}, identifier);
}