memory.js (15106B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 reportException, 9 } = require("resource://devtools/shared/DevToolsUtils.js"); 10 const { expectState } = require("resource://devtools/server/actors/common.js"); 11 12 loader.lazyRequireGetter( 13 this, 14 "EventEmitter", 15 "resource://devtools/shared/event-emitter.js" 16 ); 17 const lazy = {}; 18 ChromeUtils.defineESModuleGetters( 19 lazy, 20 { 21 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", 22 }, 23 { global: "contextual" } 24 ); 25 loader.lazyRequireGetter( 26 this, 27 "StackFrameCache", 28 "resource://devtools/server/actors/utils/stack.js", 29 true 30 ); 31 loader.lazyRequireGetter( 32 this, 33 "ParentProcessTargetActor", 34 "resource://devtools/server/actors/targets/parent-process.js", 35 true 36 ); 37 loader.lazyRequireGetter( 38 this, 39 "ContentProcessTargetActor", 40 "resource://devtools/server/actors/targets/content-process.js", 41 true 42 ); 43 44 /** 45 * A class that returns memory data for a parent actor's window. 46 * Using a target-scoped actor with this instance will measure the memory footprint of its 47 * parent tab. Using a global-scoped actor instance however, will measure the memory 48 * footprint of the chrome window referenced by its root actor. 49 * 50 * To be consumed by actor's, like MemoryActor using this module to 51 * send information over RDP, and TimelineActor for using more light-weight 52 * utilities like GC events and measuring memory consumption. 53 */ 54 class Memory extends EventEmitter { 55 constructor(parent, frameCache = new StackFrameCache()) { 56 super(); 57 58 this.parent = parent; 59 this._mgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService( 60 Ci.nsIMemoryReporterManager 61 ); 62 this.state = "detached"; 63 this._dbg = null; 64 this._frameCache = frameCache; 65 66 this._onGarbageCollection = this._onGarbageCollection.bind(this); 67 this._emitAllocations = this._emitAllocations.bind(this); 68 this._onWindowReady = this._onWindowReady.bind(this); 69 70 this.parent.on("window-ready", this._onWindowReady); 71 } 72 73 destroy() { 74 this.parent.off("window-ready", this._onWindowReady); 75 76 this._mgr = null; 77 if (this.state === "attached") { 78 this.detach(); 79 } 80 } 81 82 get dbg() { 83 if (!this._dbg) { 84 this._dbg = this.parent.makeDebugger(); 85 } 86 return this._dbg; 87 } 88 89 /** 90 * Attach to this MemoryBridge. 91 * 92 * This attaches the MemoryBridge's Debugger instance so that you can start 93 * recording allocations or take a census of the heap. In addition, the 94 * MemoryBridge will start emitting GC events. 95 */ 96 attach() { 97 // The actor may be attached by the Target via recordAllocation configuration 98 // or manually by the frontend. 99 if (this.state == "attached") { 100 return this.state; 101 } 102 this.dbg.addDebuggees(); 103 this.dbg.memory.onGarbageCollection = this._onGarbageCollection.bind(this); 104 this.state = "attached"; 105 return this.state; 106 } 107 108 /** 109 * Detach from this MemoryBridge. 110 */ 111 detach = expectState( 112 "attached", 113 function () { 114 this._clearDebuggees(); 115 this.dbg.disable(); 116 this._dbg = null; 117 this.state = "detached"; 118 return this.state; 119 }, 120 "detaching from the debugger" 121 ); 122 123 /** 124 * Gets the current MemoryBridge attach/detach state. 125 */ 126 getState() { 127 return this.state; 128 } 129 130 _clearDebuggees() { 131 if (this._dbg) { 132 if (this.isRecordingAllocations()) { 133 this.dbg.memory.drainAllocationsLog(); 134 } 135 this._clearFrames(); 136 this.dbg.removeAllDebuggees(); 137 } 138 } 139 140 _clearFrames() { 141 if (this.isRecordingAllocations()) { 142 this._frameCache.clearFrames(); 143 } 144 } 145 146 /** 147 * Handler for the parent actor's "window-ready" event. 148 */ 149 _onWindowReady({ isTopLevel }) { 150 if (this.state == "attached") { 151 this._clearDebuggees(); 152 if (isTopLevel && this.isRecordingAllocations()) { 153 this._frameCache.initFrames(); 154 } 155 this.dbg.addDebuggees(); 156 } 157 } 158 159 /** 160 * Returns a boolean indicating whether or not allocation 161 * sites are being tracked. 162 */ 163 isRecordingAllocations() { 164 return this.dbg.memory.trackingAllocationSites; 165 } 166 167 /** 168 * Save a heap snapshot scoped to the current debuggees' portion of the heap 169 * graph. 170 * 171 * @param {object | null} boundaries 172 * 173 * @returns {string} The snapshot id. 174 */ 175 saveHeapSnapshot = expectState( 176 "attached", 177 function (boundaries = null) { 178 // If we are observing the whole process, then scope the snapshot 179 // accordingly. Otherwise, use the debugger's debuggees. 180 if (!boundaries) { 181 if ( 182 this.parent instanceof ParentProcessTargetActor || 183 this.parent instanceof ContentProcessTargetActor 184 ) { 185 boundaries = { runtime: true }; 186 } else { 187 boundaries = { debugger: this.dbg }; 188 } 189 } 190 return ChromeUtils.saveHeapSnapshotGetId(boundaries); 191 }, 192 "saveHeapSnapshot" 193 ); 194 195 /** 196 * Take a census of the heap. See js/src/doc/Debugger/Debugger.Memory.md for 197 * more information. 198 */ 199 takeCensus = expectState( 200 "attached", 201 function () { 202 return this.dbg.memory.takeCensus(); 203 }, 204 "taking census" 205 ); 206 207 /** 208 * Start recording allocation sites. 209 * 210 * @param {number} options.probability 211 * The probability we sample any given allocation when recording 212 * allocations. Must be between 0 and 1 -- defaults to 1. 213 * @param {number} options.maxLogLength 214 * The maximum number of allocation events to keep in the 215 * log. If new allocs occur while at capacity, oldest 216 * allocations are lost. Must fit in a 32 bit signed integer. 217 * @param {number} options.drainAllocationsTimeout 218 * A number in milliseconds of how often, at least, an `allocation` 219 * event gets emitted (and drained), and also emits and drains on every 220 * GC event, resetting the timer. 221 */ 222 startRecordingAllocations = expectState( 223 "attached", 224 function (options = {}) { 225 if (this.isRecordingAllocations()) { 226 return this._getCurrentTime(); 227 } 228 229 this._frameCache.initFrames(); 230 231 this.dbg.memory.allocationSamplingProbability = 232 options.probability != null ? options.probability : 1.0; 233 234 this.drainAllocationsTimeoutTimer = options.drainAllocationsTimeout; 235 236 if (this.drainAllocationsTimeoutTimer != null) { 237 if (this._poller) { 238 this._poller.disarm(); 239 } 240 this._poller = new lazy.DeferredTask( 241 this._emitAllocations, 242 this.drainAllocationsTimeoutTimer, 243 0 244 ); 245 this._poller.arm(); 246 } 247 248 if (options.maxLogLength != null) { 249 this.dbg.memory.maxAllocationsLogLength = options.maxLogLength; 250 } 251 this.dbg.memory.trackingAllocationSites = true; 252 253 return this._getCurrentTime(); 254 }, 255 "starting recording allocations" 256 ); 257 258 /** 259 * Stop recording allocation sites. 260 */ 261 stopRecordingAllocations = expectState( 262 "attached", 263 function () { 264 if (!this.isRecordingAllocations()) { 265 return this._getCurrentTime(); 266 } 267 this.dbg.memory.trackingAllocationSites = false; 268 this._clearFrames(); 269 270 if (this._poller) { 271 this._poller.disarm(); 272 this._poller = null; 273 } 274 275 return this._getCurrentTime(); 276 }, 277 "stopping recording allocations" 278 ); 279 280 /** 281 * Return settings used in `startRecordingAllocations` for `probability` 282 * and `maxLogLength`. Currently only uses in tests. 283 */ 284 getAllocationsSettings = expectState( 285 "attached", 286 function () { 287 return { 288 maxLogLength: this.dbg.memory.maxAllocationsLogLength, 289 probability: this.dbg.memory.allocationSamplingProbability, 290 }; 291 }, 292 "getting allocations settings" 293 ); 294 295 /** 296 * Get a list of the most recent allocations since the last time we got 297 * allocations, as well as a summary of all allocations since we've been 298 * recording. 299 * 300 * @returns Object 301 * An object of the form: 302 * 303 * { 304 * allocations: [<index into "frames" below>, ...], 305 * allocationsTimestamps: [ 306 * <timestamp for allocations[0]>, 307 * <timestamp for allocations[1]>, 308 * ... 309 * ], 310 * allocationSizes: [ 311 * <bytesize for allocations[0]>, 312 * <bytesize for allocations[1]>, 313 * ... 314 * ], 315 * frames: [ 316 * { 317 * line: <line number for this frame>, 318 * column: <column number for this frame>, 319 * source: <filename string for this frame>, 320 * functionDisplayName: 321 * <this frame's inferred function name function or null>, 322 * parent: <index into "frames"> 323 * }, 324 * ... 325 * ], 326 * } 327 * 328 * The timestamps' unit is microseconds since the epoch. 329 * 330 * Subsequent `getAllocations` request within the same recording and 331 * tab navigation will always place the same stack frames at the same 332 * indices as previous `getAllocations` requests in the same 333 * recording. In other words, it is safe to use the index as a 334 * unique, persistent id for its frame. 335 * 336 * Additionally, the root node (null) is always at index 0. 337 * 338 * We use the indices into the "frames" array to avoid repeating the 339 * description of duplicate stack frames both when listing 340 * allocations, and when many stacks share the same tail of older 341 * frames. There shouldn't be any duplicates in the "frames" array, 342 * as that would defeat the purpose of this compression trick. 343 * 344 * In the future, we might want to split out a frame's "source" and 345 * "functionDisplayName" properties out the same way we have split 346 * frames out with the "frames" array. While this would further 347 * compress the size of the response packet, it would increase CPU 348 * usage to build the packet, and it should, of course, be guided by 349 * profiling and done only when necessary. 350 */ 351 getAllocations = expectState( 352 "attached", 353 function () { 354 if (this.dbg.memory.allocationsLogOverflowed) { 355 // Since the last time we drained the allocations log, there have been 356 // more allocations than the log's capacity, and we lost some data. There 357 // isn't anything actionable we can do about this, but put a message in 358 // the browser console so we at least know that it occurred. 359 reportException( 360 "MemoryBridge.prototype.getAllocations", 361 "Warning: allocations log overflowed and lost some data." 362 ); 363 } 364 365 const allocations = this.dbg.memory.drainAllocationsLog(); 366 const packet = { 367 allocations: [], 368 allocationsTimestamps: [], 369 allocationSizes: [], 370 }; 371 for (const { frame: stack, timestamp, size } of allocations) { 372 if (stack && Cu.isDeadWrapper(stack)) { 373 continue; 374 } 375 376 // Safe because SavedFrames are frozen/immutable. 377 const waived = Cu.waiveXrays(stack); 378 379 // Ensure that we have a form, size, and index for new allocations 380 // because we potentially haven't seen some or all of them yet. After this 381 // loop, we can rely on the fact that every frame we deal with already has 382 // its metadata stored. 383 const index = this._frameCache.addFrame(waived); 384 385 packet.allocations.push(index); 386 packet.allocationsTimestamps.push(timestamp); 387 packet.allocationSizes.push(size); 388 } 389 390 return this._frameCache.updateFramePacket(packet); 391 }, 392 "getting allocations" 393 ); 394 395 /* 396 * Force a browser-wide GC. 397 */ 398 forceGarbageCollection() { 399 for (let i = 0; i < 3; i++) { 400 Cu.forceGC(); 401 } 402 } 403 404 /** 405 * Force an XPCOM cycle collection. For more information on XPCOM cycle 406 * collection, see 407 * https://developer.mozilla.org/en-US/docs/Interfacing_with_the_XPCOM_cycle_collector#What_the_cycle_collector_does 408 */ 409 forceCycleCollection() { 410 Cu.forceCC(); 411 } 412 413 /** 414 * A method that returns a detailed breakdown of the memory consumption of the 415 * associated window. 416 * 417 * @returns object 418 */ 419 measure() { 420 const result = {}; 421 422 const jsObjectsSize = {}; 423 const jsStringsSize = {}; 424 const jsOtherSize = {}; 425 const domSize = {}; 426 const styleSize = {}; 427 const otherSize = {}; 428 const totalSize = {}; 429 const jsMilliseconds = {}; 430 const nonJSMilliseconds = {}; 431 432 try { 433 this._mgr.sizeOfTab( 434 this.parent.window, 435 jsObjectsSize, 436 jsStringsSize, 437 jsOtherSize, 438 domSize, 439 styleSize, 440 otherSize, 441 totalSize, 442 jsMilliseconds, 443 nonJSMilliseconds 444 ); 445 result.total = totalSize.value; 446 result.domSize = domSize.value; 447 result.styleSize = styleSize.value; 448 result.jsObjectsSize = jsObjectsSize.value; 449 result.jsStringsSize = jsStringsSize.value; 450 result.jsOtherSize = jsOtherSize.value; 451 result.otherSize = otherSize.value; 452 result.jsMilliseconds = jsMilliseconds.value.toFixed(1); 453 result.nonJSMilliseconds = nonJSMilliseconds.value.toFixed(1); 454 } catch (e) { 455 reportException("MemoryBridge.prototype.measure", e); 456 } 457 458 return result; 459 } 460 461 residentUnique() { 462 return this._mgr.residentUnique; 463 } 464 465 /** 466 * Handler for GC events on the Debugger.Memory instance. 467 */ 468 _onGarbageCollection(data) { 469 this.emit("garbage-collection", data); 470 471 // If `drainAllocationsTimeout` set, fire an allocations event with the drained log, 472 // which will restart the timer. 473 if (this._poller) { 474 this._poller.disarm(); 475 this._emitAllocations(); 476 } 477 } 478 479 /** 480 * Called on `drainAllocationsTimeoutTimer` interval if and only if set 481 * during `startRecordingAllocations`, or on a garbage collection event if 482 * drainAllocationsTimeout was set. 483 * Drains allocation log and emits as an event and restarts the timer. 484 */ 485 _emitAllocations() { 486 this.emit("allocations", this.getAllocations()); 487 this._poller.arm(); 488 } 489 490 /** 491 * Accesses the docshell to return the current process time. 492 */ 493 _getCurrentTime() { 494 const docShell = this.parent.isRootActor 495 ? this.parent.docShell 496 : this.parent.originalDocShell; 497 if (docShell) { 498 return docShell.now(); 499 } 500 // When used from the ContentProcessTargetActor, parent has no docShell, 501 // so fallback to ChromeUtils.now 502 return ChromeUtils.now(); 503 } 504 } 505 506 exports.Memory = Memory;