mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 15:28:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
99
devtools/server/performance/framerate.js
Normal file
99
devtools/server/performance/framerate.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
425
devtools/server/performance/memory.js
Normal file
425
devtools/server/performance/memory.js
Normal 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();
|
||||
},
|
||||
|
||||
});
|
||||
13
devtools/server/performance/moz.build
Normal file
13
devtools/server/performance/moz.build
Normal 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',
|
||||
)
|
||||
546
devtools/server/performance/profiler.js
Normal file
546
devtools/server/performance/profiler.js
Normal 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);
|
||||
}
|
||||
494
devtools/server/performance/recorder.js
Normal file
494
devtools/server/performance/recorder.js
Normal 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")
|
||||
};
|
||||
}
|
||||
356
devtools/server/performance/timeline.js
Normal file
356
devtools/server/performance/timeline.js
Normal 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);
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue