375 lines
12 KiB
JavaScript
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);
|
|
},
|
|
});
|