/* 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, } = require("resource://devtools/shared/DevToolsUtils.js"); const { expectState } = require("resource://devtools/server/actors/common.js"); loader.lazyRequireGetter( this, "EventEmitter", "resource://devtools/shared/event-emitter.js" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", }); loader.lazyRequireGetter( this, "StackFrameCache", "resource://devtools/server/actors/utils/stack.js", true ); loader.lazyRequireGetter( this, "ParentProcessTargetActor", "resource://devtools/server/actors/targets/parent-process.js", true ); loader.lazyRequireGetter( this, "ContentProcessTargetActor", "resource://devtools/server/actors/targets/content-process.js", 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: [, ...], * allocationsTimestamps: [ * , * , * ... * ], * allocationSizes: [ * , * , * ... * ], * frames: [ * { * line: , * column: , * source: , * functionDisplayName: * , * parent: * }, * ... * ], * } * * 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 * https://developer.mozilla.org/en-US/docs/Interfacing_with_the_XPCOM_cycle_collector#What_the_cycle_collector_does */ 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;