diff options
Diffstat (limited to 'devtools/server/performance/timeline.js')
-rw-r--r-- | devtools/server/performance/timeline.js | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/devtools/server/performance/timeline.js b/devtools/server/performance/timeline.js new file mode 100644 index 000000000..3d7a90811 --- /dev/null +++ b/devtools/server/performance/timeline.js @@ -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); + }, +}); |