forked from mirrors/gecko-dev
		
	 6a298fd038
			
		
	
	
		6a298fd038
		
	
	
	
	
		
			
			MozReview-Commit-ID: 3duHXef13M4 --HG-- extra : rebase_source : 8f372fedbbfe59ec7c2756bde61450e67f46345a
		
			
				
	
	
		
			432 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			432 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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 { expectState } = require("devtools/server/actors/common");
 | |
| 
 | |
| loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 | |
| loader.lazyRequireGetter(this, "DeferredTask",
 | |
|   "resource://gre/modules/DeferredTask.jsm", true);
 | |
| loader.lazyRequireGetter(this, "StackFrameCache",
 | |
|   "devtools/server/actors/utils/stack", true);
 | |
| loader.lazyRequireGetter(this, "ChromeUtils");
 | |
| loader.lazyRequireGetter(this, "ParentProcessTargetActor", "devtools/server/actors/targets/parent-process", true);
 | |
| loader.lazyRequireGetter(this, "ContentProcessTargetActor", "devtools/server/actors/targets/content-process", 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: function() {
 | |
|     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: expectState("detached", function() {
 | |
|     this.dbg.addDebuggees();
 | |
|     this.dbg.memory.onGarbageCollection = this._onGarbageCollection.bind(this);
 | |
|     this.state = "attached";
 | |
|     return this.state;
 | |
|   }, "attaching to the debugger"),
 | |
| 
 | |
|   /**
 | |
|    * Detach from this MemoryBridge.
 | |
|    */
 | |
|   detach: expectState("attached", function() {
 | |
|     this._clearDebuggees();
 | |
|     this.dbg.enabled = false;
 | |
|     this._dbg = null;
 | |
|     this.state = "detached";
 | |
|     return this.state;
 | |
|   }, "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) {
 | |
|       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 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: 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() {
 | |
|     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: function() {
 | |
|     return this._mgr.residentUnique;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handler for GC events on the Debugger.Memory instance.
 | |
|    */
 | |
|   _onGarbageCollection: function(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: function() {
 | |
|     this.emit("allocations", this.getAllocations());
 | |
|     this._poller.arm();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Accesses the docshell to return the current process time.
 | |
|    */
 | |
|   _getCurrentTime: function() {
 | |
|     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;
 |