From 9d28679208cf69c39efcfb5a0208134272fab47a Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Tue, 16 Sep 2014 14:07:48 -0700 Subject: [PATCH] Bug 1067491 - Add allocations recording to the memory actor. r=jryans --- toolkit/devtools/server/actors/memory.js | 269 +++++++++++++++++- .../server/tests/mochitest/chrome.ini | 5 + .../server/tests/mochitest/memory-helpers.js | 6 + .../mochitest/test_memory_allocations_01.html | 97 +++++++ .../mochitest/test_memory_allocations_02.html | 64 +++++ .../mochitest/test_memory_allocations_03.html | 78 +++++ .../tests/mochitest/test_memory_census.html | 33 +++ .../tests/mochitest/test_memory_gc_01.html | 43 +++ 8 files changed, 592 insertions(+), 3 deletions(-) create mode 100644 toolkit/devtools/server/tests/mochitest/test_memory_allocations_01.html create mode 100644 toolkit/devtools/server/tests/mochitest/test_memory_allocations_02.html create mode 100644 toolkit/devtools/server/tests/mochitest/test_memory_allocations_03.html create mode 100644 toolkit/devtools/server/tests/mochitest/test_memory_census.html create mode 100644 toolkit/devtools/server/tests/mochitest/test_memory_gc_01.html diff --git a/toolkit/devtools/server/actors/memory.js b/toolkit/devtools/server/actors/memory.js index 45d9c66dd530..599df2ecf14b 100644 --- a/toolkit/devtools/server/actors/memory.js +++ b/toolkit/devtools/server/actors/memory.js @@ -6,8 +6,9 @@ const { Cc, Ci, Cu } = require("chrome"); let protocol = require("devtools/server/protocol"); -let { method, RetVal } = protocol; +let { method, RetVal, Arg } = protocol; const { reportException } = require("devtools/toolkit/DevToolsUtils"); +loader.lazyRequireGetter(this, "events", "sdk/event/core"); /** * A method decorator that ensures the actor is in the expected state before @@ -59,9 +60,18 @@ let MemoryActor = protocol.ActorClass({ .getService(Ci.nsIMemoryReporterManager); this.state = "detached"; this._dbg = null; + this._framesToCounts = null; + this._framesToIndices = null; + this._framesToForms = null; + + 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(); @@ -74,7 +84,6 @@ let MemoryActor = protocol.ActorClass({ */ attach: method(expectState("detached", function() { this.dbg.addDebuggees(); - this.dbg.enabled = true; this.state = "attached"; }), { request: {}, @@ -87,7 +96,7 @@ let MemoryActor = protocol.ActorClass({ * Detach from this MemoryActor. */ detach: method(expectState("attached", function() { - this.dbg.removeAllDebuggees(); + this._clearDebuggees(); this.dbg.enabled = false; this._dbg = null; this.state = "detached"; @@ -98,6 +107,260 @@ let MemoryActor = protocol.ActorClass({ } }), + _clearDebuggees: function() { + if (this._dbg) { + if (this.dbg.memory.trackingAllocationSites) { + this.dbg.memory.drainAllocationsLog(); + } + this._clearFrames(); + this.dbg.removeAllDebuggees(); + } + }, + + _initFrames: function() { + this._framesToCounts = new Map(); + this._framesToIndices = new Map(); + this._framesToForms = new Map(); + }, + + _clearFrames: function() { + if (this.dbg.memory.trackingAllocationSites) { + this._framesToCounts.clear(); + this._framesToCounts = null; + this._framesToIndices.clear(); + this._framesToIndices = null; + this._framesToForms.clear(); + this._framesToForms = null; + } + }, + + /** + * Handler for the parent actor's "window-ready" event. + */ + _onWindowReady: function({ isTopLevel }) { + if (this.state == "attached") { + if (isTopLevel && this.dbg.memory.trackingAllocationSites) { + this._clearDebuggees(); + this._initFrames(); + } + this.dbg.addDebuggees(); + } + }, + + /** + * Take a census of the heap. See js/src/doc/Debugger/Debugger.Memory.md for + * more information. + */ + takeCensus: method(expectState("attached", function() { + return this.dbg.memory.takeCensus(); + }), { + request: {}, + response: RetVal("json") + }), + + /** + * Start recording allocation sites. + */ + startRecordingAllocations: method(expectState("attached", function() { + this._initFrames(); + this.dbg.memory.trackingAllocationSites = true; + }), { + request: {}, + response: {} + }), + + /** + * Stop recording allocation sites. + */ + stopRecordingAllocations: method(expectState("attached", function(shouldRecord) { + this.dbg.memory.trackingAllocationSites = false; + this._clearFrames(); + }), { + request: {}, + response: {} + }), + + /** + * 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: [ ...], + * frames: [ + * { + * line: , + * column: , + * source: , + * functionDisplayName: , + * parent: + * } + * ... + * ], + * counts: [ + * , + * , + * , + * ... + * ] + * } + * + * 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. + * + * Note that the allocation counts include "self" allocations only, + * and don't account for allocations in child frames. + * + * 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: method(expectState("attached", function() { + const allocations = this.dbg.memory.drainAllocationsLog() + const packet = { + allocations: [] + }; + + for (let stack 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, count, 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. + this._assignFrameIndices(waived); + this._createFrameForms(waived); + this._countFrame(waived); + + packet.allocations.push(this._framesToIndices.get(waived)); + } + + // Now that we are guaranteed to have a form for every frame, we know the + // size the "frames" property's array must be. We use that information to + // create dense arrays even though we populate them out of order. + const size = this._framesToForms.size; + packet.frames = Array(size).fill(null); + packet.counts = Array(size).fill(0); + + // Populate the "frames" and "counts" properties. + for (let [stack, index] of this._framesToIndices) { + packet.frames[index] = this._framesToForms.get(stack); + packet.counts[index] = this._framesToCounts.get(stack) || 0; + } + + return packet; + }), { + request: {}, + response: RetVal("json") + }), + + /** + * Assigns an index to the given frame and its parents, if an index is not + * already assigned. + * + * @param SavedFrame frame + * A frame to assign an index to. + */ + _assignFrameIndices: function(frame) { + if (this._framesToIndices.has(frame)) { + return; + } + + if (frame) { + this._assignFrameIndices(frame.parent); + } + + const index = this._framesToIndices.size; + this._framesToIndices.set(frame, index); + }, + + /** + * Create the form for the given frame, if one doesn't already exist. + * + * @param SavedFrame frame + * A frame to create a form for. + */ + _createFrameForms: function(frame) { + if (this._framesToForms.has(frame)) { + return; + } + + let form = null; + if (frame) { + form = { + line: frame.line, + column: frame.column, + source: frame.source, + functionDisplayName: frame.functionDisplayName, + parent: this._framesToIndices.get(frame.parent) + }; + this._createFrameForms(frame.parent); + } + + this._framesToForms.set(frame, form); + }, + + /** + * Increment the allocation count for the provided frame. + * + * @param SavedFrame frame + * The frame whose allocation count should be incremented. + */ + _countFrame: function(frame) { + if (!this._framesToCounts.has(frame)) { + this._framesToCounts.set(frame, 1); + } else { + let count = this._framesToCounts.get(frame); + this._framesToCounts.set(frame, count + 1); + } + }, + + /* + * Force a browser-wide GC. + */ + forceGarbageCollection: method(function() { + for (let i = 0; i < 3; i++) { + Cu.forceGC(); + } + }, { + request: {}, + response: {} + }), + + /** + * 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: method(function() { + Cu.forceCC(); + }, { + request: {}, + response: {} + }), + /** * A method that returns a detailed breakdown of the memory consumption of the * associated window. diff --git a/toolkit/devtools/server/tests/mochitest/chrome.ini b/toolkit/devtools/server/tests/mochitest/chrome.ini index d0c622a69742..e18c572a5723 100644 --- a/toolkit/devtools/server/tests/mochitest/chrome.ini +++ b/toolkit/devtools/server/tests/mochitest/chrome.ini @@ -74,8 +74,13 @@ skip-if = buildapp == 'mulet' [test_inspector_getImageData.html] skip-if = buildapp == 'mulet' [test_memory.html] +[test_memory_allocations_01.html] +[test_memory_allocations_02.html] +[test_memory_allocations_03.html] [test_memory_attach_01.html] [test_memory_attach_02.html] +[test_memory_census.html] +[test_memory_gc_01.html] [test_preference.html] [test_connectToChild.html] skip-if = buildapp == 'mulet' diff --git a/toolkit/devtools/server/tests/mochitest/memory-helpers.js b/toolkit/devtools/server/tests/mochitest/memory-helpers.js index caf784a4ea35..b7d6fe993a50 100644 --- a/toolkit/devtools/server/tests/mochitest/memory-helpers.js +++ b/toolkit/devtools/server/tests/mochitest/memory-helpers.js @@ -52,3 +52,9 @@ function destroyServerAndFinish(client) { SimpleTest.finish() }); } + +function waitForTime(ms) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); +} diff --git a/toolkit/devtools/server/tests/mochitest/test_memory_allocations_01.html b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_01.html new file mode 100644 index 000000000000..653f7ee0f57a --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_01.html @@ -0,0 +1,97 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/toolkit/devtools/server/tests/mochitest/test_memory_allocations_02.html b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_02.html new file mode 100644 index 000000000000..37a7ccdb9b4d --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_02.html @@ -0,0 +1,64 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/toolkit/devtools/server/tests/mochitest/test_memory_allocations_03.html b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_03.html new file mode 100644 index 000000000000..b7d18d7edee9 --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_03.html @@ -0,0 +1,78 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/toolkit/devtools/server/tests/mochitest/test_memory_census.html b/toolkit/devtools/server/tests/mochitest/test_memory_census.html new file mode 100644 index 000000000000..f240503372eb --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/test_memory_census.html @@ -0,0 +1,33 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ + diff --git a/toolkit/devtools/server/tests/mochitest/test_memory_gc_01.html b/toolkit/devtools/server/tests/mochitest/test_memory_gc_01.html new file mode 100644 index 000000000000..e28716ebbe6e --- /dev/null +++ b/toolkit/devtools/server/tests/mochitest/test_memory_gc_01.html @@ -0,0 +1,43 @@ + + + + + + Memory monitoring actor test + + + + +
+
+
+
+ +