import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,99 @@
/* 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 { on, once, off, emit } = require("sdk/event/core");
const { Class } = require("sdk/core/heritage");
/**
* A very simple utility for monitoring framerate. Takes a `tabActor`
* and monitors framerate over time. The actor wrapper around this
* can be found at devtools/server/actors/framerate.js
*/
var Framerate = exports.Framerate = Class({
initialize: function (tabActor) {
this.tabActor = tabActor;
this._contentWin = tabActor.window;
this._onRefreshDriverTick = this._onRefreshDriverTick.bind(this);
this._onGlobalCreated = this._onGlobalCreated.bind(this);
on(this.tabActor, "window-ready", this._onGlobalCreated);
},
destroy: function (conn) {
off(this.tabActor, "window-ready", this._onGlobalCreated);
this.stopRecording();
},
/**
* Starts monitoring framerate, storing the frames per second.
*/
startRecording: function () {
if (this._recording) {
return;
}
this._recording = true;
this._ticks = [];
this._startTime = this.tabActor.docShell.now();
this._rafID = this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
},
/**
* Stops monitoring framerate, returning the recorded values.
*/
stopRecording: function (beginAt = 0, endAt = Number.MAX_SAFE_INTEGER) {
if (!this._recording) {
return [];
}
let ticks = this.getPendingTicks(beginAt, endAt);
this.cancelRecording();
return ticks;
},
/**
* Stops monitoring framerate, without returning the recorded values.
*/
cancelRecording: function () {
this._contentWin.cancelAnimationFrame(this._rafID);
this._recording = false;
this._ticks = null;
this._rafID = -1;
},
/**
* Returns whether this instance is currently recording.
*/
isRecording: function () {
return !!this._recording;
},
/**
* Gets the refresh driver ticks recorded so far.
*/
getPendingTicks: function (beginAt = 0, endAt = Number.MAX_SAFE_INTEGER) {
if (!this._ticks) {
return [];
}
return this._ticks.filter(e => e >= beginAt && e <= endAt);
},
/**
* Function invoked along with the refresh driver.
*/
_onRefreshDriverTick: function () {
if (!this._recording) {
return;
}
this._rafID = this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
this._ticks.push(this.tabActor.docShell.now() - this._startTime);
},
/**
* When the content window for the tab actor is created.
*/
_onGlobalCreated: function (win) {
if (this._recording) {
this._contentWin.cancelAnimationFrame(this._rafID);
this._rafID = this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
}
}
});

View file

@ -0,0 +1,425 @@
/* 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 { reportException } = require("devtools/shared/DevToolsUtils");
const { Class } = require("sdk/core/heritage");
const { expectState } = require("devtools/server/actors/common");
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
loader.lazyRequireGetter(this, "DeferredTask",
"resource://gre/modules/DeferredTask.jsm", true);
loader.lazyRequireGetter(this, "StackFrameCache",
"devtools/server/actors/utils/stack", true);
loader.lazyRequireGetter(this, "ThreadSafeChromeUtils");
loader.lazyRequireGetter(this, "HeapSnapshotFileUtils",
"devtools/shared/heapsnapshot/HeapSnapshotFileUtils");
loader.lazyRequireGetter(this, "ChromeActor", "devtools/server/actors/chrome",
true);
loader.lazyRequireGetter(this, "ChildProcessActor",
"devtools/server/actors/child-process", true);
/**
* A class that returns memory data for a parent actor's window.
* Using a tab-scoped actor with this instance will measure the memory footprint of its
* parent tab. Using a global-scoped actor instance however, will measure the memory
* footprint of the chrome window referenced by its root actor.
*
* To be consumed by actor's, like MemoryActor using this module to
* send information over RDP, and TimelineActor for using more light-weight
* utilities like GC events and measuring memory consumption.
*/
var Memory = exports.Memory = Class({
extends: EventTarget,
/**
* Requires a root actor and a StackFrameCache.
*/
initialize: function (parent, frameCache = new StackFrameCache()) {
this.parent = parent;
this._mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
.getService(Ci.nsIMemoryReporterManager);
this.state = "detached";
this._dbg = null;
this._frameCache = frameCache;
this._onGarbageCollection = this._onGarbageCollection.bind(this);
this._emitAllocations = this._emitAllocations.bind(this);
this._onWindowReady = this._onWindowReady.bind(this);
events.on(this.parent, "window-ready", this._onWindowReady);
},
destroy: function () {
events.off(this.parent, "window-ready", this._onWindowReady);
this._mgr = null;
if (this.state === "attached") {
this.detach();
}
},
get dbg() {
if (!this._dbg) {
this._dbg = this.parent.makeDebugger();
}
return this._dbg;
},
/**
* Attach to this MemoryBridge.
*
* This attaches the MemoryBridge's Debugger instance so that you can start
* recording allocations or take a census of the heap. In addition, the
* MemoryBridge will start emitting GC events.
*/
attach: expectState("detached", function () {
this.dbg.addDebuggees();
this.dbg.memory.onGarbageCollection = this._onGarbageCollection.bind(this);
this.state = "attached";
}, "attaching to the debugger"),
/**
* Detach from this MemoryBridge.
*/
detach: expectState("attached", function () {
this._clearDebuggees();
this.dbg.enabled = false;
this._dbg = null;
this.state = "detached";
}, "detaching from the debugger"),
/**
* Gets the current MemoryBridge attach/detach state.
*/
getState: function () {
return this.state;
},
_clearDebuggees: function () {
if (this._dbg) {
if (this.isRecordingAllocations()) {
this.dbg.memory.drainAllocationsLog();
}
this._clearFrames();
this.dbg.removeAllDebuggees();
}
},
_clearFrames: function () {
if (this.isRecordingAllocations()) {
this._frameCache.clearFrames();
}
},
/**
* Handler for the parent actor's "window-ready" event.
*/
_onWindowReady: function ({ isTopLevel }) {
if (this.state == "attached") {
this._clearDebuggees();
if (isTopLevel && this.isRecordingAllocations()) {
this._frameCache.initFrames();
}
this.dbg.addDebuggees();
}
},
/**
* Returns a boolean indicating whether or not allocation
* sites are being tracked.
*/
isRecordingAllocations: function () {
return this.dbg.memory.trackingAllocationSites;
},
/**
* Save a heap snapshot scoped to the current debuggees' portion of the heap
* graph.
*
* @param {Object|null} boundaries
*
* @returns {String} The snapshot id.
*/
saveHeapSnapshot: expectState("attached", function (boundaries = null) {
// If we are observing the whole process, then scope the snapshot
// accordingly. Otherwise, use the debugger's debuggees.
if (!boundaries) {
boundaries = this.parent instanceof ChromeActor || this.parent instanceof ChildProcessActor
? { runtime: true }
: { debugger: this.dbg };
}
const path = ThreadSafeChromeUtils.saveHeapSnapshot(boundaries);
return HeapSnapshotFileUtils.getSnapshotIdFromPath(path);
}, "saveHeapSnapshot"),
/**
* Take a census of the heap. See js/src/doc/Debugger/Debugger.Memory.md for
* more information.
*/
takeCensus: expectState("attached", function () {
return this.dbg.memory.takeCensus();
}, "taking census"),
/**
* Start recording allocation sites.
*
* @param {number} options.probability
* The probability we sample any given allocation when recording allocations.
* Must be between 0 and 1 -- defaults to 1.
* @param {number} options.maxLogLength
* The maximum number of allocation events to keep in the
* log. If new allocs occur while at capacity, oldest
* allocations are lost. Must fit in a 32 bit signed integer.
* @param {number} options.drainAllocationsTimeout
* A number in milliseconds of how often, at least, an `allocation` event
* gets emitted (and drained), and also emits and drains on every GC event,
* resetting the timer.
*/
startRecordingAllocations: expectState("attached", function (options = {}) {
if (this.isRecordingAllocations()) {
return this._getCurrentTime();
}
this._frameCache.initFrames();
this.dbg.memory.allocationSamplingProbability = options.probability != null
? options.probability
: 1.0;
this.drainAllocationsTimeoutTimer = typeof options.drainAllocationsTimeout === "number" ? options.drainAllocationsTimeout : null;
if (this.drainAllocationsTimeoutTimer != null) {
if (this._poller) {
this._poller.disarm();
}
this._poller = new DeferredTask(this._emitAllocations, this.drainAllocationsTimeoutTimer);
this._poller.arm();
}
if (options.maxLogLength != null) {
this.dbg.memory.maxAllocationsLogLength = options.maxLogLength;
}
this.dbg.memory.trackingAllocationSites = true;
return this._getCurrentTime();
}, "starting recording allocations"),
/**
* Stop recording allocation sites.
*/
stopRecordingAllocations: expectState("attached", function () {
if (!this.isRecordingAllocations()) {
return this._getCurrentTime();
}
this.dbg.memory.trackingAllocationSites = false;
this._clearFrames();
if (this._poller) {
this._poller.disarm();
this._poller = null;
}
return this._getCurrentTime();
}, "stopping recording allocations"),
/**
* Return settings used in `startRecordingAllocations` for `probability`
* and `maxLogLength`. Currently only uses in tests.
*/
getAllocationsSettings: expectState("attached", function () {
return {
maxLogLength: this.dbg.memory.maxAllocationsLogLength,
probability: this.dbg.memory.allocationSamplingProbability
};
}, "getting allocations settings"),
/**
* Get a list of the most recent allocations since the last time we got
* allocations, as well as a summary of all allocations since we've been
* recording.
*
* @returns Object
* An object of the form:
*
* {
* allocations: [<index into "frames" below>, ...],
* allocationsTimestamps: [
* <timestamp for allocations[0]>,
* <timestamp for allocations[1]>,
* ...
* ],
* allocationSizes: [
* <bytesize for allocations[0]>,
* <bytesize for allocations[1]>,
* ...
* ],
* frames: [
* {
* line: <line number for this frame>,
* column: <column number for this frame>,
* source: <filename string for this frame>,
* functionDisplayName: <this frame's inferred function name function or null>,
* parent: <index into "frames">
* },
* ...
* ],
* }
*
* The timestamps' unit is microseconds since the epoch.
*
* Subsequent `getAllocations` request within the same recording and
* tab navigation will always place the same stack frames at the same
* indices as previous `getAllocations` requests in the same
* recording. In other words, it is safe to use the index as a
* unique, persistent id for its frame.
*
* Additionally, the root node (null) is always at index 0.
*
* We use the indices into the "frames" array to avoid repeating the
* description of duplicate stack frames both when listing
* allocations, and when many stacks share the same tail of older
* frames. There shouldn't be any duplicates in the "frames" array,
* as that would defeat the purpose of this compression trick.
*
* In the future, we might want to split out a frame's "source" and
* "functionDisplayName" properties out the same way we have split
* frames out with the "frames" array. While this would further
* compress the size of the response packet, it would increase CPU
* usage to build the packet, and it should, of course, be guided by
* profiling and done only when necessary.
*/
getAllocations: expectState("attached", function () {
if (this.dbg.memory.allocationsLogOverflowed) {
// Since the last time we drained the allocations log, there have been
// more allocations than the log's capacity, and we lost some data. There
// isn't anything actionable we can do about this, but put a message in
// the browser console so we at least know that it occurred.
reportException("MemoryBridge.prototype.getAllocations",
"Warning: allocations log overflowed and lost some data.");
}
const allocations = this.dbg.memory.drainAllocationsLog();
const packet = {
allocations: [],
allocationsTimestamps: [],
allocationSizes: [],
};
for (let { frame: stack, timestamp, size } of allocations) {
if (stack && Cu.isDeadWrapper(stack)) {
continue;
}
// Safe because SavedFrames are frozen/immutable.
let waived = Cu.waiveXrays(stack);
// Ensure that we have a form, size, and index for new allocations
// because we potentially haven't seen some or all of them yet. After this
// loop, we can rely on the fact that every frame we deal with already has
// its metadata stored.
let index = this._frameCache.addFrame(waived);
packet.allocations.push(index);
packet.allocationsTimestamps.push(timestamp);
packet.allocationSizes.push(size);
}
return this._frameCache.updateFramePacket(packet);
}, "getting allocations"),
/*
* Force a browser-wide GC.
*/
forceGarbageCollection: function () {
for (let i = 0; i < 3; i++) {
Cu.forceGC();
}
},
/**
* Force an XPCOM cycle collection. For more information on XPCOM cycle
* collection, see
* https://developer.mozilla.org/en-US/docs/Interfacing_with_the_XPCOM_cycle_collector#What_the_cycle_collector_does
*/
forceCycleCollection: function () {
Cu.forceCC();
},
/**
* A method that returns a detailed breakdown of the memory consumption of the
* associated window.
*
* @returns object
*/
measure: function () {
let result = {};
let jsObjectsSize = {};
let jsStringsSize = {};
let jsOtherSize = {};
let domSize = {};
let styleSize = {};
let otherSize = {};
let totalSize = {};
let jsMilliseconds = {};
let nonJSMilliseconds = {};
try {
this._mgr.sizeOfTab(this.parent.window, jsObjectsSize, jsStringsSize, jsOtherSize,
domSize, styleSize, otherSize, totalSize, jsMilliseconds, nonJSMilliseconds);
result.total = totalSize.value;
result.domSize = domSize.value;
result.styleSize = styleSize.value;
result.jsObjectsSize = jsObjectsSize.value;
result.jsStringsSize = jsStringsSize.value;
result.jsOtherSize = jsOtherSize.value;
result.otherSize = otherSize.value;
result.jsMilliseconds = jsMilliseconds.value.toFixed(1);
result.nonJSMilliseconds = nonJSMilliseconds.value.toFixed(1);
} catch (e) {
reportException("MemoryBridge.prototype.measure", e);
}
return result;
},
residentUnique: function () {
return this._mgr.residentUnique;
},
/**
* Handler for GC events on the Debugger.Memory instance.
*/
_onGarbageCollection: function (data) {
events.emit(this, "garbage-collection", data);
// If `drainAllocationsTimeout` set, fire an allocations event with the drained log,
// which will restart the timer.
if (this._poller) {
this._poller.disarm();
this._emitAllocations();
}
},
/**
* Called on `drainAllocationsTimeoutTimer` interval if and only if set during `startRecordingAllocations`,
* or on a garbage collection event if drainAllocationsTimeout was set.
* Drains allocation log and emits as an event and restarts the timer.
*/
_emitAllocations: function () {
events.emit(this, "allocations", this.getAllocations());
this._poller.arm();
},
/**
* Accesses the docshell to return the current process time.
*/
_getCurrentTime: function () {
return (this.parent.isRootActor ? this.parent.docShell : this.parent.originalDocShell).now();
},
});

View file

@ -0,0 +1,13 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'framerate.js',
'memory.js',
'profiler.js',
'recorder.js',
'timeline.js',
)

View file

@ -0,0 +1,546 @@
/* 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/shared/DevToolsUtils");
loader.lazyRequireGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm", true);
loader.lazyRequireGetter(this, "Task", "devtools/shared/task", true);
// Events piped from system observers to Profiler instances.
const PROFILER_SYSTEM_EVENTS = [
"console-api-profiler",
"profiler-started",
"profiler-stopped"
];
// How often the "profiler-status" is emitted by default
const BUFFER_STATUS_INTERVAL_DEFAULT = 5000; // ms
loader.lazyGetter(this, "nsIProfilerModule", () => {
return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
});
var 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 {
// How often the "profiler-status" is emitted
_profilerStatusInterval: BUFFER_STATUS_INTERVAL_DEFAULT,
// How many subscribers there
_profilerStatusSubscribers: 0,
/**
* 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 profiler actor instances here.
*
* @param Profiler instance
* A profiler actor class.
*/
addInstance: function (instance) {
consumers.add(instance);
// Lazily register events
this.registerEventListeners();
},
/**
* Remove the profiler actor instances here.
*
* @param Profiler instance
* A profiler actor class.
*/
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();
try {
nsIProfilerModule.StartProfiler(
config.entries,
config.interval,
config.features,
config.features.length,
config.threadFilters,
config.threadFilters.length
);
} catch (e) {
// For some reason, the profiler couldn't be started. This could happen,
// for example, when in private browsing mode.
Cu.reportError(`Could not start the profiler module: ${e.message}`);
return { started: false, reason: e, currentTime };
}
this._updateProfilerStatusPolling();
let { position, totalSize, generation } = this.getBufferInfo();
return { started: true, position, totalSize, generation, currentTime };
},
/**
* Attempts to stop the nsIProfiler module.
*/
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();
}
this._updateProfilerStatusPolling();
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;
// 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.
this.emitEvent(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"
* - "profiler-status"
*
* 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;
}
},
/**
* Takes an event name and additional data and emits them
* through each profiler instance that is subscribed to the event.
*
* @param {string} eventName
* @param {object} data
*/
emitEvent: function (eventName, data) {
let subscribers = Array.from(consumers).filter(c => c.subscribedEvents.has(eventName));
for (let subscriber of subscribers) {
events.emit(subscriber, eventName, data);
}
},
/**
* Updates the frequency that the "profiler-status" event is emitted
* during recording.
*
* @param {number} interval
*/
setProfilerStatusInterval: function (interval) {
this._profilerStatusInterval = interval;
if (this._poller) {
this._poller._delayMs = interval;
}
},
subscribeToProfilerStatusEvents: function () {
this._profilerStatusSubscribers++;
this._updateProfilerStatusPolling();
},
unsubscribeToProfilerStatusEvents: function () {
this._profilerStatusSubscribers--;
this._updateProfilerStatusPolling();
},
/**
* Will enable or disable "profiler-status" events depending on
* if there are subscribers and if the profiler is current recording.
*/
_updateProfilerStatusPolling: function () {
if (this._profilerStatusSubscribers > 0 && nsIProfilerModule.IsActive()) {
if (!this._poller) {
this._poller = new DeferredTask(this._emitProfilerStatus.bind(this), this._profilerStatusInterval);
}
this._poller.arm();
}
// No subscribers; turn off if it exists.
else if (this._poller) {
this._poller.disarm();
}
},
_emitProfilerStatus: function () {
this.emitEvent("profiler-status", this.isActive());
this._poller.arm();
}
};
})();
/**
* The profiler actor provides remote access to the built-in nsIProfiler module.
*/
var Profiler = exports.Profiler = Class({
extends: EventTarget,
initialize: function () {
this.subscribedEvents = new Set();
ProfilerManager.addInstance(this);
},
destroy: function () {
this.unregisterEventNotifications({ events: Array.from(this.subscribedEvents) });
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(); },
/**
* @see ProfilerManager.setProfilerStatusInterval
*/
setProfilerStatusInterval: function (interval) { return ProfilerManager.setProfilerStatusInterval(interval); },
/**
* Subscribes this instance to one of several events defined in
* an events array.
* - "console-api-profiler",
* - "profiler-started",
* - "profiler-stopped"
* - "profiler-status"
*
* @param {Array<string>} data.event
* @return {object}
*/
registerEventNotifications: function (data = {}) {
let response = [];
(data.events || []).forEach(e => {
if (!this.subscribedEvents.has(e)) {
if (e === "profiler-status") {
ProfilerManager.subscribeToProfilerStatusEvents();
}
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)) {
if (e === "profiler-status") {
ProfilerManager.unsubscribeToProfilerStatusEvents();
}
this.subscribedEvents.delete(e);
response.push(e);
}
});
return { registered: response };
},
});
/**
* Checks whether or not the profiler module can currently run.
* @return boolean
*/
Profiler.canProfile = function () {
return nsIProfilerModule.CanProfile();
};
/**
* 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);
}

View file

@ -0,0 +1,494 @@
/* 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 { Task } = require("devtools/shared/task");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "extend",
"sdk/util/object", true);
loader.lazyRequireGetter(this, "Class",
"sdk/core/heritage", true);
loader.lazyRequireGetter(this, "EventTarget",
"sdk/event/target", true);
loader.lazyRequireGetter(this, "events",
"sdk/event/core");
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, "PerformanceRecordingFront",
"devtools/server/actors/performance-recording", true);
loader.lazyRequireGetter(this, "mapRecordingOptions",
"devtools/shared/performance/recording-utils", true);
loader.lazyRequireGetter(this, "DevToolsUtils",
"devtools/shared/DevToolsUtils");
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.
*/
const PerformanceRecorder = exports.PerformanceRecorder = Class({
extends: EventTarget,
initialize: function (conn, tabActor) {
this.conn = conn;
this.tabActor = tabActor;
this._pendingConsoleRecordings = [];
this._recordings = [];
this._onTimelineData = this._onTimelineData.bind(this);
this._onProfilerEvent = this._onProfilerEvent.bind(this);
},
/**
* 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.tabActor);
this._memory = new Memory(this.tabActor);
this._timeline = new Timeline(this.tabActor);
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") {
events.emit(this, "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.
*/
_onConsoleProfileStart: Task.async(function* ({ profileLabel, currentTime: startTime }) {
let 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.
events.emit(this, "console-profile-start");
let model = yield this.startRecording(extend({}, 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.
*/
_onConsoleProfileEnd: Task.async(function* (data) {
// If no data, abort; can occur if profiler isn't running and we get a surprise
// call to console.profileEnd()
if (!data) {
return;
}
let { profileLabel, currentTime: endTime } = data;
let 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);
}
// If no label supplied, pop off the most recent pending console recording
else {
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;
}
yield 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.
let activeRecordings = this._recordings.filter(r => r.isRecording());
if (activeRecordings.length) {
events.emit(this, "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;
let 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.
*/
startRecording: Task.async(function* (options) {
let profilerStart, timelineStart, memoryStart;
profilerStart = Task.spawn(function* () {
let data = yield this._profiler.isActive();
if (data.isActive) {
return data;
}
let startData = yield 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();
}
memoryStart = this._memory.startRecordingAllocations(extend(mapRecordingOptions("memory", options), {
drainAllocationsTimeout: DRAIN_ALLOCATIONS_TIMEOUT
}));
}
let [profilerStartData, timelineStartData, memoryStartData] = yield promise.all([
profilerStart, timelineStart, memoryStart
]);
let 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.
let 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 = yield getSystemInfo();
let model = new PerformanceRecordingActor(this.conn, options, data);
this._recordings.push(model);
events.emit(this, "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.
*/
stopRecording: Task.async(function* (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.indexOf(model) === -1) {
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".
let endTime = Date.now();
events.emit(this, "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);
let startTime = model._startTime;
let 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();
}
let recordingData = {
// Data available only at the end of a recording.
profile: profilerData.profile,
// End times for all the actors.
duration: profilerData.currentTime - startTime,
};
events.emit(this, "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 extend({}, 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")
};
}

View file

@ -0,0 +1,356 @@
/* 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";
/**
* Many Gecko operations (painting, reflows, restyle, ...) can be tracked
* in real time. A marker is a representation of one operation. A marker
* has a name, start and end timestamps. Markers are stored in docShells.
*
* This module exposes this tracking mechanism. To use with devtools' RDP,
* use devtools/server/actors/timeline.js directly.
*
* To start/stop recording markers:
* timeline.start()
* timeline.stop()
* timeline.isRecording()
*
* When markers are available, an event is emitted:
* timeline.on("markers", function(markers) {...})
*/
const { Ci, Cu } = require("chrome");
const { Class } = require("sdk/core/heritage");
// Be aggressive about lazy loading, as this will run on every
// toolbox startup
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "Task", "devtools/shared/task", true);
loader.lazyRequireGetter(this, "Memory", "devtools/server/performance/memory", true);
loader.lazyRequireGetter(this, "Framerate", "devtools/server/performance/framerate", true);
loader.lazyRequireGetter(this, "StackFrameCache", "devtools/server/actors/utils/stack", true);
loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
// How often do we pull markers from the docShells, and therefore, how often do
// we send events to the front (knowing that when there are no markers in the
// docShell, no event is sent).
const DEFAULT_TIMELINE_DATA_PULL_TIMEOUT = 200; // ms
/**
* The timeline actor pops and forwards timeline markers registered in docshells.
*/
var Timeline = exports.Timeline = Class({
extends: EventTarget,
/**
* Initializes this actor with the provided connection and tab actor.
*/
initialize: function (tabActor) {
this.tabActor = tabActor;
this._isRecording = false;
this._stackFrames = null;
this._memory = null;
this._framerate = null;
// Make sure to get markers from new windows as they become available
this._onWindowReady = this._onWindowReady.bind(this);
this._onGarbageCollection = this._onGarbageCollection.bind(this);
events.on(this.tabActor, "window-ready", this._onWindowReady);
},
/**
* Destroys this actor, stopping recording first.
*/
destroy: function () {
this.stop();
events.off(this.tabActor, "window-ready", this._onWindowReady);
this.tabActor = null;
},
/**
* Get the list of docShells in the currently attached tabActor. Note that we
* always list the docShells included in the real root docShell, even if the
* tabActor was switched to a child frame. This is because for now, paint
* markers are only recorded at parent frame level so switching the timeline
* to a child frame would hide all paint markers.
* See https://bugzilla.mozilla.org/show_bug.cgi?id=1050773#c14
* @return {Array}
*/
get docShells() {
let originalDocShell;
let docShells = [];
if (this.tabActor.isRootActor) {
originalDocShell = this.tabActor.docShell;
} else {
originalDocShell = this.tabActor.originalDocShell;
}
if (!originalDocShell) {
return docShells;
}
let docShellsEnum = originalDocShell.getDocShellEnumerator(
Ci.nsIDocShellTreeItem.typeAll,
Ci.nsIDocShell.ENUMERATE_FORWARDS
);
while (docShellsEnum.hasMoreElements()) {
let docShell = docShellsEnum.getNext();
docShells.push(docShell.QueryInterface(Ci.nsIDocShell));
}
return docShells;
},
/**
* At regular intervals, pop the markers from the docshell, and forward
* markers, memory, tick and frames events, if any.
*/
_pullTimelineData: function () {
let docShells = this.docShells;
if (!this._isRecording || !docShells.length) {
return;
}
let endTime = docShells[0].now();
let markers = [];
// Gather markers if requested.
if (this._withMarkers || this._withDocLoadingEvents) {
for (let docShell of docShells) {
for (let marker of docShell.popProfileTimelineMarkers()) {
markers.push(marker);
// The docshell may return markers with stack traces attached.
// Here we transform the stack traces via the stack frame cache,
// which lets us preserve tail sharing when transferring the
// frames to the client. We must waive xrays here because Firefox
// doesn't understand that the Debugger.Frame object is safe to
// use from chrome. See Tutorial-Alloc-Log-Tree.md.
if (this._withFrames) {
if (marker.stack) {
marker.stack = this._stackFrames.addFrame(Cu.waiveXrays(marker.stack));
}
if (marker.endStack) {
marker.endStack = this._stackFrames.addFrame(Cu.waiveXrays(marker.endStack));
}
}
// Emit some helper events for "DOMContentLoaded" and "Load" markers.
if (this._withDocLoadingEvents) {
if (marker.name == "document::DOMContentLoaded" ||
marker.name == "document::Load") {
events.emit(this, "doc-loading", marker, endTime);
}
}
}
}
}
// Emit markers if requested.
if (this._withMarkers && markers.length > 0) {
events.emit(this, "markers", markers, endTime);
}
// Emit framerate data if requested.
if (this._withTicks) {
events.emit(this, "ticks", endTime, this._framerate.getPendingTicks());
}
// Emit memory data if requested.
if (this._withMemory) {
events.emit(this, "memory", endTime, this._memory.measure());
}
// Emit stack frames data if requested.
if (this._withFrames && this._withMarkers) {
let frames = this._stackFrames.makeEvent();
if (frames) {
events.emit(this, "frames", endTime, frames);
}
}
this._dataPullTimeout = setTimeout(() => {
this._pullTimelineData();
}, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
},
/**
* Are we recording profile markers currently?
*/
isRecording: function () {
return this._isRecording;
},
/**
* Start recording profile markers.
*
* @option {boolean} withMarkers
* Boolean indicating whether or not timeline markers are emitted
* once they're accumulated every `DEFAULT_TIMELINE_DATA_PULL_TIMEOUT`
* milliseconds.
* @option {boolean} withTicks
* Boolean indicating whether a `ticks` event is fired and a
* FramerateActor is created.
* @option {boolean} withMemory
* Boolean indiciating whether we want memory measurements sampled.
* @option {boolean} withFrames
* Boolean indicating whether or not stack frames should be handled
* from timeline markers.
* @option {boolean} withGCEvents
* Boolean indicating whether or not GC markers should be emitted.
* TODO: Remove these fake GC markers altogether in bug 1198127.
* @option {boolean} withDocLoadingEvents
* Boolean indicating whether or not DOMContentLoaded and Load
* marker events are emitted.
*/
start: Task.async(function* ({
withMarkers,
withTicks,
withMemory,
withFrames,
withGCEvents,
withDocLoadingEvents,
}) {
let docShells = this.docShells;
if (!docShells.length) {
return -1;
}
let startTime = this._startTime = docShells[0].now();
if (this._isRecording) {
return startTime;
}
this._isRecording = true;
this._withMarkers = !!withMarkers;
this._withTicks = !!withTicks;
this._withMemory = !!withMemory;
this._withFrames = !!withFrames;
this._withGCEvents = !!withGCEvents;
this._withDocLoadingEvents = !!withDocLoadingEvents;
if (this._withMarkers || this._withDocLoadingEvents) {
for (let docShell of docShells) {
docShell.recordProfileTimelineMarkers = true;
}
}
if (this._withTicks) {
this._framerate = new Framerate(this.tabActor);
this._framerate.startRecording();
}
if (this._withMemory || this._withGCEvents) {
this._memory = new Memory(this.tabActor, this._stackFrames);
this._memory.attach();
}
if (this._withGCEvents) {
events.on(this._memory, "garbage-collection", this._onGarbageCollection);
}
if (this._withFrames && this._withMarkers) {
this._stackFrames = new StackFrameCache();
this._stackFrames.initFrames();
}
this._pullTimelineData();
return startTime;
}),
/**
* Stop recording profile markers.
*/
stop: Task.async(function* () {
let docShells = this.docShells;
if (!docShells.length) {
return -1;
}
let endTime = this._startTime = docShells[0].now();
if (!this._isRecording) {
return endTime;
}
if (this._withMarkers || this._withDocLoadingEvents) {
for (let docShell of docShells) {
docShell.recordProfileTimelineMarkers = false;
}
}
if (this._withTicks) {
this._framerate.stopRecording();
this._framerate.destroy();
this._framerate = null;
}
if (this._withMemory || this._withGCEvents) {
this._memory.detach();
this._memory.destroy();
}
if (this._withGCEvents) {
events.off(this._memory, "garbage-collection", this._onGarbageCollection);
}
if (this._withFrames && this._withMarkers) {
this._stackFrames = null;
}
this._isRecording = false;
this._withMarkers = false;
this._withTicks = false;
this._withMemory = false;
this._withFrames = false;
this._withDocLoadingEvents = false;
this._withGCEvents = false;
clearTimeout(this._dataPullTimeout);
return endTime;
}),
/**
* When a new window becomes available in the tabActor, start recording its
* markers if we were recording.
*/
_onWindowReady: function ({ window }) {
if (this._isRecording) {
let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
docShell.recordProfileTimelineMarkers = true;
}
},
/**
* Fired when the Memory component emits a `garbage-collection` event. Used to
* take the data and make it look like the rest of our markers.
*
* A GC "marker" here represents a full GC cycle, which may contain several incremental
* events within its `collection` array. The marker contains a `reason` field, indicating
* why there was a GC, and may contain a `nonincrementalReason` when SpiderMonkey could
* not incrementally collect garbage.
*/
_onGarbageCollection: function ({ collections, gcCycleNumber, reason, nonincrementalReason }) {
let docShells = this.docShells;
if (!this._isRecording || !docShells.length) {
return;
}
let endTime = docShells[0].now();
events.emit(this, "markers", collections.map(({ startTimestamp: start, endTimestamp: end }) => {
return {
name: "GarbageCollection",
causeName: reason,
nonincrementalReason: nonincrementalReason,
cycle: gcCycleNumber,
start,
end,
};
}), endTime);
},
});