Bug 1067491 - Add allocations recording to the memory actor. r=jryans
authorNick Fitzgerald <fitzgen@gmail.com>
Wed, 17 Sep 2014 10:41:00 +0200
changeset 206161 f6c42abb5457af7e430404e558db58794ae7f06a
parent 206160 cb1f250550ff76652ee86bee3786d89095f1ee46
child 206162 3475e6a1665a6339dcbdc220a06ebdcdd9a9293c
child 206236 8b9340f3a185570285d26f69fb016cf99fb93919
push id27514
push usercbook@mozilla.com
push dateFri, 19 Sep 2014 12:24:09 +0000
treeherdermozilla-central@3475e6a1665a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1067491
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1067491 - Add allocations recording to the memory actor. r=jryans
toolkit/devtools/server/actors/memory.js
toolkit/devtools/server/tests/mochitest/chrome.ini
toolkit/devtools/server/tests/mochitest/memory-helpers.js
toolkit/devtools/server/tests/mochitest/test_memory_allocations_01.html
toolkit/devtools/server/tests/mochitest/test_memory_allocations_02.html
toolkit/devtools/server/tests/mochitest/test_memory_allocations_03.html
toolkit/devtools/server/tests/mochitest/test_memory_census.html
toolkit/devtools/server/tests/mochitest/test_memory_gc_01.html
--- a/toolkit/devtools/server/actors/memory.js
+++ b/toolkit/devtools/server/actors/memory.js
@@ -1,18 +1,19 @@
 /* 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");
 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
  * proceeding. If the actor is not in the expected state, the decorated method
  * returns a rejected promise.
  *
  * @param String expectedState
  *        The expected state.
@@ -54,55 +55,317 @@ let MemoryActor = protocol.ActorClass({
 
   initialize: function(conn, parent) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.parent = parent;
     this._mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
                   .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();
     }
     protocol.Actor.prototype.destroy.call(this);
   },
 
   /**
    * Attach to this MemoryActor.
    */
   attach: method(expectState("detached", function() {
     this.dbg.addDebuggees();
-    this.dbg.enabled = true;
     this.state = "attached";
   }), {
     request: {},
     response: {
       type: "attached"
     }
   }),
 
   /**
    * Detach from this MemoryActor.
    */
   detach: method(expectState("attached", function() {
-    this.dbg.removeAllDebuggees();
+    this._clearDebuggees();
     this.dbg.enabled = false;
     this._dbg = null;
     this.state = "detached";
   }), {
     request: {},
     response: {
       type: "detached"
     }
   }),
 
+  _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.
    *
    * @returns object
    */
   measure: method(function() {
     let result = {};
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -69,13 +69,18 @@ skip-if = buildapp == 'mulet'
 [test_styles-matched.html]
 [test_styles-modify.html]
 [test_styles-svg.html]
 [test_unsafeDereference.html]
 [test_evalInGlobal-outerized_this.html]
 [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'
--- a/toolkit/devtools/server/tests/mochitest/memory-helpers.js
+++ b/toolkit/devtools/server/tests/mochitest/memory-helpers.js
@@ -47,8 +47,14 @@ function startServerAndGetSelectedTabMem
 }
 
 function destroyServerAndFinish(client) {
   client.close(() => {
     DebuggerServer.destroy();
     SimpleTest.finish()
   });
 }
+
+function waitForTime(ms) {
+  return new Promise((resolve, reject) => {
+    setTimeout(resolve, ms);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_01.html
@@ -0,0 +1,98 @@
+<!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
+        && 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>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_02.html
@@ -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>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_memory_allocations_03.html
@@ -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>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_memory_census.html
@@ -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>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_memory_gc_01.html
@@ -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>