Source code

Revision control

Copy as Markdown

Other Tools

/* 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 {
reportException,
const { expectState } = require("resource://devtools/server/actors/common.js");
loader.lazyRequireGetter(
this,
"EventEmitter",
);
const lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{
},
{ global: "contextual" }
);
loader.lazyRequireGetter(
this,
"StackFrameCache",
true
);
loader.lazyRequireGetter(
this,
"ParentProcessTargetActor",
true
);
loader.lazyRequireGetter(
this,
"ContentProcessTargetActor",
true
);
/**
* A class that returns memory data for a parent actor's window.
* Using a target-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.
*/
function Memory(parent, frameCache = new StackFrameCache()) {
EventEmitter.decorate(this);
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);
EventEmitter.on(this.parent, "window-ready", this._onWindowReady);
}
Memory.prototype = {
destroy() {
EventEmitter.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() {
// The actor may be attached by the Target via recordAllocation configuration
// or manually by the frontend.
if (this.state == "attached") {
return this.state;
}
this.dbg.addDebuggees();
this.dbg.memory.onGarbageCollection = this._onGarbageCollection.bind(this);
this.state = "attached";
return this.state;
},
/**
* Detach from this MemoryBridge.
*/
detach: expectState(
"attached",
function () {
this._clearDebuggees();
this.dbg.disable();
this._dbg = null;
this.state = "detached";
return this.state;
},
"detaching from the debugger"
),
/**
* Gets the current MemoryBridge attach/detach state.
*/
getState() {
return this.state;
},
_clearDebuggees() {
if (this._dbg) {
if (this.isRecordingAllocations()) {
this.dbg.memory.drainAllocationsLog();
}
this._clearFrames();
this.dbg.removeAllDebuggees();
}
},
_clearFrames() {
if (this.isRecordingAllocations()) {
this._frameCache.clearFrames();
}
},
/**
* Handler for the parent actor's "window-ready" event.
*/
_onWindowReady({ 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() {
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) {
if (
this.parent instanceof ParentProcessTargetActor ||
this.parent instanceof ContentProcessTargetActor
) {
boundaries = { runtime: true };
} else {
boundaries = { debugger: this.dbg };
}
}
return ChromeUtils.saveHeapSnapshotGetId(boundaries);
},
"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 = options.drainAllocationsTimeout;
if (this.drainAllocationsTimeoutTimer != null) {
if (this._poller) {
this._poller.disarm();
}
this._poller = new lazy.DeferredTask(
this._emitAllocations,
this.drainAllocationsTimeoutTimer,
0
);
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 (const { frame: stack, timestamp, size } of allocations) {
if (stack && Cu.isDeadWrapper(stack)) {
continue;
}
// Safe because SavedFrames are frozen/immutable.
const 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.
const 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() {
for (let i = 0; i < 3; i++) {
Cu.forceGC();
}
},
/**
* Force an XPCOM cycle collection. For more information on XPCOM cycle
* collection, see
*/
forceCycleCollection() {
Cu.forceCC();
},
/**
* A method that returns a detailed breakdown of the memory consumption of the
* associated window.
*
* @returns object
*/
measure() {
const result = {};
const jsObjectsSize = {};
const jsStringsSize = {};
const jsOtherSize = {};
const domSize = {};
const styleSize = {};
const otherSize = {};
const totalSize = {};
const jsMilliseconds = {};
const 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() {
return this._mgr.residentUnique;
},
/**
* Handler for GC events on the Debugger.Memory instance.
*/
_onGarbageCollection(data) {
this.emit("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() {
this.emit("allocations", this.getAllocations());
this._poller.arm();
},
/**
* Accesses the docshell to return the current process time.
*/
_getCurrentTime() {
const docShell = this.parent.isRootActor
? this.parent.docShell
: this.parent.originalDocShell;
if (docShell) {
return docShell.now();
}
// When used from the ContentProcessTargetActor, parent has no docShell,
// so fallback to Cu.now
return Cu.now();
},
};
exports.Memory = Memory;