Bug 1136945 - Convert GC events from memory actor to be emitted as pseudo-markers from the TimelineActor. Pull out the core of the MemoryActor into a bridge, so it does not have to be used over RDP. r=vp,fitzgen
authorJordan Santell <jsantell@gmail.com>
Tue, 28 Apr 2015 10:32:32 -0700
changeset 273065 ef2bdaf2121ea22ed7b1ea13716444a957feaaa9
parent 273064 cd5b4e44e260a2f8bdbd7f1f4505443702e48ff9
child 273066 d915bf908fe7f13d0a5d5c3b4be02e0bc4431b18
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp, fitzgen
bugs1136945
milestone40.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 1136945 - Convert GC events from memory actor to be emitted as pseudo-markers from the TimelineActor. Pull out the core of the MemoryActor into a bridge, so it does not have to be used over RDP. r=vp,fitzgen
browser/devtools/performance/test/browser.ini
browser/devtools/performance/test/browser_markers-gc.js
browser/devtools/performance/test/head.js
browser/devtools/shared/timeline/global.js
browser/locales/en-US/chrome/browser/devtools/timeline.properties
browser/themes/shared/devtools/performance.inc.css
toolkit/devtools/server/actors/memory.js
toolkit/devtools/server/actors/timeline.js
toolkit/devtools/server/actors/utils/memory-bridge.js
toolkit/devtools/server/moz.build
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -6,17 +6,17 @@ support-files =
   doc_innerHTML.html
   doc_simple-test.html
   head.js
 
 # Commented out tests are profiler tests
 # that need to be moved over to performance tool
 
 [browser_perf-aaa-run-first-leaktest.js]
-
+[browser_markers-gc.js]
 [browser_markers-parse-html.js]
 [browser_perf-allocations-to-samples.js]
 [browser_perf-compatibility-01.js]
 [browser_perf-compatibility-02.js]
 [browser_perf-compatibility-03.js]
 [browser_perf-compatibility-04.js]
 [browser_perf-compatibility-05.js]
 [browser_perf-clear-01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_markers-gc.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we get a "GarbageCollection" marker.
+ */
+
+const TIME_CLOSE_TO = 10000;
+
+function* spawnTest () {
+  let { target, front } = yield initBackend(SIMPLE_URL);
+  let markers;
+
+  front.on("timeline-data", handler);
+  let model = yield front.startRecording({ withTicks: true });
+
+  // Check async for markers found while GC/CCing between
+  yield waitUntil(() => {
+    forceCC();
+    return !!markers;
+  }, 100);
+
+  front.off("timeline-data", handler);
+  yield front.stopRecording(model);
+
+  info(`Got ${markers.length} markers.`);
+
+  let maxMarkerTime = model._timelineStartTime + model.getDuration() + TIME_CLOSE_TO;
+
+  ok(markers.every(({name}) => name === "GarbageCollection"), "All markers found are GC markers");
+  ok(markers.length > 0, "found atleast one GC marker");
+  ok(markers.every(({start}) => typeof start === "number" && start > 0 && start < maxMarkerTime),
+    "All markers have a start time between the valid range.");
+  ok(markers.every(({end}) => typeof end === "number" && end > 0 && end < maxMarkerTime),
+    "All markers have an end time between the valid range.");
+  ok(markers.every(({causeName}) => typeof causeName === "string"),
+    "All markers have a causeName.");
+
+  yield removeTab(target.tab);
+  finish();
+
+  function handler (_, name, m) {
+    if (name === "markers" && m[0].name === "GarbageCollection") {
+      markers = m;
+    }
+  }
+}
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -492,8 +492,18 @@ function dropSelection(graph) {
 function fireKey (e) {
   EventUtils.synthesizeKey(e, {});
 }
 
 function reload (aTarget, aEvent = "navigate") {
   aTarget.activeTab.reload();
   return once(aTarget, aEvent);
 }
+
+/**
+* Forces cycle collection and GC, used in AudioNode destruction tests.
+*/
+function forceCC () {
+  info("Triggering GC/CC...");
+  SpecialPowers.DOMWindowUtils.cycleCollect();
+  SpecialPowers.DOMWindowUtils.garbageCollect();
+  SpecialPowers.DOMWindowUtils.garbageCollect();
+}
--- a/browser/devtools/shared/timeline/global.js
+++ b/browser/devtools/shared/timeline/global.js
@@ -16,17 +16,18 @@ const L10N = new ViewHelpers.L10N(STRING
 /**
  * A simple schema for mapping markers to the timeline UI. The keys correspond
  * to marker names, while the values are objects with the following format:
  *   - group: the row index in the timeline overview graph; multiple markers
  *            can be added on the same row. @see <overview.js/buildGraphImage>
  *   - label: the label used in the waterfall to identify the marker
  *   - colorName: the name of the DevTools color used for this marker. If adding
  *                a new color, be sure to check that there's an entry for
- *                `.marker-details-bullet.{COLORNAME}` for the equivilent entry.
+ *                `.marker-details-bullet.{COLORNAME}` for the equivilent entry
+ *                in ./browser/themes/shared/devtools/performance.inc.css
  *                https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
  *
  * Whenever this is changed, browser_timeline_waterfall-styles.js *must* be
  * updated as well.
  */
 const TIMELINE_BLUEPRINT = {
   "Styles": {
     group: 0,
@@ -63,13 +64,18 @@ const TIMELINE_BLUEPRINT = {
     colorName: "highlight-lightorange",
     label: L10N.getStr("timeline.label.parseXML")
   },
   "ConsoleTime": {
     group: 2,
     colorName: "highlight-bluegrey",
     label: L10N.getStr("timeline.label.consoleTime")
   },
+  "GarbageCollection": {
+    group: 1,
+    colorName: "highlight-red",
+    label: L10N.getStr("timeline.label.garbageCollection")
+  },
 };
 
 // Exported symbols.
 exports.L10N = L10N;
 exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties
@@ -39,16 +39,17 @@ timeline.records=RECORDS
 timeline.label.styles2=Recalculate Style
 timeline.label.reflow2=Layout
 timeline.label.paint=Paint
 timeline.label.javascript2=Function Call
 timeline.label.parseHTML=Parse HTML
 timeline.label.parseXML=Parse XML
 timeline.label.domevent=DOM Event
 timeline.label.consoleTime=Console
+timeline.label.garbageCollection=GC Event
 
 # LOCALIZATION NOTE (graphs.memory):
 # This string is displayed in the memory graph of the Performance tool,
 # as the unit used to memory consumption. This label should be kept
 # AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
 graphs.memory=MB
 
 # LOCALIZATION NOTE (timeline.markerDetailFormat):
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -442,16 +442,23 @@
   background-color: var(--theme-highlight-green);
 }
 #performance-filter-menupopup > menuitem.highlight-lightorange:before,
 .marker-details-bullet.highlight-lightorange,
 .waterfall-marker-bar.highlight-lightorange,
 .waterfall-marker-bullet.highlight-lightorange {
   background-color: var(--theme-highlight-lightorange);
 }
+#performance-filter-menupopup > menuitem.highlight-red:before,
+.marker-details-bullet.highlight-red,
+.waterfall-marker-bar.highlight-red,
+.waterfall-marker-bullet.highlight-red {
+  background-color: var(--theme-highlight-red);
+}
+
 
 #waterfall-details > * {
   padding-top: 3px;
 }
 
 .marker-details-labelname {
   -moz-padding-end: 4px;
 }
--- a/toolkit/devtools/server/actors/memory.js
+++ b/toolkit/devtools/server/actors/memory.js
@@ -1,49 +1,30 @@
 /* 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, Arg, types } = protocol;
-const { reportException } = require("devtools/toolkit/DevToolsUtils");
+const protocol = require("devtools/server/protocol");
+const { method, RetVal, Arg, types } = protocol;
+const { MemoryBridge } = require("./utils/memory-bridge");
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 loader.lazyRequireGetter(this, "StackFrameCache",
                          "devtools/server/actors/utils/stack", true);
 
 /**
- * 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.
- * @param String activity
- *        Additional info about what's going on.
- * @param Function method
- *        The actor method to proceed with when the actor is in the expected
- *        state.
- *
- * @returns Function
- *          The decorated method.
+ * Proxies a call to the MemoryActor to the underlying MemoryBridge,
+ * allowing access to MemoryBridge features by defining the RDP
+ * request/response signature.
  */
-function expectState(expectedState, method, activity) {
-  return function(...args) {
-    if (this.state !== expectedState) {
-      const msg = `Wrong state while ${activity}:` +
-                  `Expected '${expectedState}',` +
-                  `but current state is '${this.state}'.`;
-      return Promise.reject(new Error(msg));
-    }
-
-    return method.apply(this, args);
-  };
+function linkBridge (methodName, definition) {
+  return method(function () {
+    return this.bridge[methodName].apply(this.bridge, arguments);
+  }, definition);
 }
 
 types.addDictType("AllocationsRecordingOptions", {
   // The probability we sample any given allocation when recording
   // allocations. Must be between 0.0 and 1.0. Defaults to 1.0, or sampling
   // every allocation.
   probability: "number",
 
@@ -61,385 +42,167 @@ types.addDictType("AllocationsRecordingO
  */
 let MemoryActor = protocol.ActorClass({
   typeName: "memory",
 
   /**
    * The set of unsolicited events the MemoryActor emits that will be sent over
    * the RDP (by protocol.js).
    */
+
   events: {
     // Same format as the data passed to the
     // `Debugger.Memory.prototype.onGarbageCollection` hook. See
     // `js/src/doc/Debugger/Debugger.Memory.md` for documentation.
     "garbage-collection": {
       type: "garbage-collection",
       data: Arg(0, "json"),
     },
   },
 
-  get dbg() {
-    if (!this._dbg) {
-      this._dbg = this.parent.makeDebugger();
-    }
-    return this._dbg;
-  },
-
   initialize: function(conn, parent, frameCache = new StackFrameCache()) {
     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._frameCache = frameCache;
 
-    this._onGarbageCollection = data =>
-      events.emit(this, "garbage-collection", data);
-
-    this._onWindowReady = this._onWindowReady.bind(this);
-
-    events.on(this.parent, "window-ready", this._onWindowReady);
+    this._onGarbageCollection = this._onGarbageCollection.bind(this);
+    this.bridge = new MemoryBridge(parent, frameCache);
+    this.bridge.on("garbage-collection", this._onGarbageCollection);
   },
 
   destroy: function() {
-    events.off(this.parent, "window-ready", this._onWindowReady);
-
-    this._mgr = null;
-    if (this.state === "attached") {
-      this.detach();
-    }
+    this.bridge.off("garbage-collection", this._onGarbageCollection);
+    this.bridge.destroy();
     protocol.Actor.prototype.destroy.call(this);
   },
 
   /**
    * Attach to this MemoryActor.
    *
    * This attaches the MemoryActor's Debugger instance so that you can start
    * recording allocations or take a census of the heap. In addition, the
    * MemoryActor will start emitting GC events.
    */
-  attach: method(expectState("detached", function() {
-    this.dbg.addDebuggees();
-    this.dbg.memory.onGarbageCollection = this._onGarbageCollection;
-    this.state = "attached";
-  },
-  `attaching to the debugger`), {
+  attach: linkBridge("attach", {
     request: {},
     response: {
       type: "attached"
     }
   }),
 
   /**
    * Detach from this MemoryActor.
    */
-  detach: method(expectState("attached", function() {
-    this._clearDebuggees();
-    this.dbg.enabled = false;
-    this._dbg = null;
-    this.state = "detached";
-  },
-  `detaching from the debugger`), {
+  detach: linkBridge("detach", {
     request: {},
     response: {
       type: "detached"
     }
   }),
 
   /**
    * Gets the current MemoryActor attach/detach state.
    */
-  getState: method(function() {
-    return this.state;
-  }, {
+  getState: linkBridge("getState", {
     response: {
       state: RetVal(0, "string")
     }
   }),
 
-  _clearDebuggees: function() {
-    if (this._dbg) {
-      if (this.dbg.memory.trackingAllocationSites) {
-        this.dbg.memory.drainAllocationsLog();
-      }
-      this._clearFrames();
-      this.dbg.removeAllDebuggees();
-    }
-  },
-
-  _clearFrames: function() {
-    if (this.dbg.memory.trackingAllocationSites) {
-      this._frameCache.clearFrames();
-    }
-  },
-
-  /**
-   * 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._frameCache.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();
-  },
-  `taking census`), {
+  takeCensus: linkBridge("takeCensus", {
     request: {},
     response: RetVal("json")
   }),
 
   /**
    * Start recording allocation sites.
    *
    * @param AllocationsRecordingOptions options
    *        See the protocol.js definition of AllocationsRecordingOptions above.
    */
-  startRecordingAllocations: method(expectState("attached", function(options = {}) {
-    if (this.dbg.memory.trackingAllocationSites) {
-      return Date.now();
-    }
-
-    this._frameCache.initFrames();
-
-    this.dbg.memory.allocationSamplingProbability = options.probability != null
-      ? options.probability
-      : 1.0;
-    if (options.maxLogLength != null) {
-      this.dbg.memory.maxAllocationsLogLength = options.maxLogLength;
-    }
-    this.dbg.memory.trackingAllocationSites = true;
-
-    return Date.now();
-  },
-  `starting recording allocations`), {
+  startRecordingAllocations: linkBridge("startRecordingAllocations", {
     request: {
       options: Arg(0, "nullable:AllocationsRecordingOptions")
     },
     response: {
       // Accept `nullable` in the case of server Gecko <= 37, handled on the front
       value: RetVal(0, "nullable:number")
     }
   }),
 
   /**
    * Stop recording allocation sites.
    */
-  stopRecordingAllocations: method(expectState("attached", function() {
-    this.dbg.memory.trackingAllocationSites = false;
-    this._clearFrames();
-
-    return Date.now();
-  },
-  `stopping recording allocations`), {
+  stopRecordingAllocations: linkBridge("stopRecordingAllocations", {
     request: {},
     response: {
       // Accept `nullable` in the case of server Gecko <= 37, handled on the front
       value: RetVal(0, "nullable:number")
     }
   }),
 
   /**
    * Return settings used in `startRecordingAllocations` for `probability`
    * and `maxLogLength`. Currently only uses in tests.
    */
-  getAllocationsSettings: method(expectState("attached", function() {
-    return {
-      maxLogLength: this.dbg.memory.maxAllocationsLogLength,
-      probability: this.dbg.memory.allocationSamplingProbability
-    };
-  },
-  `getting allocations settings`), {
+  getAllocationsSettings: linkBridge("getAllocationsSettings", {
     request: {},
     response: {
       options: RetVal(0, "json")
     }
   }),
 
-  /**
-   * 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]>,
-   *                ...
-   *              ],
-   *              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]>,
-   *                ...
-   *              ]
-   *            }
-   *
-   *          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.
-   *
-   *          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() {
-    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("MemoryActor.prototype.getAllocations",
-                      "Warning: allocations log overflowed and lost some data.");
-    }
-
-    const allocations = this.dbg.memory.drainAllocationsLog()
-    const packet = {
-      allocations: [],
-      allocationsTimestamps: []
-    };
-
-    for (let { frame: stack, timestamp } 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.
-      let index = this._frameCache.addFrame(waived);
-
-      packet.allocations.push(index);
-      packet.allocationsTimestamps.push(timestamp);
-    }
-
-    return this._frameCache.updateFramePacket(packet);
-  },
-  `getting allocations`), {
+  getAllocations: linkBridge("getAllocations", {
     request: {},
     response: RetVal("json")
   }),
 
   /*
    * Force a browser-wide GC.
    */
-  forceGarbageCollection: method(function() {
-    for (let i = 0; i < 3; i++) {
-      Cu.forceGC();
-    }
-  }, {
+  forceGarbageCollection: linkBridge("forceGarbageCollection", {
     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();
-  }, {
+  forceCycleCollection: linkBridge("forceCycleCollection", {
     request: {},
     response: {}
   }),
 
   /**
    * A method that returns a detailed breakdown of the memory consumption of the
    * associated window.
    *
    * @returns object
    */
-  measure: method(function() {
-    let result = {};
-
-    let jsObjectsSize = {};
-    let jsStringsSize = {};
-    let jsOtherSize = {};
-    let domSize = {};
-    let styleSize = {};
-    let otherSize = {};
-    let totalSize = {};
-    let jsMilliseconds = {};
-    let 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("MemoryActor.prototype.measure", e);
-    }
-
-    return result;
-  }, {
+  measure: linkBridge("measure", {
     request: {},
     response: RetVal("json"),
   }),
 
-  residentUnique: method(function() {
-    return this._mgr.residentUnique;
-  }, {
+  residentUnique: linkBridge("residentUnique", {
     request: {},
     response: { value: RetVal("number") }
-  })
+  }),
+
+  /**
+   * Called when the underlying MemoryBridge fires a "garbage-collection" events.
+   * Propagates over RDP.
+   */
+  _onGarbageCollection: function (data) {
+    events.emit(this, "garbage-collection", data);
+  },
 });
 
 exports.MemoryActor = MemoryActor;
 
 exports.MemoryFront = protocol.FrontClass(MemoryActor, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
     this.actorID = form.memoryActor;
--- a/toolkit/devtools/server/actors/timeline.js
+++ b/toolkit/devtools/server/actors/timeline.js
@@ -19,18 +19,19 @@
  *   TimelineFront.on("markers", function(markers) {...})
  */
 
 const {Ci, Cu} = require("chrome");
 const protocol = require("devtools/server/protocol");
 const {method, Arg, RetVal, Option} = protocol;
 const events = require("sdk/event/core");
 const {setTimeout, clearTimeout} = require("sdk/timers");
+const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 
-const {MemoryActor} = require("devtools/server/actors/memory");
+const {MemoryBridge} = require("devtools/server/actors/utils/memory-bridge");
 const {FramerateActor} = require("devtools/server/actors/framerate");
 const {StackFrameCache} = require("devtools/server/actors/utils/stack");
 
 // How often do we pull markers from the docShells, and therefore, how often do
 // we send events to the front (knowing that when there are no markers in the
 // docShell, no event is sent).
 const DEFAULT_TIMELINE_DATA_PULL_TIMEOUT = 200; // ms
 
@@ -104,19 +105,21 @@ let TimelineActor = exports.TimelineActo
    * Initializes this actor with the provided connection and tab actor.
    */
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
 
     this._isRecording = false;
     this._stackFrames = null;
+    this._memoryBridge = null;
 
     // Make sure to get markers from new windows as they become available
     this._onWindowReady = this._onWindowReady.bind(this);
+    this._onGarbageCollection = this._onGarbageCollection.bind(this);
     events.on(this.tabActor, "window-ready", this._onWindowReady);
   },
 
   /**
    * The timeline actor is the first (and last) in its hierarchy to use protocol.js
    * so it doesn't have a parent protocol actor that takes care of its lifetime.
    * So it needs a disconnect method to cleanup.
    */
@@ -127,16 +130,17 @@ let TimelineActor = exports.TimelineActo
   /**
    * Destroys this actor, stopping recording first.
    */
   destroy: function() {
     this.stop();
 
     events.off(this.tabActor, "window-ready", this._onWindowReady);
     this.tabActor = null;
+    this._memoryBridge = null;
 
     protocol.Actor.prototype.destroy.call(this);
   },
 
   /**
    * Get the list of docShells in the currently attached tabActor. Note that we
    * always list the docShells included in the real root docShell, even if the
    * tabActor was switched to a child frame. This is because for now, paint
@@ -168,20 +172,17 @@ let TimelineActor = exports.TimelineActo
     return docShells;
   },
 
   /**
    * At regular intervals, pop the markers from the docshell, and forward
    * markers, memory, tick and frames events, if any.
    */
   _pullTimelineData: function() {
-    if (!this._isRecording) {
-      return;
-    }
-    if (!this.docShells.length) {
+    if (!this._isRecording || !this.docShells.length) {
       return;
     }
 
     let endTime = this.docShells[0].now();
     let markers = [];
 
     for (let docShell of this.docShells) {
       markers.push(...docShell.popProfileTimelineMarkers());
@@ -204,20 +205,20 @@ let TimelineActor = exports.TimelineActo
 
     let frames = this._stackFrames.makeEvent();
     if (frames) {
       events.emit(this, "frames", endTime, frames);
     }
     if (markers.length > 0) {
       events.emit(this, "markers", markers, endTime);
     }
-    if (this._memoryActor) {
-      events.emit(this, "memory", endTime, this._memoryActor.measure());
+    if (this._withMemory) {
+      events.emit(this, "memory", endTime, this._memoryBridge.measure());
     }
-    if (this._framerateActor) {
+    if (this._withTicks) {
       events.emit(this, "ticks", endTime, this._framerateActor.getPendingTicks());
     }
 
     this._dataPullTimeout = setTimeout(() => {
       this._pullTimelineData();
     }, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
   },
 
@@ -230,79 +231,91 @@ let TimelineActor = exports.TimelineActo
     request: {},
     response: {
       value: RetVal("boolean")
     }
   }),
 
   /**
    * Start recording profile markers.
+   *
+   * @option {boolean} withMemory
+   *         Boolean indiciating whether we want memory measurements sampled. A memory actor
+   *         will be created regardless (to hook into GC events), but this determines
+   *         whether or not a `memory` event gets fired.
+   * @option {boolean} withTicks
+   *         Boolean indicating whether a `ticks` event is fired and a FramerateActor
+   *         is created.
    */
-  start: method(function({ withMemory, withTicks }) {
-    var startTime = this.docShells[0].now();
+  start: method(Task.async(function *({ withMemory, withTicks }) {
+    var startTime = this._startTime = this.docShells[0].now();
+    // Store the start time from unix epoch so we can normalize
+    // markers from the memory actor
+    this._unixStartTime = Date.now();
 
     if (this._isRecording) {
       return startTime;
     }
 
     this._isRecording = true;
     this._stackFrames = new StackFrameCache();
     this._stackFrames.initFrames();
+    this._withMemory = withMemory;
+    this._withTicks = withTicks;
 
     for (let docShell of this.docShells) {
       docShell.recordProfileTimelineMarkers = true;
     }
 
-    if (withMemory) {
-      this._memoryActor = new MemoryActor(this.conn, this.tabActor, this._stackFrames);
-    }
+    this._memoryBridge = new MemoryBridge(this.tabActor, this._stackFrames);
+    this._memoryBridge.attach();
+    events.on(this._memoryBridge, "garbage-collection", this._onGarbageCollection);
 
     if (withTicks) {
       this._framerateActor = new FramerateActor(this.conn, this.tabActor);
       this._framerateActor.startRecording();
     }
 
     this._pullTimelineData();
     return startTime;
-  }, {
+  }), {
     request: {
       withMemory: Option(0, "boolean"),
       withTicks: Option(0, "boolean")
     },
     response: {
       value: RetVal("number")
     }
   }),
 
   /**
    * Stop recording profile markers.
    */
-  stop: method(function() {
+  stop: method(Task.async(function *() {
     if (!this._isRecording) {
       return;
     }
     this._isRecording = false;
     this._stackFrames = null;
 
-    if (this._memoryActor) {
-      this._memoryActor = null;
-    }
+    events.off(this._memoryBridge, "garbage-collection", this._onGarbageCollection);
+    this._memoryBridge.detach();
 
     if (this._framerateActor) {
       this._framerateActor.stopRecording();
       this._framerateActor = null;
     }
 
     for (let docShell of this.docShells) {
       docShell.recordProfileTimelineMarkers = false;
     }
 
     clearTimeout(this._dataPullTimeout);
     return this.docShells[0].now();
-  }, {
+  }), {
     response: {
       // Set as possibly nullable due to the end time possibly being
       // undefined during destruction
       value: RetVal("nullable:number")
     }
   }),
 
   /**
@@ -311,17 +324,48 @@ let TimelineActor = exports.TimelineActo
    */
   _onWindowReady: function({window}) {
     if (this._isRecording) {
       let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIWebNavigation)
                            .QueryInterface(Ci.nsIDocShell);
       docShell.recordProfileTimelineMarkers = true;
     }
-  }
+  },
+
+  /**
+   * Fired when the MemoryActor emits a `garbage-collection` event. Used to
+   * emit the data to the front end and in similar format to other markers.
+   *
+   * A GC "marker" here represents a full GC cycle, which may contain several incremental
+   * events within its `collection` array. The marker contains a `reason` field, indicating
+   * why there was a GC, and may contain a `nonincrementalReason` when SpiderMonkey could
+   * not incrementally collect garbage.
+   */
+  _onGarbageCollection: function ({ collections, reason, nonincrementalReason }) {
+    if (!this._isRecording || !this.docShells.length) {
+      return;
+    }
+
+    // Normalize the start time to docshell start time, and convert it
+    // to microseconds.
+    let startTime = (this._unixStartTime - this._startTime) * 1000;
+    let endTime = this.docShells[0].now();
+
+    events.emit(this, "markers", collections.map(({ startTimestamp: start, endTimestamp: end }) => {
+      return {
+        name: "GarbageCollection",
+        causeName: reason,
+        nonincrementalReason: nonincrementalReason,
+        // Both timestamps are in microseconds -- convert to milliseconds to match other markers
+        start: (start - startTime) / 1000,
+        end: (end - startTime) / 1000
+      };
+    }), endTime);
+  },
 });
 
 exports.TimelineFront = protocol.FrontClass(TimelineActor, {
   initialize: function(client, {timelineActor}) {
     protocol.Front.prototype.initialize.call(this, client, {actor: timelineActor});
     this.manage(this);
   },
   destroy: function() {
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/utils/memory-bridge.js
@@ -0,0 +1,367 @@
+/* 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/toolkit/DevToolsUtils");
+const { Class } = require("sdk/core/heritage");
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
+loader.lazyRequireGetter(this, "StackFrameCache",
+                         "devtools/server/actors/utils/stack", true);
+
+/**
+ * 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.
+ * @param String activity
+ *        Additional info about what's going on.
+ * @param Function method
+ *        The actor method to proceed with when the actor is in the expected
+ *        state.
+ *
+ * @returns Function
+ *          The decorated method.
+ */
+function expectState(expectedState, method, activity) {
+  return function(...args) {
+    if (this.state !== expectedState) {
+      const msg = `Wrong state while ${activity}:` +
+                  `Expected '${expectedState}',` +
+                  `but current state is '${this.state}'.`;
+      return Promise.reject(new Error(msg));
+    }
+
+    return method.apply(this, args);
+  };
+}
+
+/**
+ * A class that returns memory data for a parent actor's window.
+ * Using a tab-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 MemoryBridge to
+ * send information over RDP, and TimelineActor for using more light-weight
+ * utilities like GC events and measuring memory consumption.
+ */
+let MemoryBridge = Class({
+  extends: EventTarget,
+
+  /**
+   * Requires a root actor and a StackFrameCache.
+   */
+  initialize: function (parent, frameCache = new StackFrameCache()) {
+    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._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();
+    }
+  },
+
+  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";
+  }, `attaching to the debugger`),
+
+  /**
+   * Detach from this MemoryBridge.
+   */
+  detach: expectState("attached", function() {
+    this._clearDebuggees();
+    this.dbg.enabled = false;
+    this._dbg = null;
+    this.state = "detached";
+  }, `detaching from the debugger`),
+
+  /**
+   * Gets the current MemoryBridge attach/detach state.
+   */
+  getState: function () {
+    return this.state;
+  },
+
+  _clearDebuggees: function() {
+    if (this._dbg) {
+      if (this.dbg.memory.trackingAllocationSites) {
+        this.dbg.memory.drainAllocationsLog();
+      }
+      this._clearFrames();
+      this.dbg.removeAllDebuggees();
+    }
+  },
+
+  _clearFrames: function() {
+    if (this.dbg.memory.trackingAllocationSites) {
+      this._frameCache.clearFrames();
+    }
+  },
+
+  /**
+   * 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._frameCache.initFrames();
+      }
+      this.dbg.addDebuggees();
+    }
+  },
+
+  /**
+   * Handler for GC events on the Debugger.Memory instance.
+   */
+  _onGarbageCollection: function (data) {
+    events.emit(this, "garbage-collection", data);
+  },
+
+  /**
+   * 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 AllocationsRecordingOptions options
+   *        See the protocol.js definition of AllocationsRecordingOptions above.
+   */
+  startRecordingAllocations: expectState("attached", function(options = {}) {
+    if (this.dbg.memory.trackingAllocationSites) {
+      return Date.now();
+    }
+
+    this._frameCache.initFrames();
+
+    this.dbg.memory.allocationSamplingProbability = options.probability != null
+      ? options.probability
+      : 1.0;
+    if (options.maxLogLength != null) {
+      this.dbg.memory.maxAllocationsLogLength = options.maxLogLength;
+    }
+    this.dbg.memory.trackingAllocationSites = true;
+
+    return Date.now();
+  }, `starting recording allocations`),
+
+  /**
+   * Stop recording allocation sites.
+   */
+  stopRecordingAllocations: expectState("attached", function() {
+    this.dbg.memory.trackingAllocationSites = false;
+    this._clearFrames();
+
+    return Date.now();
+  }, `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]>,
+   *                ...
+   *              ],
+   *              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]>,
+   *                ...
+   *              ]
+   *            }
+   *
+   *          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.
+   *
+   *          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: 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: []
+    };
+
+    for (let { frame: stack, timestamp } 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.
+      let index = this._frameCache.addFrame(waived);
+
+      packet.allocations.push(index);
+      packet.allocationsTimestamps.push(timestamp);
+    }
+
+    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 () {
+    let result = {};
+
+    let jsObjectsSize = {};
+    let jsStringsSize = {};
+    let jsOtherSize = {};
+    let domSize = {};
+    let styleSize = {};
+    let otherSize = {};
+    let totalSize = {};
+    let jsMilliseconds = {};
+    let 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;
+  }
+});
+
+exports.MemoryBridge = MemoryBridge;
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -74,14 +74,15 @@ EXTRA_JS_MODULES.devtools.server.actors 
 ]
 
 EXTRA_JS_MODULES.devtools.server.actors.utils += [
     'actors/utils/actor-registry-utils.js',
     'actors/utils/audionodes.json',
     'actors/utils/automation-timeline.js',
     'actors/utils/make-debugger.js',
     'actors/utils/map-uri-to-addon-id.js',
+    'actors/utils/memory-bridge.js',
     'actors/utils/ScriptStore.js',
     'actors/utils/stack.js',
     'actors/utils/TabSources.js'
 ]
 
 FAIL_ON_WARNINGS = True