Bug 1067491 - Add allocations recording to the memory actor. r=jryans

This commit is contained in:
Nick Fitzgerald 2014-09-16 14:07:48 -07:00
parent 375b0f2a2f
commit fa4e94a502
8 changed files with 592 additions and 3 deletions

View File

@ -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: [<index into "frames" below> ...],
* 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">
* }
* ...
* ],
* counts: [
* <number of allocations in frames[0]>,
* <number of allocations in frames[1]>,
* <number of allocations in frames[2]>,
* ...
* ]
* }
*
* 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.

View File

@ -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'

View File

@ -52,3 +52,9 @@ function destroyServerAndFinish(client) {
SimpleTest.finish()
});
}
function waitForTime(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}

View File

@ -0,0 +1,97 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1067491 - Test recording allocations.
-->
<head>
<meta charset="utf-8">
<title>Memory monitoring actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script src="memory-helpers.js" type="application/javascript;version=1.8"></script>
<script>
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
var { memory, client } = yield startServerAndGetSelectedTabMemory();
yield memory.attach();
yield memory.startRecordingAllocations();
ok(true, "Can start recording allocations");
// Allocate some objects.
var alloc1, alloc2, alloc3;
(function outer() {
(function middle() {
(function inner() {
alloc1 = {}; alloc1.line = Error().lineNumber;
alloc2 = []; alloc2.line = Error().lineNumber;
alloc3 = new function() {}; alloc3.line = Error().lineNumber;
}());
}());
}());
var response = yield memory.getAllocations();
yield memory.stopRecordingAllocations();
ok(true, "Can stop recording allocations");
// Filter out allocations by library and test code, and get only the
// allocations that occurred in our test case above.
function isTestAllocation(alloc) {
var frame = response.frames[alloc];
return frame.functionDisplayName === "inner"
&& (frame.line === alloc1.line
|| frame.line === alloc2.line
|| frame.line === alloc3.line);
}
var testAllocations = response.allocations.filter(isTestAllocation);
ok(testAllocations.length >= 3,
"Should find our 3 test allocations (plus some allocations for the error "
+ "objects used to get line numbers)");
// For each of the test case's allocations, ensure that the parent frame
// indices are correct. Also test that we did get an allocation at each
// line we expected (rather than a bunch on the first line and none on the
// others, etc).
var expectedLines = new Set([alloc1.line, alloc2.line, alloc3.line]);
for (var alloc of testAllocations) {
var innerFrame = response.frames[alloc];
ok(innerFrame, "Should get the inner frame");
is(innerFrame.functionDisplayName, "inner");
expectedLines.delete(innerFrame.line);
var middleFrame = response.frames[innerFrame.parent];
ok(middleFrame, "Should get the middle frame");
is(middleFrame.functionDisplayName, "middle");
var outerFrame = response.frames[middleFrame.parent];
ok(outerFrame, "Should get the outer frame");
is(outerFrame.functionDisplayName, "outer");
// Not going to test the rest of the frames because they are Task.jsm
// and promise frames and it gets gross. Plus, I wouldn't want this test
// to start failing if they changed their implementations in a way that
// added or removed stack frames here.
}
is(expectedLines.size, 0,
"Should have found all the expected lines");
yield memory.detach();
destroyServerAndFinish(client);
});
};
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,64 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1067491 - Test aggregating allocation counts.
-->
<head>
<meta charset="utf-8">
<title>Memory monitoring actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script src="memory-helpers.js" type="application/javascript;version=1.8"></script>
<script>
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
var { memory, client } = yield startServerAndGetSelectedTabMemory();
yield memory.attach();
yield memory.startRecordingAllocations();
ok(true, "Can start recording allocations");
// Allocate some objects.
var allocs = [];
(function allocator() {
for (var i = 0; i < 10; i++) {
allocs.push({});
}
}());
var response = yield memory.getAllocations();
yield memory.stopRecordingAllocations();
ok(true, "Can stop recording allocations");
// Find the index of our 10 allocations, and then assert that it is in the
// `allocator` frame.
var index = 0;
var found = false;
for (var count of response.counts) {
if (count === 10) {
found = true;
break;
}
index++;
}
ok(found, "Should find the 10 allocations.");
is(response.frames[index].functionDisplayName, "allocator",
"Should have found the allocator frame.");
yield memory.detach();
destroyServerAndFinish(client);
});
};
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,78 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1067491 - Test that frames keep the same index while we are recording.
-->
<head>
<meta charset="utf-8">
<title>Memory monitoring actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script src="memory-helpers.js" type="application/javascript;version=1.8"></script>
<script>
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
var { memory, client } = yield startServerAndGetSelectedTabMemory();
yield memory.attach();
yield memory.startRecordingAllocations();
// Allocate twice with the exact same stack (hence setTimeout rather than
// allocating directly in the generator), but with getAllocations() calls in
// between.
var allocs = [];
function allocator() {
allocs.push({});
}
setTimeout(allocator, 1);
yield waitForTime(2);
var first = yield memory.getAllocations();
setTimeout(allocator, 1);
yield waitForTime(2);
var second = yield memory.getAllocations();
yield memory.stopRecordingAllocations();
// Assert that each frame in the first response has the same index in the
// second response. This isn't commutative, so we don't check that all
// of the second response's frames are the same in the first response,
// because there might be new allocations that happen after the first query
// but before the second.
function assertSameFrame(a, b) {
info("Checking frames at index " + i + ":");
info(" First frame = " + JSON.stringify(a, null, 4));
info(" Second frame = " + JSON.stringify(b, null, 4));
is(!!a, !!b);
if (!a || !b) {
return;
}
is(a.source, b.source);
is(a.line, b.line);
is(a.column, b.column);
is(a.functionDisplayName, b.functionDisplayName);
is(a.parent, b.parent);
}
for (var i = 0; i < first.frames.length; i++) {
assertSameFrame(first.frames[i], second.frames[i]);
}
yield memory.detach();
destroyServerAndFinish(client);
});
};
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,33 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1067491 - Test taking a census over the RDP.
-->
<head>
<meta charset="utf-8">
<title>Memory monitoring actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script src="memory-helpers.js" type="application/javascript;version=1.8"></script>
<script>
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
var { memory, client } = yield startServerAndGetSelectedTabMemory();
yield memory.attach();
var census = yield memory.takeCensus();
is(typeof census, "object");
yield memory.detach();
destroyServerAndFinish(client);
});
};
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,43 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1067491 - Test forcing a gc.
-->
<head>
<meta charset="utf-8">
<title>Memory monitoring actor test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script src="memory-helpers.js" type="application/javascript;version=1.8"></script>
<script>
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
var { memory, client } = yield startServerAndGetSelectedTabMemory();
var objects = [];
for (var i = 0; i < 1000; i++) {
var o = {};
o[Math.random] = 1;
objects.push(o);
}
objects = null;
var { total: beforeGC } = yield memory.measure();
yield memory.forceGarbageCollection();
var { total: afterGC } = yield memory.measure();
ok(beforeGC > afterGC);
destroyServerAndFinish(client);
});
};
</script>
</pre>
</body>
</html>