Files
tubestation/toolkit/devtools/server/actors/timeline.js

375 lines
12 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* 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 actor exposes this tracking mechanism to the devtools protocol.
*
* To start/stop recording markers:
* TimelineFront.start()
* TimelineFront.stop()
* TimelineFront.isRecording()
*
* When markers are available, an event is emitted:
* TimelineFront.on("markers", function(markers) {...})
*/
const {Ci, Cu} = require("chrome");
const protocol = require("devtools/server/protocol");
const {method, Arg, RetVal, Option} = protocol;
const events = require("sdk/event/core");
const {setTimeout, clearTimeout} = require("sdk/timers");
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const {MemoryBridge} = require("devtools/server/actors/utils/memory-bridge");
const {FramerateActor} = require("devtools/server/actors/framerate");
const {StackFrameCache} = require("devtools/server/actors/utils/stack");
// 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
/**
* Type representing an array of numbers as strings, serialized fast(er).
* http://jsperf.com/json-stringify-parse-vs-array-join-split/3
*
* XXX: It would be nice if on local connections (only), we could just *give*
* the array directly to the front, instead of going through all this
* serialization redundancy.
*/
protocol.types.addType("array-of-numbers-as-strings", {
write: (v) => v.join(","),
// In Gecko <= 37, `v` is an array; do not transform in this case.
read: (v) => typeof v === "string" ? v.split(",") : v
});
/**
* The timeline actor pops and forwards timeline markers registered in docshells.
*/
let TimelineActor = exports.TimelineActor = protocol.ActorClass({
typeName: "timeline",
events: {
/**
* The "markers" events emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms
* at most, when profile markers are found. The timestamps on each marker
* are relative to when recording was started.
*/
"markers" : {
type: "markers",
markers: Arg(0, "json"),
endTime: Arg(1, "number")
},
/**
* The "memory" events emitted in tandem with "markers", if this was enabled
* when the recording started. The `delta` timestamp on this measurement is
* relative to when recording was started.
*/
"memory" : {
type: "memory",
delta: Arg(0, "number"),
measurement: Arg(1, "json")
},
/**
* The "ticks" events (from the refresh driver) emitted in tandem with
* "markers", if this was enabled when the recording started. All ticks
* are timestamps with a zero epoch.
*/
"ticks" : {
type: "ticks",
delta: Arg(0, "number"),
timestamps: Arg(1, "array-of-numbers-as-strings")
},
/**
* The "frames" events emitted in tandem with "markers", containing
* JS stack frames. The `delta` timestamp on this frames packet is
* relative to when recording was started.
*/
"frames" : {
type: "frames",
delta: Arg(0, "number"),
frames: Arg(1, "json")
}
},
/**
* Initializes this actor with the provided connection and tab actor.
*/
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._isRecording = false;
this._stackFrames = null;
this._memoryBridge = 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);
},
/**
* The timeline actor is the first (and last) in its hierarchy to use protocol.js
* so it doesn't have a parent protocol actor that takes care of its lifetime.
* So it needs a disconnect method to cleanup.
*/
disconnect: function() {
this.destroy();
},
/**
* Destroys this actor, stopping recording first.
*/
destroy: function() {
this.stop();
events.off(this.tabActor, "window-ready", this._onWindowReady);
this.tabActor = null;
this._memoryBridge = null;
protocol.Actor.prototype.destroy.call(this);
},
/**
* 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;
if (this.tabActor.isRootActor) {
originalDocShell = this.tabActor.docShell;
} else {
originalDocShell = this.tabActor.originalDocShell;
}
let docShellsEnum = originalDocShell.getDocShellEnumerator(
Ci.nsIDocShellTreeItem.typeAll,
Ci.nsIDocShell.ENUMERATE_FORWARDS
);
let docShells = [];
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() {
if (!this._isRecording || !this.docShells.length) {
return;
}
let endTime = this.docShells[0].now();
let markers = [];
for (let docShell of this.docShells) {
markers.push(...docShell.popProfileTimelineMarkers());
}
// 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.
for (let marker of markers) {
if (marker.stack) {
marker.stack = this._stackFrames.addFrame(Cu.waiveXrays(marker.stack));
}
if (marker.endStack) {
marker.endStack = this._stackFrames.addFrame(Cu.waiveXrays(marker.endStack));
}
}
let frames = this._stackFrames.makeEvent();
if (frames) {
events.emit(this, "frames", endTime, frames);
}
if (markers.length > 0) {
events.emit(this, "markers", markers, endTime);
}
if (this._withMemory) {
events.emit(this, "memory", endTime, this._memoryBridge.measure());
}
if (this._withTicks) {
events.emit(this, "ticks", endTime, this._framerateActor.getPendingTicks());
}
this._dataPullTimeout = setTimeout(() => {
this._pullTimelineData();
}, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
},
/**
* Are we recording profile markers currently?
*/
isRecording: method(function() {
return this._isRecording;
}, {
request: {},
response: {
value: RetVal("boolean")
}
}),
/**
* Start recording profile markers.
*
* @option {boolean} withMemory
* Boolean indiciating whether we want memory measurements sampled. A memory actor
* will be created regardless (to hook into GC events), but this determines
* whether or not a `memory` event gets fired.
* @option {boolean} withTicks
* Boolean indicating whether a `ticks` event is fired and a FramerateActor
* is created.
*/
start: method(Task.async(function *({ withMemory, withTicks }) {
var startTime = this._startTime = this.docShells[0].now();
// Store the start time from unix epoch so we can normalize
// markers from the memory actor
this._unixStartTime = Date.now();
if (this._isRecording) {
return startTime;
}
this._isRecording = true;
this._stackFrames = new StackFrameCache();
this._stackFrames.initFrames();
this._withMemory = withMemory;
this._withTicks = withTicks;
for (let docShell of this.docShells) {
docShell.recordProfileTimelineMarkers = true;
}
this._memoryBridge = new MemoryBridge(this.tabActor, this._stackFrames);
this._memoryBridge.attach();
events.on(this._memoryBridge, "garbage-collection", this._onGarbageCollection);
if (withTicks) {
this._framerateActor = new FramerateActor(this.conn, this.tabActor);
this._framerateActor.startRecording();
}
this._pullTimelineData();
return startTime;
}), {
request: {
withMemory: Option(0, "boolean"),
withTicks: Option(0, "boolean")
},
response: {
value: RetVal("number")
}
}),
/**
* Stop recording profile markers.
*/
stop: method(Task.async(function *() {
if (!this._isRecording) {
return;
}
this._isRecording = false;
this._stackFrames = null;
events.off(this._memoryBridge, "garbage-collection", this._onGarbageCollection);
this._memoryBridge.detach();
if (this._framerateActor) {
this._framerateActor.stopRecording();
this._framerateActor = null;
}
for (let docShell of this.docShells) {
docShell.recordProfileTimelineMarkers = false;
}
clearTimeout(this._dataPullTimeout);
return this.docShells[0].now();
}), {
response: {
// Set as possibly nullable due to the end time possibly being
// undefined during destruction
value: RetVal("nullable:number")
}
}),
/**
* 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 MemoryActor emits a `garbage-collection` event. Used to
* emit the data to the front end and in similar format to other 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, reason, nonincrementalReason }) {
if (!this._isRecording || !this.docShells.length) {
return;
}
// Normalize the start time to docshell start time, and convert it
// to microseconds.
let startTime = (this._unixStartTime - this._startTime) * 1000;
let endTime = this.docShells[0].now();
events.emit(this, "markers", collections.map(({ startTimestamp: start, endTimestamp: end }) => {
return {
name: "GarbageCollection",
causeName: reason,
nonincrementalReason: nonincrementalReason,
// Both timestamps are in microseconds -- convert to milliseconds to match other markers
start: (start - startTime) / 1000,
end: (end - startTime) / 1000
};
}), endTime);
},
});
exports.TimelineFront = protocol.FrontClass(TimelineActor, {
initialize: function(client, {timelineActor}) {
protocol.Front.prototype.initialize.call(this, client, {actor: timelineActor});
this.manage(this);
},
destroy: function() {
protocol.Front.prototype.destroy.call(this);
},
});