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