Bug 1104213 - add stack traces to timeline markers. r=smaug r=vporof
authorTom Tromey <tom@tromey.com>
Mon, 29 Dec 2014 11:32:00 +0100
changeset 247643 0253e47deae727b01101adfcde3f557e57864bc5
parent 247642 381e48ab43960dcaf428db8c48b1d02b2389da90
child 247644 858f0c4f4bdf67be9f58fafca0e51af22f758ab8
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, vporof
bugs1104213
milestone37.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 1104213 - add stack traces to timeline markers. r=smaug r=vporof
browser/devtools/performance/modules/front.js
browser/devtools/performance/performance-controller.js
browser/devtools/performance/views/details-waterfall.js
browser/devtools/timeline/timeline.js
browser/devtools/timeline/widgets/marker-details.js
browser/locales/en-US/chrome/browser/devtools/timeline.properties
browser/themes/shared/devtools/performance.inc.css
browser/themes/shared/devtools/timeline.inc.css
docshell/base/nsDocShell.cpp
docshell/base/nsDocShell.h
docshell/test/browser/browser.ini
docshell/test/browser/browser_timelineMarkers-05.js
docshell/test/browser/browser_timelineMarkers-frame-05.js
dom/base/Console.cpp
dom/events/EventListenerManager.cpp
dom/webidl/ProfileTimelineMarker.webidl
toolkit/devtools/server/actors/memory.js
toolkit/devtools/server/actors/timeline.js
toolkit/devtools/server/actors/utils/stack.js
toolkit/devtools/server/moz.build
--- a/browser/devtools/performance/modules/front.js
+++ b/browser/devtools/performance/modules/front.js
@@ -197,16 +197,17 @@ PerformanceActorsConnection.prototype = 
  */
 function PerformanceFront(connection) {
   EventEmitter.decorate(this);
 
   this._request = connection._request;
 
   // Pipe events from TimelineActor to the PerformanceFront
   connection._timeline.on("markers", markers => this.emit("markers", markers));
+  connection._timeline.on("frames", (delta, frames) => this.emit("frames", delta, frames));
   connection._timeline.on("memory", (delta, measurement) => this.emit("memory", delta, measurement));
   connection._timeline.on("ticks", (delta, timestamps) => this.emit("ticks", delta, timestamps));
 }
 
 PerformanceFront.prototype = {
   /**
    * Manually begins a recording session.
    *
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -142,16 +142,17 @@ let PerformanceController = {
   /**
    * Permanent storage for the markers and the memory measurements streamed by
    * the backend, along with the start and end timestamps.
    */
   _localStartTime: RECORDING_UNAVAILABLE,
   _startTime: RECORDING_UNAVAILABLE,
   _endTime: RECORDING_UNAVAILABLE,
   _markers: [],
+  _frames: [],
   _memory: [],
   _ticks: [],
   _profilerData: {},
 
   /**
    * Listen for events emitted by the current tab target and
    * main UI events.
    */
@@ -164,30 +165,32 @@ let PerformanceController = {
 
     PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
 
     gFront.on("ticks", this._onTimelineData); // framerate
     gFront.on("markers", this._onTimelineData); // timeline markers
+    gFront.on("frames", this._onTimelineData); // stack frames
     gFront.on("memory", this._onTimelineData); // timeline memory
   },
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function() {
     PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
     PerformanceView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
 
     gFront.off("ticks", this._onTimelineData);
     gFront.off("markers", this._onTimelineData);
+    gFront.off("frames", this._onTimelineData);
     gFront.off("memory", this._onTimelineData);
   },
 
   /**
    * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
    * when the front has started to record.
    */
   startRecording: Task.async(function *() {
@@ -200,16 +203,17 @@ let PerformanceController = {
     let { startTime } = yield gFront.startRecording({
       withTicks: true,
       withMemory: true
     });
 
     this._startTime = startTime;
     this._endTime = RECORDING_IN_PROGRESS;
     this._markers = [];
+    this._frames = [];
     this._memory = [];
     this._ticks = [];
 
     this.emit(EVENTS.RECORDING_STARTED);
   }),
 
   /**
    * Stops recording with the PerformanceFront. Emits `EVENTS.RECORDING_STOPPED`
@@ -251,16 +255,17 @@ let PerformanceController = {
    *        The file to import the data from.
    */
   importRecording: Task.async(function*(_, file) {
     let recordingData = yield PerformanceIO.loadRecordingFromFile(file);
 
     this._startTime = recordingData.interval.startTime;
     this._endTime = recordingData.interval.endTime;
     this._markers = recordingData.markers;
+    this._frames = recordingData.frames;
     this._memory = recordingData.memory;
     this._ticks = recordingData.ticks;
     this._profilerData = recordingData.profilerData;
 
     this.emit(EVENTS.RECORDING_IMPORTED, recordingData);
 
     // Flush the current recording.
     this.emit(EVENTS.RECORDING_STARTED);
@@ -296,16 +301,24 @@ let PerformanceController = {
    * Gets the accumulated markers in the current recording.
    * @return array
    */
   getMarkers: function() {
     return this._markers;
   },
 
   /**
+   * Gets the accumulated stack frames in the current recording.
+   * @return array
+   */
+  getFrames: function() {
+    return this._frames;
+  },
+
+  /**
    * Gets the accumulated memory measurements in this recording.
    * @return array
    */
   getMemory: function() {
     return this._memory;
   },
 
   /**
@@ -325,31 +338,37 @@ let PerformanceController = {
   },
 
   /**
    * Gets all the data in this recording.
    */
   getAllData: function() {
     let interval = this.getInterval();
     let markers = this.getMarkers();
+    let frames = this.getFrames();
     let memory = this.getMemory();
     let ticks = this.getTicks();
     let profilerData = this.getProfilerData();
-    return { interval, markers, memory, ticks, profilerData };
+    return { interval, markers, frames, memory, ticks, profilerData };
   },
 
   /**
    * Fired whenever the PerformanceFront emits markers, memory or ticks.
    */
   _onTimelineData: function (eventName, ...data) {
     // Accumulate markers into an array.
     if (eventName == "markers") {
       let [markers] = data;
       Array.prototype.push.apply(this._markers, markers);
     }
+    // Accumulate stack frames into an array.
+    else if (eventName == "frames") {
+      let [delta, frames] = data;
+      Array.prototype.push.apply(this._frames, frames);
+    }
     // Accumulate memory measurements into an array.
     else if (eventName == "memory") {
       let [delta, measurement] = data;
       this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
     }
     // Save the accumulated refresh driver ticks.
     else if (eventName == "ticks") {
       let [delta, timestamps] = data;
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -67,17 +67,21 @@ let WaterfallView = {
   },
 
   /**
    * Called when a marker is selected in the waterfall view,
    * updating the markers detail view.
    */
   _onMarkerSelected: function (event, marker) {
     if (event === "selected") {
-      this.markerDetails.render(marker);
+      this.markerDetails.render({
+        toolbox: gToolbox,
+        marker: marker,
+        frames: PerformanceController.getFrames()
+      });
     }
     if (event === "unselected") {
       this.markerDetails.empty();
     }
   },
 
   /**
    * Called when the marker details view is resized.
--- a/browser/devtools/timeline/timeline.js
+++ b/browser/devtools/timeline/timeline.js
@@ -72,34 +72,38 @@ let TimelineController = {
   /**
    * Permanent storage for the markers and the memory measurements streamed by
    * the backend, along with the start and end timestamps.
    */
   _starTime: 0,
   _endTime: 0,
   _markers: [],
   _memory: [],
+  _frames: [],
 
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function() {
     this._onRecordingTick = this._onRecordingTick.bind(this);
     this._onMarkers = this._onMarkers.bind(this);
     this._onMemory = this._onMemory.bind(this);
+    this._onFrames = this._onFrames.bind(this);
     gFront.on("markers", this._onMarkers);
     gFront.on("memory", this._onMemory);
+    gFront.on("frames", this._onFrames);
   },
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
     gFront.off("markers", this._onMarkers);
     gFront.off("memory", this._onMemory);
+    gFront.off("frames", this._onFrames);
   },
 
   /**
    * Gets the { stat, end } time interval for this recording.
    * @return object
    */
   getInterval: function() {
     return { startTime: this._startTime, endTime: this._endTime };
@@ -117,16 +121,26 @@ let TimelineController = {
    * Gets the accumulated memory measurements in this recording.
    * @return array
    */
   getMemory: function() {
     return this._memory;
   },
 
   /**
+   * Gets stack frame array reported by the actor.  The marker "stack"
+   * and "endStack" properties are indices into this array.  See
+   * actors/utils/stack.js for more details.
+   * @return array
+   */
+  getFrames: function() {
+    return this._frames;
+  },
+
+  /**
    * Updates the views to show or hide the memory recording data.
    */
   updateMemoryRecording: Task.async(function*() {
     if ($("#memory-checkbox").checked) {
       yield TimelineView.showMemoryOverview();
     } else {
       yield TimelineView.hideMemoryOverview();
     }
@@ -158,16 +172,17 @@ let TimelineController = {
     // even when the actor is not generating data.  To do this we get
     // the local time and use it to compute a reasonable elapsed time.
     // See _onRecordingTick.
     this._localStartTime = performance.now();
     this._startTime = startTime;
     this._endTime = startTime;
     this._markers = [];
     this._memory = [];
+    this._frames = [];
     this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
   },
 
   /**
    * Stops the recording, updating the UI as needed.
    */
   _stopRecording: function*() {
     clearInterval(this._updateId);
@@ -222,16 +237,28 @@ let TimelineController = {
    * @param object measurement
    *        A detailed breakdown of the current memory usage.
    */
   _onMemory: function(delta, measurement) {
     this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
   },
 
   /**
+   * Callback handling the "frames" event on the timeline front.
+   *
+   * @param number delta
+   *        The number of milliseconds elapsed since epoch.
+   * @param object frames
+   *        Newly generated frame objects.
+   */
+  _onFrames: function(delta, frames) {
+    Array.prototype.push.apply(this._frames, frames);
+  },
+
+  /**
    * Callback invoked at a fixed interval while recording.
    * Updates the current time and the timeline overview.
    */
   _onRecordingTick: function() {
     // Compute an approximate ending time for the view.  This is
     // needed to ensure that the view updates even when new data is
     // not being generated.
     let fakeTime = this._startTime + (performance.now() - this._localStartTime);
@@ -313,17 +340,21 @@ let TimelineView = {
     this.memoryOverview = null;
   },
 
   /**
    * A marker has been selected in the waterfall.
    */
   _onMarkerSelected: function(event, marker) {
     if (event == "selected") {
-      this.markerDetails.render(marker);
+      this.markerDetails.render({
+        toolbox: gToolbox,
+        marker: marker,
+        frames: TimelineController.getFrames()
+      });
     }
     if (event == "unselected") {
       this.markerDetails.empty();
     }
   },
 
   /**
    * Signals that a recording session has started and triggers the appropriate
--- a/browser/devtools/timeline/widgets/marker-details.js
+++ b/browser/devtools/timeline/widgets/marker-details.js
@@ -1,15 +1,16 @@
 /* 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";
 
 let { Ci } = require("chrome");
+let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
 
 /**
  * This file contains the rendering code for the marker sidebar.
  */
 
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
 loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
@@ -98,20 +99,23 @@ MarkerDetails.prototype = {
     hbox.appendChild(labelName);
     hbox.appendChild(labelValue);
     return hbox;
   },
 
   /**
    * Populates view with marker's details.
    *
-   * @param object marker
-   *        The marker to display.
+   * @param object params
+   *        An options object holding:
+   *        toolbox - The toolbox.
+   *        marker - The marker to display.
+   *        frames - Array of stack frame information; see stack.js.
    */
-  render: function(marker) {
+  render: function({toolbox: toolbox, marker: marker, frames: frames}) {
     this.empty();
 
     // UI for any marker
 
     let title = this.buildMarkerTypeLabel(marker.name);
 
     let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
 
@@ -134,16 +138,94 @@ MarkerDetails.prototype = {
       case "ConsoleTime":
         this.renderConsoleTimeMarker(this._parent, marker);
         break;
       case "DOMEvent":
         this.renderDOMEventMarker(this._parent, marker);
         break;
       default:
     }
+
+    if (marker.stack) {
+      let property = "timeline.markerDetail.stack";
+      if (marker.endStack) {
+        property = "timeline.markerDetail.startStack";
+      }
+      this.renderStackTrace({toolbox: toolbox, parent: this._parent, property: property,
+                             frameIndex: marker.stack, frames: frames});
+    }
+
+    if (marker.endStack) {
+      this.renderStackTrace({toolbox: toolbox, parent: this._parent, property: "timeline.markerDetail.endStack",
+                             frameIndex: marker.endStack, frames: frames});
+    }
+  },
+
+  /**
+   * Render a stack trace.
+   *
+   * @param object params
+   *        An options object with the following members:
+   *        object toolbox - The toolbox.
+   *        nsIDOMNode parent - The parent node holding the view.
+   *        string property - String identifier for label's name.
+   *        integer frameIndex - The index of the topmost stack frame.
+   *        array frames - Array of stack frames.
+   */
+  renderStackTrace: function({toolbox: toolbox, parent: parent,
+                              property: property, frameIndex: frameIndex,
+                              frames: frames}) {
+    let labelName = this._document.createElement("label");
+    labelName.className = "plain marker-details-labelname";
+    labelName.setAttribute("value", L10N.getStr(property));
+    parent.appendChild(labelName);
+
+    while (frameIndex > 0) {
+      let frame = frames[frameIndex];
+      let url = frame.source;
+      let displayName = frame.functionDisplayName;
+      let line = frame.line;
+
+      let hbox = this._document.createElement("hbox");
+
+      if (displayName) {
+        let functionLabel = this._document.createElement("label");
+        functionLabel.setAttribute("value", displayName);
+        hbox.appendChild(functionLabel);
+      }
+
+      if (url) {
+        let aNode = this._document.createElement("a");
+        aNode.className = "waterfall-marker-location theme-link devtools-monospace";
+        aNode.href = url;
+        aNode.draggable = false;
+        aNode.setAttribute("title", url);
+
+        let text = WebConsoleUtils.abbreviateSourceURL(url) + ":" + line;
+        let label = this._document.createElement("label");
+        label.setAttribute("value", text);
+        aNode.appendChild(label);
+        hbox.appendChild(aNode);
+
+        aNode.addEventListener("click", (event) => {
+          event.preventDefault();
+          viewSourceInDebugger(toolbox, url, line);
+        });
+      }
+
+      if (!displayName && !url) {
+        let label = this._document.createElement("label");
+        label.setAttribute("value", L10N.getStr("timeline.markerDetail.unknownFrame"));
+        hbox.appendChild(label);
+      }
+
+      parent.appendChild(hbox);
+
+      frameIndex = frame.parent;
+    }
   },
 
   /**
    * Render details of a console marker (console.time).
    *
    * @param nsIDOMNode parent
    *        The parent node holding the view.
    * @param object marker
@@ -180,11 +262,41 @@ MarkerDetails.prototype = {
       if (marker.eventPhase == Ci.nsIDOMEvent.BUBBLING_PHASE) {
         phaseL10NProp = "timeline.markerDetail.DOMEventBubblingPhase";
       }
       let phase = this.buildNameValueLabel("timeline.markerDetail.DOMEventPhase", L10N.getStr(phaseL10NProp));
       this._parent.appendChild(phase);
     }
   },
 
-}
+};
+
+/**
+ * Opens/selects the debugger in this toolbox and jumps to the specified
+ * file name and line number.
+ * @param object toolbox
+ *        The toolbox.
+ * @param string url
+ * @param number line
+ */
+let viewSourceInDebugger = Task.async(function *(toolbox, url, line) {
+  // If the Debugger was already open, switch to it and try to show the
+  // source immediately. Otherwise, initialize it and wait for the sources
+  // to be added first.
+  let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
+  let { panelWin: dbg } = yield toolbox.selectTool("jsdebugger");
+
+  if (!debuggerAlreadyOpen) {
+    yield dbg.once(dbg.EVENTS.SOURCES_ADDED);
+  }
+
+  let { DebuggerView } = dbg;
+  let { Sources } = DebuggerView;
+
+  let item = Sources.getItemForAttachment(a => a.source.url === url);
+  if (item) {
+    return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true });
+  }
+
+  return Promise.reject("Couldn't find the specified source in the debugger.");
+});
 
 exports.MarkerDetails = MarkerDetails;
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties
@@ -60,8 +60,12 @@ timeline.markerDetail.start=Start:
 timeline.markerDetail.end=End:
 timeline.markerDetail.duration=Duration:
 timeline.markerDetail.consoleTimerName=Timer Name:
 timeline.markerDetail.DOMEventType=Event Type:
 timeline.markerDetail.DOMEventPhase=Phase:
 timeline.markerDetail.DOMEventTargetPhase=Target
 timeline.markerDetail.DOMEventCapturingPhase=Capture
 timeline.markerDetail.DOMEventBubblingPhase=Bubbling
+timeline.markerDetail.stack=Stack:
+timeline.markerDetail.startStack=Stack at start:
+timeline.markerDetail.endStack=Stack at end:
+timeline.markerDetail.unknownFrame=<unknown location>
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -314,16 +314,25 @@
   color: var(--theme-selection-color);
 }
 
 .waterfall-marker-container.selected .waterfall-marker-bullet,
 .waterfall-marker-container.selected .waterfall-marker-bar {
   border-color: initial !important;
 }
 
+.waterfall-marker-location {
+   color: -moz-nativehyperlinktext;
+}
+
+.waterfall-marker-location:hover,
+.waterfall-marker-location:focus {
+   text-decoration: underline;
+}
+
 #waterfall-details {
   -moz-padding-start: 8px;
   -moz-padding-end: 8px;
   padding-top: 8vh;
   overflow: auto;
 }
 
 .marker-details-bullet {
--- a/browser/themes/shared/devtools/timeline.inc.css
+++ b/browser/themes/shared/devtools/timeline.inc.css
@@ -170,16 +170,25 @@
   color: #f5f7fa; /* Light foreground text */
 }
 
 .waterfall-marker-container.selected .waterfall-marker-bullet,
 .waterfall-marker-container.selected .waterfall-marker-bar {
   border-color: initial!important;
 }
 
+.waterfall-marker-location {
+   color: -moz-nativehyperlinktext;
+}
+
+.waterfall-marker-location:hover,
+.waterfall-marker-location:focus {
+   text-decoration: underline;
+}
+
 #timeline-waterfall-details {
   -moz-padding-start: 8px;
   -moz-padding-end: 8px;
   padding-top: 8vh;
   overflow: auto;
 }
 
 .marker-details-bullet {
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -2882,16 +2882,17 @@ nsDocShell::PopProfileTimelineMarkers(JS
   // START marker is found, look for the corresponding END marker and build a
   // {name,start,end} JS object.
   // Paint markers are different because paint is handled at root docShell level
   // in the information that a paint was done is then stored at each sub
   // docShell level but we can only be sure that a paint did happen in a
   // docShell if an Layer marker type was recorded too.
 
   nsTArray<mozilla::dom::ProfileTimelineMarker> profileTimelineMarkers;
+  SequenceRooter<mozilla::dom::ProfileTimelineMarker> rooter(aCx, &profileTimelineMarkers);
 
   // If we see an unpaired START, we keep it around for the next call
   // to PopProfileTimelineMarkers.  We store the kept START objects in
   // this array.
   nsTArray<TimelineMarker*> keptMarkers;
 
   for (uint32_t i = 0; i < mProfileTimelineMarkers.Length(); ++i) {
     TimelineMarker* startPayload = mProfileTimelineMarkers[i];
@@ -2933,27 +2934,28 @@ nsDocShell::PopProfileTimelineMarkers(JS
         if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
           ++markerDepth;
         } else if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
           if (markerDepth > 0) {
             --markerDepth;
           } else {
             // But ignore paint start/end if no layer has been painted.
             if (!isPaint || (isPaint && hasSeenPaintedLayer)) {
-              mozilla::dom::ProfileTimelineMarker marker;
-
-              marker.mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
-              marker.mStart = startPayload->GetTime();
-              marker.mEnd = endPayload->GetTime();
+              mozilla::dom::ProfileTimelineMarker* marker =
+                profileTimelineMarkers.AppendElement();
+
+              marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
+              marker->mStart = startPayload->GetTime();
+              marker->mEnd = endPayload->GetTime();
+              marker->mStack = startPayload->GetStack();
               if (isPaint) {
-                marker.mRectangles.Construct(layerRectangles);
-              } else {
-                startPayload->AddDetails(marker);
+                marker->mRectangles.Construct(layerRectangles);
               }
-              profileTimelineMarkers.AppendElement(marker);
+              startPayload->AddDetails(*marker);
+              endPayload->AddDetails(*marker);
             }
 
             // We want the start to be dropped either way.
             hasSeenEnd = true;
 
             break;
           }
         }
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -18,23 +18,25 @@
 #include "nsIContentViewerContainer.h"
 #include "nsIDOMStorageManager.h"
 #include "nsDocLoader.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/TimeStamp.h"
 #include "GeckoProfiler.h"
 #include "mozilla/dom/ProfileTimelineMarkerBinding.h"
+#include "jsapi.h"
 
 // Helper Classes
 #include "nsCOMPtr.h"
 #include "nsPoint.h" // mCurrent/mDefaultScrollbarPreferences
 #include "nsString.h"
 #include "nsAutoPtr.h"
 #include "nsThreadUtils.h"
+#include "nsContentUtils.h"
 
 // Threshold value in ms for META refresh based redirects
 #define REFRESH_REDIRECT_TIMER 15000
 
 // Interfaces Needed
 #include "nsIDocCharset.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIRefreshURI.h"
@@ -267,45 +269,54 @@ public:
         TimelineMarker(nsDocShell* aDocShell, const char* aName,
                        TracingMetadata aMetaData)
             : mName(aName)
             , mMetaData(aMetaData)
         {
             MOZ_COUNT_CTOR(TimelineMarker);
             MOZ_ASSERT(aName);
             aDocShell->Now(&mTime);
+            if (aMetaData == TRACING_INTERVAL_START) {
+                CaptureStack();
+            }
         }
 
         TimelineMarker(nsDocShell* aDocShell, const char* aName,
                        TracingMetadata aMetaData,
                        const nsAString& aCause)
             : mName(aName)
             , mMetaData(aMetaData)
             , mCause(aCause)
         {
             MOZ_COUNT_CTOR(TimelineMarker);
             MOZ_ASSERT(aName);
             aDocShell->Now(&mTime);
+            if (aMetaData == TRACING_INTERVAL_START) {
+                CaptureStack();
+            }
         }
 
         virtual ~TimelineMarker()
         {
             MOZ_COUNT_DTOR(TimelineMarker);
         }
 
         // Check whether two markers should be considered the same,
         // for the purpose of pairing start and end markers.  Normally
         // this definition suffices.
         virtual bool Equals(const TimelineMarker* other)
         {
             return strcmp(mName, other->mName) == 0;
         }
 
         // Add details specific to this marker type to aMarker.  The
-        // standard elements have already been set.
+        // standard elements have already been set.  This method is
+        // called on both the starting and ending markers of a pair.
+        // Ordinarily the ending marker doesn't need to do anything
+        // here.
         virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
         {
         }
 
         virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&)
         {
             MOZ_ASSERT_UNREACHABLE("can only be called on layer markers");
         }
@@ -325,21 +336,52 @@ public:
             return mTime;
         }
 
         const nsString& GetCause() const
         {
             return mCause;
         }
 
+        JSObject* GetStack()
+        {
+            if (mStackTrace) {
+                return mStackTrace->get();
+            }
+            return nullptr;
+        }
+
+    protected:
+
+        void CaptureStack()
+        {
+            JSContext* ctx = nsContentUtils::GetCurrentJSContext();
+            if (ctx) {
+                JS::RootedObject stack(ctx);
+                if (JS::CaptureCurrentStack(ctx, &stack)) {
+                    mStackTrace.emplace(ctx, stack.get());
+                } else {
+                    JS_ClearPendingException(ctx);
+                }
+            }
+        }
+
     private:
+
         const char* mName;
         TracingMetadata mMetaData;
         DOMHighResTimeStamp mTime;
         nsString mCause;
+
+        // While normally it is not a good idea to make a persistent
+        // root, in this case changing nsDocShell to participate in
+        // cycle collection was deemed too invasive, the stack trace
+        // can't actually cause a cycle, and the markers are only held
+        // here temporarily to boot.
+        mozilla::Maybe<JS::PersistentRooted<JSObject*>> mStackTrace;
     };
 
     // Add new profile timeline markers to this docShell. This will only add
     // markers if the docShell is currently recording profile timeline markers.
     // See nsIDocShell::recordProfileTimelineMarkers
     void AddProfileTimelineMarker(const char* aName,
                                   TracingMetadata aMetaData);
     void AddProfileTimelineMarker(mozilla::UniquePtr<TimelineMarker> &aMarker);
--- a/docshell/test/browser/browser.ini
+++ b/docshell/test/browser/browser.ini
@@ -34,16 +34,17 @@ support-files =
   file_bug852909.png
   file_bug1046022.html
   print_postdata.sjs
   test-form_sjis.html
   timelineMarkers-04.html
   browser_timelineMarkers-frame-02.js
   browser_timelineMarkers-frame-03.js
   browser_timelineMarkers-frame-04.js
+  browser_timelineMarkers-frame-05.js
   head.js
   frame-head.js
 
 [browser_bug134911.js]
 skip-if = e10s # Bug ?????? - BrowserSetForcedCharacterSet() in browser.js references docShell
 [browser_bug234628-1.js]
 skip-if = e10s # Bug ?????? - event handler checks event.target is the content document and test e10s-utils doesn't do that.
 [browser_bug234628-10.js]
@@ -100,8 +101,9 @@ skip-if = e10s
 skip-if = e10s # Bug ?????? - event handler checks event.target is the content document and test e10s-utils doesn't do that.
 [browser_onbeforeunload_navigation.js]
 skip-if = e10s
 [browser_search_notification.js]
 [browser_timelineMarkers-01.js]
 [browser_timelineMarkers-02.js]
 [browser_timelineMarkers-03.js]
 [browser_timelineMarkers-04.js]
+[browser_timelineMarkers-05.js]
new file mode 100644
--- /dev/null
+++ b/docshell/test/browser/browser_timelineMarkers-05.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let URL = '<!DOCTYPE html><style>' +
+          'body {margin:0; padding: 0;} ' +
+          'div {width:100px;height:100px;background:red;} ' +
+          '.resize-change-color {width:50px;height:50px;background:blue;} ' +
+          '.change-color {width:50px;height:50px;background:yellow;} ' +
+          '.add-class {}' +
+          '</style><div></div>';
+URL = "data:text/html;charset=utf8," + encodeURIComponent(URL);
+
+let test = makeTimelineTest("browser_timelineMarkers-frame-05.js", URL);
new file mode 100644
--- /dev/null
+++ b/docshell/test/browser/browser_timelineMarkers-frame-05.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function forceSyncReflow(div) {
+  div.setAttribute('class', 'resize-change-color');
+  // Force a reflow.
+  return div.offsetWidth;
+}
+
+function testSendingEvent() {
+  content.document.body.dispatchEvent(new content.Event("dog"));
+}
+
+function testConsoleTime() {
+  content.console.time("cats");
+}
+
+function testConsoleTimeEnd() {
+  content.console.timeEnd("cats");
+}
+
+let TESTS = [{
+  desc: "Stack trace on sync reflow",
+  searchFor: "Reflow",
+  setup: function(docShell) {
+    let div = content.document.querySelector("div");
+    forceSyncReflow(div);
+  },
+  check: function(markers) {
+    markers = markers.filter(m => m.name == "Reflow");
+    ok(markers.length > 0, "Reflow marker includes stack");
+    ok(markers[0].stack.functionDisplayName == "forceSyncReflow");
+  }
+}, {
+  desc: "Stack trace on DOM event",
+  searchFor: "DOMEvent",
+  setup: function(docShell) {
+    content.document.body.addEventListener("dog",
+                                           function(e) { console.log("hi"); },
+                                           true);
+    testSendingEvent();
+  },
+  check: function(markers) {
+    markers = markers.filter(m => m.name == "DOMEvent");
+    ok(markers.length > 0, "DOMEvent marker includes stack");
+    ok(markers[0].stack.functionDisplayName == "testSendingEvent",
+       "testSendingEvent is on the stack");
+  }
+}, {
+  desc: "Stack trace on console event",
+  searchFor: "ConsoleTime",
+  setup: function(docShell) {
+    testConsoleTime();
+    testConsoleTimeEnd();
+  },
+  check: function(markers) {
+    markers = markers.filter(m => m.name == "ConsoleTime");
+    ok(markers.length > 0, "ConsoleTime marker includes stack");
+    ok(markers[0].stack.functionDisplayName == "testConsoleTime",
+       "testConsoleTime is on the stack");
+    ok(markers[0].endStack.functionDisplayName == "testConsoleTimeEnd",
+       "testConsoleTimeEnd is on the stack");
+  }
+}];
+
+timelineContentTest(TESTS);
--- a/dom/base/Console.cpp
+++ b/dom/base/Console.cpp
@@ -808,30 +808,37 @@ ReifyStack(nsIStackFrame* aStack, nsTArr
 class ConsoleTimelineMarker : public nsDocShell::TimelineMarker
 {
 public:
   ConsoleTimelineMarker(nsDocShell* aDocShell,
                         TracingMetadata aMetaData,
                         const nsAString& aCause)
     : nsDocShell::TimelineMarker(aDocShell, "ConsoleTime", aMetaData, aCause)
   {
+    if (aMetaData == TRACING_INTERVAL_END) {
+      CaptureStack();
+    }
   }
 
   virtual bool Equals(const nsDocShell::TimelineMarker* aOther)
   {
     if (!nsDocShell::TimelineMarker::Equals(aOther)) {
       return false;
     }
     // Console markers must have matching causes as well.
     return GetCause() == aOther->GetCause();
   }
 
   virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
   {
-    aMarker.mCauseName.Construct(GetCause());
+    if (GetMetaData() == TRACING_INTERVAL_START) {
+      aMarker.mCauseName.Construct(GetCause());
+    } else {
+      aMarker.mEndStack = GetStack();
+    }
   }
 };
 
 // Queue a call to a console method. See the CALL_DELAY constant.
 void
 Console::Method(JSContext* aCx, MethodName aMethodName,
                 const nsAString& aMethodString,
                 const Sequence<JS::Value>& aData)
--- a/dom/events/EventListenerManager.cpp
+++ b/dom/events/EventListenerManager.cpp
@@ -1031,18 +1031,20 @@ public:
                       uint16_t aPhase, const nsAString& aCause)
     : nsDocShell::TimelineMarker(aDocShell, "DOMEvent", aMetaData, aCause)
     , mPhase(aPhase)
   {
   }
 
   virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
   {
-    aMarker.mType.Construct(GetCause());
-    aMarker.mEventPhase.Construct(mPhase);
+    if (GetMetaData() == TRACING_INTERVAL_START) {
+      aMarker.mType.Construct(GetCause());
+      aMarker.mEventPhase.Construct(mPhase);
+    }
   }
 
 private:
   uint16_t mPhase;
 };
 
 /**
 * Causes a check for event listeners and processing by them if they exist.
--- a/dom/webidl/ProfileTimelineMarker.webidl
+++ b/dom/webidl/ProfileTimelineMarker.webidl
@@ -10,16 +10,18 @@ dictionary ProfileTimelineLayerRect {
   long width = 0;
   long height = 0;
 };
 
 dictionary ProfileTimelineMarker {
   DOMString name = "";
   DOMHighResTimeStamp start = 0;
   DOMHighResTimeStamp end = 0;
+  object? stack = null;
   /* For ConsoleTime markers.  */
   DOMString causeName;
+  object? endStack = null;
   /* For DOMEvent markers.  */
   DOMString type;
   unsigned short eventPhase;
   /* For Paint markers.  */
   sequence<ProfileTimelineLayerRect> rectangles;
 };
--- a/toolkit/devtools/server/actors/memory.js
+++ b/toolkit/devtools/server/actors/memory.js
@@ -4,16 +4,18 @@
 
 "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");
 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.
@@ -55,27 +57,24 @@ let MemoryActor = protocol.ActorClass({
 
   get dbg() {
     if (!this._dbg) {
       this._dbg = this.parent.makeDebugger();
     }
     return this._dbg;
   },
 
-  initialize: function(conn, parent) {
+  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._framesToCounts = null;
-    this._framesToIndices = null;
-    this._framesToForms = null;
-
+    this._frameCache = frameCache;
     this._onWindowReady = this._onWindowReady.bind(this);
 
     events.on(this.parent, "window-ready", this._onWindowReady);
   },
 
   destroy: function() {
     events.off(this.parent, "window-ready", this._onWindowReady);
 
@@ -119,46 +118,30 @@ let MemoryActor = protocol.ActorClass({
       if (this.dbg.memory.trackingAllocationSites) {
         this.dbg.memory.drainAllocationsLog();
       }
       this._clearFrames();
       this.dbg.removeAllDebuggees();
     }
   },
 
-  _initFrames: function() {
-    if (this._framesToCounts) {
-      // The maps are already initialized.
-      return;
-    }
-
-    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;
+      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._initFrames();
+        nthis._frameCache.initFrames();
       }
       this.dbg.addDebuggees();
     }
   },
 
   /**
    * Take a census of the heap. See js/src/doc/Debugger/Debugger.Memory.md for
    * more information.
@@ -172,17 +155,17 @@ let MemoryActor = protocol.ActorClass({
 
   /**
    * Start recording allocation sites.
    *
    * @param AllocationsRecordingOptions options
    *        See the protocol.js definition of AllocationsRecordingOptions above.
    */
   startRecordingAllocations: method(expectState("attached", function(options = {}) {
-    this._initFrames();
+    this._frameCache.initFrames();
     this.dbg.memory.allocationSamplingProbability = options.probability != null
       ? options.probability
       : 1.0;
     this.dbg.memory.trackingAllocationSites = true;
   }), {
     request: {
       options: Arg(0, "nullable:AllocationsRecordingOptions")
     },
@@ -273,104 +256,28 @@ let MemoryActor = protocol.ActorClass({
 
       // 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);
+      let index = this._frameCache.addFrame(waived);
 
-      packet.allocations.push(this._framesToIndices.get(waived));
+      packet.allocations.push(index);
       packet.allocationsTimestamps.push(timestamp);
     }
 
-    // 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;
+    return this._frameCache.updateFramePacket(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();
     }
   }, {
--- a/toolkit/devtools/server/actors/timeline.js
+++ b/toolkit/devtools/server/actors/timeline.js
@@ -23,16 +23,17 @@
 
 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 {MemoryActor} = require("devtools/server/actors/memory");
 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
 
 /**
  * Type representing an array of numbers as strings, serialized fast(er).
@@ -81,25 +82,32 @@ let TimelineActor = exports.TimelineActo
     /**
      * "ticks" events (from the refresh driver) emitted in tandem with "markers",
      * if this was enabled when the recording started.
      */
     "ticks" : {
       type: "ticks",
       delta: Arg(0, "number"),
       timestamps: Arg(1, "array-of-numbers-as-strings")
+    },
+
+    "frames" : {
+      type: "frames",
+      delta: Arg(0, "number"),
+      frames: Arg(1, "json")
     }
   },
 
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
 
     this._isRecording = false;
     this._startTime = 0;
+    this._stackFrames = null;
 
     // Make sure to get markers from new windows as they become available
     this._onWindowReady = this._onWindowReady.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
@@ -165,16 +173,35 @@ let TimelineActor = exports.TimelineActo
 
     let endTime = this.docShells[0].now();
     let markers = [];
 
     for (let docShell of this.docShells) {
       markers = [...markers, ...docShell.popProfileTimelineMarkers()];
     }
 
+    // The docshell may return markers with stack traces attached.
+    // Here we transform the stack traces via the stack frame cache,
+    // which lets us preserve tail sharing when transferring the
+    // frames to the client.  We must waive xrays here because Firefox
+    // doesn't understand that the Debugger.Frame object is safe to
+    // use from chrome.  See Tutorial-Alloc-Log-Tree.md.
+    for (let marker of markers) {
+      if (marker.stack) {
+        marker.stack = this._stackFrames.addFrame(Cu.waiveXrays(marker.stack));
+      }
+      if (marker.endStack) {
+        marker.endStack = this._stackFrames.addFrame(Cu.waiveXrays(marker.endStack));
+      }
+    }
+
+    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._framerateActor) {
       events.emit(this, "ticks", endTime, this._framerateActor.getPendingTicks());
@@ -201,23 +228,26 @@ let TimelineActor = exports.TimelineActo
    * Start recording profile markers.
    */
   start: method(function({ withMemory, withTicks }) {
     if (this._isRecording) {
       return;
     }
     this._isRecording = true;
     this._startTime = this.docShells[0].now();
+    this._stackFrames = new StackFrameCache();
+    this._stackFrames.initFrames();
 
     for (let docShell of this.docShells) {
       docShell.recordProfileTimelineMarkers = true;
     }
 
     if (withMemory) {
-      this._memoryActor = new MemoryActor(this.conn, this.tabActor);
+      this._memoryActor = new MemoryActor(this.conn, this.tabActor,
+                                          this._stackFrames);
       events.emit(this, "memory", this._startTime, this._memoryActor.measure());
     }
     if (withTicks) {
       this._framerateActor = new FramerateActor(this.conn, this.tabActor);
       this._framerateActor.startRecording();
     }
 
     this._pullTimelineData();
@@ -235,16 +265,17 @@ let TimelineActor = exports.TimelineActo
   /**
    * Stop recording profile markers.
    */
   stop: method(function() {
     if (!this._isRecording) {
       return;
     }
     this._isRecording = false;
+    this._stackFrames = null;
 
     if (this._memoryActor) {
       this._memoryActor = null;
     }
     if (this._framerateActor) {
       this._framerateActor.stopRecording();
       this._framerateActor = null;
     }
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/utils/stack.js
@@ -0,0 +1,202 @@
+/* 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";
+
+let {Class} = require("sdk/core/heritage");
+
+/**
+ * A helper class that stores stack frame objects.  Each frame is
+ * assigned an index, and if a frame is added more than once, the same
+ * index is used.  Users of the class can get an array of all frames
+ * that have been added.
+ */
+let StackFrameCache = Class({
+  /**
+   * Initialize this object.
+   */
+  initialize: function() {
+    this._framesToCounts = null;
+    this._framesToIndices = null;
+    this._framesToForms = null;
+    this._lastEventSize = -1;
+  },
+
+  /**
+   * Prepare to accept frames.
+   */
+  initFrames: function() {
+    if (this._framesToCounts) {
+      // The maps are already initialized.
+      return;
+    }
+
+    this._framesToCounts = new Map();
+    this._framesToIndices = new Map();
+    this._framesToForms = new Map();
+    this._lastEventSize = -1;
+  },
+
+  /**
+   * Forget all stored frames and reset to the initialized state.
+   */
+  clearFrames: function() {
+    this._framesToCounts.clear();
+    this._framesToCounts = null;
+    this._framesToIndices.clear();
+    this._framesToIndices = null;
+    this._framesToForms.clear();
+    this._framesToForms = null;
+    this._lastEventSize = -1;
+  },
+
+  /**
+   * Add a frame to this stack frame cache, and return the index of
+   * the frame.
+   */
+  addFrame: function(frame) {
+    this._assignFrameIndices(frame);
+    this._createFrameForms(frame);
+    this._countFrame(frame);
+    return this._framesToIndices.get(frame);
+  },
+
+  /**
+   * A helper method for the memory actor.  This populates the packet
+   * object with "frames" and "counts" properties.  Each of these
+   * properties will be an array indexed by frame ID.  "frames" will
+   * contain frame objects (see makeEvent) and "counts" will hold
+   * allocation counts for each frame.
+   *
+   * @param packet
+   *        The packet to update.
+   *
+   * @returns packet
+   */
+  updateFramePacket: function(packet) {
+    // 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;
+  },
+
+  /**
+   * If any new stack frames have been added to this cache since the
+   * last call to makeEvent (clearing the cache also resets the "last
+   * call"), then return a new array describing the new frames.  If no
+   * new frames are available, return null.
+   *
+   * The frame cache assumes that the user of the cache keeps track of
+   * all previously-returned arrays and, in theory, concatenates them
+   * all to form a single array holding all frames added to the cache
+   * since the last reset.  This concatenated array can be indexed by
+   * the frame ID.  The array returned by this function, though, is
+   * dense and starts at 0.
+   *
+   * Each element in the array is an object of the form:
+   * {
+   *   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: <frame ID -- an index into the concatenated array mentioned above>
+   * }
+   *
+   * The intent of this approach is to make it simpler to efficiently
+   * send frame information over the debugging protocol, by only
+   * sending new frames.
+   *
+   * @returns array or null
+   */
+  makeEvent: function() {
+    const size = this._framesToForms.size;
+    if (!size || size <= this._lastEventSize) {
+      return null;
+    }
+
+    let packet = Array(size - this._lastEventSize).fill(null);
+    for (let [stack, index] of this._framesToIndices) {
+      if (index > this._lastEventSize) {
+        packet[index - this._lastEventSize - 1] = this._framesToForms.get(stack);
+      }
+    }
+
+    this._lastEventSize = size;
+
+    return packet;
+  },
+
+  /**
+   * 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);
+    }
+  }
+});
+
+exports.StackFrameCache = StackFrameCache;
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -68,12 +68,13 @@ EXTRA_JS_MODULES.devtools.server.actors 
     'actors/webconsole.js',
     'actors/webgl.js',
 ]
 
 EXTRA_JS_MODULES.devtools.server.actors.utils += [
     'actors/utils/automation-timeline.js',
     'actors/utils/make-debugger.js',
     'actors/utils/map-uri-to-addon-id.js',
-    'actors/utils/ScriptStore.js'
+    'actors/utils/ScriptStore.js',
+    'actors/utils/stack.js',
 ]
 
 FAIL_ON_WARNINGS = True