summaryrefslogtreecommitdiffstats
path: root/devtools/server/performance/memory.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/performance/memory.js')
-rw-r--r--devtools/server/performance/memory.js502
1 files changed, 502 insertions, 0 deletions
diff --git a/devtools/server/performance/memory.js b/devtools/server/performance/memory.js
new file mode 100644
index 0000000000..c983a742ec
--- /dev/null
+++ b/devtools/server/performance/memory.js
@@ -0,0 +1,502 @@
+/* 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: [<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
+ * 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;