Bug 972045 - Add a compact representation for call stacks in SpiderMonkey. r=jimb
☠☠ backed out by 9688e85bd87c ☠ ☠
authorNick Fitzgerald <fitzgen@mozilla.com>
Thu, 24 Apr 2014 10:04:00 -0400
changeset 180432 ace4dd4263499c5cc8592af910ae464ee212f87c
parent 180431 d5a514b9a0db31234c541fb0bdf65d72a2afee9e
child 180433 114aa4935a4390e933593b39aaf8c65edef01eac
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersjimb
bugs972045
milestone31.0a1
Bug 972045 - Add a compact representation for call stacks in SpiderMonkey. r=jimb
js/public/MemoryMetrics.h
js/src/builtin/TestingFunctions.cpp
js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js
js/src/jit-test/tests/saved-stacks/evals.js
js/src/jit-test/tests/saved-stacks/function-display-name.js
js/src/jit-test/tests/saved-stacks/gc-frame-cache.js
js/src/jit-test/tests/saved-stacks/generators.js
js/src/jit-test/tests/saved-stacks/get-set.js
js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js
js/src/jit-test/tests/saved-stacks/native-calls.js
js/src/jit-test/tests/saved-stacks/principals-01.js
js/src/jit-test/tests/saved-stacks/principals-02.js
js/src/jit-test/tests/saved-stacks/proxy-handlers.js
js/src/jit-test/tests/saved-stacks/same-stack.js
js/src/jit-test/tests/saved-stacks/self-hosted.js
js/src/jit-test/tests/saved-stacks/shared-parent-frames.js
js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js
js/src/jscompartment.cpp
js/src/jscompartment.h
js/src/moz.build
js/src/vm/MemoryMetrics.cpp
js/src/vm/SavedStacks.cpp
js/src/vm/SavedStacks.h
js/src/vm/Stack.cpp
--- a/js/public/MemoryMetrics.h
+++ b/js/public/MemoryMetrics.h
@@ -507,17 +507,18 @@ struct CompartmentStats
     macro(Other,   NotLiveGCThing, ionData) \
     macro(Other,   NotLiveGCThing, typeInferenceTypeScripts) \
     macro(Other,   NotLiveGCThing, typeInferenceAllocationSiteTables) \
     macro(Other,   NotLiveGCThing, typeInferenceArrayTypeTables) \
     macro(Other,   NotLiveGCThing, typeInferenceObjectTypeTables) \
     macro(Other,   NotLiveGCThing, compartmentObject) \
     macro(Other,   NotLiveGCThing, crossCompartmentWrappersTable) \
     macro(Other,   NotLiveGCThing, regexpCompartment) \
-    macro(Other,   NotLiveGCThing, debuggeesSet)
+    macro(Other,   NotLiveGCThing, debuggeesSet) \
+    macro(Other,   IsLiveGCThing,  savedStacksSet)
 
     CompartmentStats()
       : FOR_EACH_SIZE(ZERO_SIZE)
         objectsExtra(),
         extra()
     {}
 
     CompartmentStats(const CompartmentStats &other)
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -18,16 +18,17 @@
 
 #include "jit/AsmJS.h"
 #include "jit/AsmJSLink.h"
 #include "js/StructuredClone.h"
 #include "vm/ForkJoin.h"
 #include "vm/GlobalObject.h"
 #include "vm/Interpreter.h"
 #include "vm/ProxyObject.h"
+#include "vm/SavedStacks.h"
 #include "vm/TraceLogging.h"
 
 #include "jscntxtinlines.h"
 #include "jsobjinlines.h"
 
 using namespace js;
 using namespace JS;
 
@@ -845,16 +846,35 @@ CountHeap(JSContext *cx, unsigned argc, 
         JS_ReportOutOfMemory(cx);
         return false;
     }
 
     args.rval().setNumber(double(counter));
     return true;
 }
 
+static bool
+GetSavedFrameCount(JSContext *cx, unsigned argc, jsval *vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    args.rval().setNumber(cx->compartment()->savedStacks().count());
+    return true;
+}
+
+static bool
+SaveStack(JSContext *cx, unsigned argc, jsval *vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    Rooted<SavedFrame*> frame(cx);
+    if (!cx->compartment()->savedStacks().saveCurrentStack(cx, &frame))
+        return false;
+    args.rval().setObject(*frame.get());
+    return true;
+}
+
 #if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
 static bool
 OOMAfterAllocations(JSContext *cx, unsigned argc, jsval *vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     if (args.length() != 1) {
         JS_ReportError(cx, "count argument required");
         return false;
@@ -1545,16 +1565,25 @@ static const JSFunctionSpecWithHelp Test
 "countHeap([start[, kind[, thing]]])",
 "  Count the number of live GC things in the heap or things reachable from\n"
 "  start when it is given and is not null. kind is either 'all' (default) to\n"
 "  count all things or one of 'object', 'double', 'string', 'function'\n"
 "  to count only things of that kind. If kind is the string 'specific',\n"
 "  then you can provide an extra argument with some specific traceable\n"
 "  thing to count.\n"),
 
+    JS_FN_HELP("getSavedFrameCount", GetSavedFrameCount, 0, 0,
+"getSavedFrameCount()",
+"  Return the number of SavedFrame instances stored in this compartment's\n"
+"  SavedStacks cache."),
+
+    JS_FN_HELP("saveStack", SaveStack, 0, 0,
+"saveStack()",
+"  Capture a stack.\n"),
+
 #if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
     JS_FN_HELP("oomAfterAllocations", OOMAfterAllocations, 1, 0,
 "oomAfterAllocations(count)",
 "  After 'count' js_malloc memory allocations, fail every following allocation\n"
 "  (return NULL)."),
 #endif
 
     JS_FN_HELP("makeFinalizeObserver", MakeFinalizeObserver, 0, 0,
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js
@@ -0,0 +1,3 @@
+// The SavedFrame constructor shouldn't have been exposed to JS on the global.
+
+assertEq(typeof SavedFrame, "undefined");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/evals.js
@@ -0,0 +1,38 @@
+// Test that we can save stacks with direct and indirect eval calls.
+
+const directEval = (function iife() {
+  return eval("(" + function evalFrame() {
+    return saveStack();
+  } + "())");
+}());
+
+assertEq(directEval.source.contains("> eval"), true);
+assertEq(directEval.functionDisplayName, "evalFrame");
+
+assertEq(directEval.parent.source.contains("> eval"), true);
+
+assertEq(directEval.parent.parent.source.contains("> eval"), false);
+assertEq(directEval.parent.parent.functionDisplayName, "iife");
+
+assertEq(directEval.parent.parent.parent.source.contains("> eval"), false);
+
+assertEq(directEval.parent.parent.parent.parent, null);
+
+const lave = eval;
+const indirectEval = (function iife() {
+  return lave("(" + function evalFrame() {
+    return saveStack();
+  } + "())");
+}());
+
+assertEq(indirectEval.source.contains("> eval"), true);
+assertEq(indirectEval.functionDisplayName, "evalFrame");
+
+assertEq(indirectEval.parent.source.contains("> eval"), true);
+
+assertEq(indirectEval.parent.parent.source.contains("> eval"), false);
+assertEq(indirectEval.parent.parent.functionDisplayName, "iife");
+
+assertEq(indirectEval.parent.parent.parent.source.contains("> eval"), false);
+
+assertEq(indirectEval.parent.parent.parent.parent, null);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/function-display-name.js
@@ -0,0 +1,17 @@
+// Test the functionDisplayName of SavedFrame instances.
+
+function uno() { return dos(); }
+const dos = () => tres.quattro();
+const tres = {
+  quattro: () => saveStack()
+};
+
+const frame = uno();
+
+assertEq(frame.functionDisplayName, "tres.quattro");
+assertEq(frame.parent.functionDisplayName, "dos");
+assertEq(frame.parent.parent.functionDisplayName, "uno");
+assertEq(frame.parent.parent.parent.functionDisplayName, null);
+
+assertEq(frame.parent.parent.parent.parent, null);
+
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js
@@ -0,0 +1,76 @@
+// Test that SavedFrame instances get removed from the SavedStacks frames cache
+// after a GC.
+
+const FUZZ_FACTOR = 3;
+
+function assertAboutEq(actual, expected) {
+  if (Math.abs(actual - expected) > FUZZ_FACTOR)
+    throw new Error("Assertion failed: expected about " + expected + ", got " + actual +
+                    ". FUZZ_FACTOR = " + FUZZ_FACTOR);
+}
+
+const stacks = [];
+
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+stacks.push(saveStack());
+
+assertAboutEq(getSavedFrameCount(), 50);
+
+while (stacks.length) {
+  stacks.pop();
+}
+gc();
+
+stacks = null;
+gc();
+
+assertAboutEq(getSavedFrameCount(), 0);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/generators.js
@@ -0,0 +1,15 @@
+// Test that we can save stacks which have generator frames.
+
+const { value: frame } = (function iife1() {
+  return (function* generator() {
+    yield (function iife2() {
+      return saveStack();
+    }());
+  }()).next();
+}());
+
+assertEq(frame.functionDisplayName, "iife2");
+assertEq(frame.parent.functionDisplayName, "generator");
+assertEq(frame.parent.parent.functionDisplayName, "iife1");
+assertEq(frame.parent.parent.parent.functionDisplayName, null);
+assertEq(frame.parent.parent.parent.parent, null);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/get-set.js
@@ -0,0 +1,25 @@
+// Test that we can save stacks with getter and setter function frames.
+
+function assertStackLengthEq(stack, expectedLength) {
+  let actual = 0;
+  while (stack) {
+    actual++;
+    stack = stack.parent;
+  }
+  assertEq(actual, expectedLength);
+}
+
+const get = { get s() { return saveStack(); } }.s;
+assertStackLengthEq(get, 2);
+
+let set;
+try {
+  ({
+    set s(v) {
+      throw saveStack();
+    }
+  }).s = 1;
+} catch (s) {
+  set = s;
+}
+assertStackLengthEq(set, 2);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js
@@ -0,0 +1,21 @@
+// Test that you can't call the SavedFrame constructor and can only use
+// SavedFrame's getters on SavedFrame instances.
+
+load(libdir + "asserts.js");
+
+let proto = Object.getPrototypeOf(saveStack());
+
+// Can't create new SavedFrame instances by hand.
+assertThrowsInstanceOf(() => {
+  new proto.constructor();
+}, TypeError);
+
+for (let p of ["source", "line", "column", "functionDisplayName", "parent"]) {
+  // The getters shouldn't work on the prototype.
+  assertThrowsInstanceOf(() => proto[p], TypeError);
+
+  // Nor should they work on random objects.
+  let o = {};
+  Object.defineProperty(o, p, Object.getOwnPropertyDescriptor(proto, p));
+  assertThrowsInstanceOf(() => o[p], TypeError);
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/native-calls.js
@@ -0,0 +1,12 @@
+// Test that we can save stacks with native code on the stack.
+
+// Unlike Array.prototype.map, Array.prototype.filter is not self-hosted.
+const filter = (function iife() {
+  try {
+    [3].filter(n => { throw saveStack() });
+  } catch (s) {
+    return s;
+  }
+}());
+
+assertEq(filter.parent.functionDisplayName, "iife");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/principals-01.js
@@ -0,0 +1,70 @@
+// Test that SavedFrame.prototype.parent gives the next older frame whose
+// principals are subsumed by the caller's principals.
+
+// Given a string of letters |expected|, say "abc", assert that the stack
+// contains calls to a series of functions named by the next letter from
+// the string, say a, b, and then c. Younger frames appear earlier in
+// |expected| than older frames.
+function check(expected, stack) {
+  print("check(" + uneval(expected) + ") against:\n" + stack);
+  count++;
+
+  while (stack.length && expected.length) {
+    assertEq(stack.shift(), expected[0]);
+    expected = expected.slice(1);
+  }
+
+  if (expected.length > 0) {
+    throw new Error("Missing frames for: " + expected);
+  }
+  if (stack.length > 0 && !stack.every(s => s === null)) {
+    throw new Error("Unexpected extra frame(s):\n" + stack);
+  }
+}
+
+// Go from a SavedFrame linked list to an array of function display names.
+function extract(stack) {
+  const results = [];
+  while (stack) {
+    results.push(stack.functionDisplayName);
+    stack = stack.parent;
+  }
+  return results;
+}
+
+const low  = newGlobal({ principal: 0       });
+const mid  = newGlobal({ principal: 0xffff  });
+const high = newGlobal({ principal: 0xfffff });
+
+var count = 0;
+
+     eval('function a() { check("a",        extract(saveStack())); b(); }');
+low .eval('function b() { check("b",        extract(saveStack())); c(); }');
+mid .eval('function c() { check("cba",      extract(saveStack())); d(); }');
+high.eval('function d() { check("dcba",     extract(saveStack())); e(); }');
+     eval('function e() { check("edcba",    extract(saveStack())); f(); }'); // no principal, so checks skipped
+low .eval('function f() { check("fb",       extract(saveStack())); g(); }');
+mid .eval('function g() { check("gfecba",   extract(saveStack())); h(); }');
+high.eval('function h() { check("hgfedcba", extract(saveStack()));      }');
+
+// Make everyone's functions visible to each other, as needed.
+     b = low .b;
+low .c = mid .c;
+mid .d = high.d;
+high.e =      e;
+     f = low .f;
+low .g = mid .g;
+mid .h = high.h;
+
+low.check = mid.check = high.check = check;
+
+// They each must have their own extract so that CCWs don't mess up the
+// principals when we ask for the parent property.
+low. eval("" + extract);
+mid. eval("" + extract);
+high.eval("" + extract);
+
+// Kick the whole process off.
+a();
+
+assertEq(count, 8);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/principals-02.js
@@ -0,0 +1,56 @@
+// Test that SavedFrame.prototype.toString only shows frames whose principal is
+// subsumed by the caller's principal.
+
+var count = 0;
+
+// Given a string of letters |expected|, say "abc", assert that the stack
+// contains calls to a series of functions named by the next letter from
+// the string, say a, b, and then c. Younger frames appear earlier in
+// |expected| than older frames.
+function check(expected, stack) {
+  print("check(" + uneval(expected) + ") against:\n" + stack);
+  count++;
+
+  // Extract only the function names from the stack trace. Omit the frames
+  // for the top-level evaluation, if it is present.
+  const frames = stack
+    .split("\n")
+    .filter(f => f.match(/^.@/))
+    .map(f => f.replace(/@.*$/g, ""));
+
+  // Check the function names against the expected sequence.
+  assertEq(frames.length, expected.length);
+  for (var i = 0; i < expected.length; i++) {
+    assertEq(frames[i], expected[i]);
+  }
+}
+
+var low = newGlobal({ principal: 0 });
+var mid = newGlobal({ principal: 0xffff });
+var high = newGlobal({ principal: 0xfffff });
+
+     eval('function a() { check("a",        saveStack().toString()); b(); }');
+low .eval('function b() { check("b",        saveStack().toString()); c(); }');
+mid .eval('function c() { check("cba",      saveStack().toString()); d(); }');
+high.eval('function d() { check("dcba",     saveStack().toString()); e(); }');
+     eval('function e() { check("edcba",    saveStack().toString()); f(); }'); // no principal, so checks skipped
+low .eval('function f() { check("fb",       saveStack().toString()); g(); }');
+mid .eval('function g() { check("gfecba",   saveStack().toString()); h(); }');
+high.eval('function h() { check("hgfedcba", saveStack().toString());      }');
+
+// Make everyone's functions visible to each other, as needed.
+     b = low .b;
+low .c = mid .c;
+mid .d = high.d;
+high.e =      e;
+     f = low .f;
+low .g = mid .g;
+mid .h = high.h;
+
+low.check = mid.check = high.check = check;
+
+// Kick the whole process off.
+a();
+
+assertEq(count, 8);
+
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/proxy-handlers.js
@@ -0,0 +1,10 @@
+// Test that we can save stacks with proxy handler frames.
+
+const stack = (function iife() {
+  return (new Proxy({}, {
+    get: function get(t, n, r) { return saveStack(); }
+  })).stack;
+}());
+
+assertEq(stack.functionDisplayName, "get");
+assertEq(stack.parent.functionDisplayName, "iife");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/same-stack.js
@@ -0,0 +1,12 @@
+// Test that the same saved stack is only ever allocated once.
+
+const stacks = [];
+
+for (let i = 0; i < 10; i++) {
+  stacks.push(saveStack());
+}
+
+const s = stacks.pop();
+for (let stack of stacks) {
+  assertEq(s, stack);
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/self-hosted.js
@@ -0,0 +1,26 @@
+// Test that we can save stacks with self-hosted function frames in them.
+
+const map = (function () {
+  return [3].map(n => saveStack()).pop();
+}());
+
+assertEq(map.parent.functionDisplayName, "map");
+assertEq(map.parent.source, "self-hosted");
+
+const reduce = (function () {
+  return [3].reduce(() => saveStack(), 3);
+}());
+
+assertEq(reduce.parent.functionDisplayName, "reduce");
+assertEq(reduce.parent.source, "self-hosted");
+
+const forEach = (function () {
+  try {
+    [3].forEach(n => { throw saveStack() });
+  } catch (s) {
+    return s;
+  }
+}());
+
+assertEq(forEach.parent.functionDisplayName, "forEach");
+assertEq(forEach.parent.source, "self-hosted");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js
@@ -0,0 +1,19 @@
+// Test that parent frames are shared when the older portions of two stacks are
+// the same.
+
+let f1, f2;
+
+function dos() {
+  f1 = saveStack();
+  f2 = saveStack();
+}
+
+(function uno() {
+  dos();
+}());
+
+
+// Different youngest frames.
+assertEq(f1 == f2, false);
+// However the parents should be the same.
+assertEq(f1.parent, f2.parent);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js
@@ -0,0 +1,8 @@
+// Test that stringify'ing a saved frame with self-hosted parent frames doesn't
+// include the self-hosted parent frame in the output.
+
+const map = (function () {
+  return [3].map(n => saveStack()).pop();
+}());
+
+assertEq(map.toString().contains("@self-hosted:"), false);
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -108,16 +108,19 @@ JSCompartment::init(JSContext *cx)
 
     if (!regExps.init(cx))
         return false;
 
     enumerators = NativeIterator::allocateSentinel(cx);
     if (!enumerators)
         return false;
 
+    if (!savedStacks_.init())
+        return false;
+
     return debuggees.init(0);
 }
 
 #ifdef JS_ION
 jit::JitRuntime *
 JSRuntime::createJitRuntime(JSContext *cx)
 {
     // The shared stubs are created in the atoms compartment, which may be
@@ -558,16 +561,17 @@ JSCompartment::sweep(FreeOp *fop, bool r
 
         /* Remove dead references held weakly by the compartment. */
 
         sweepBaseShapeTable();
         sweepInitialShapeTable();
         sweepNewTypeObjectTable(newTypeObjects);
         sweepNewTypeObjectTable(lazyTypeObjects);
         sweepCallsiteClones();
+        savedStacks_.sweep(rt);
 
         if (global_ && IsObjectAboutToBeFinalized(global_.unsafeGet()))
             global_ = nullptr;
 
         if (selfHostingScriptSource &&
             IsObjectAboutToBeFinalized((JSObject **) selfHostingScriptSource.unsafeGet()))
         {
             selfHostingScriptSource = nullptr;
@@ -660,16 +664,18 @@ JSCompartment::clearTables()
     if (baseShapes.initialized())
         baseShapes.clear();
     if (initialShapes.initialized())
         initialShapes.clear();
     if (newTypeObjects.initialized())
         newTypeObjects.clear();
     if (lazyTypeObjects.initialized())
         lazyTypeObjects.clear();
+    if (savedStacks_.initialized())
+        savedStacks_.clear();
 }
 
 void
 JSCompartment::setObjectMetadataCallback(js::ObjectMetadataCallback callback)
 {
     // Clear any jitcode in the runtime, which behaves differently depending on
     // whether there is a creation callback.
     ReleaseAllJITCode(runtime_->defaultFreeOp());
@@ -910,27 +916,29 @@ void
 JSCompartment::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
                                       size_t *tiAllocationSiteTables,
                                       size_t *tiArrayTypeTables,
                                       size_t *tiObjectTypeTables,
                                       size_t *compartmentObject,
                                       size_t *shapesCompartmentTables,
                                       size_t *crossCompartmentWrappersArg,
                                       size_t *regexpCompartment,
-                                      size_t *debuggeesSet)
+                                      size_t *debuggeesSet,
+                                      size_t *savedStacksSet)
 {
     *compartmentObject += mallocSizeOf(this);
     types.addSizeOfExcludingThis(mallocSizeOf, tiAllocationSiteTables,
                                  tiArrayTypeTables, tiObjectTypeTables);
     *shapesCompartmentTables += baseShapes.sizeOfExcludingThis(mallocSizeOf)
                               + initialShapes.sizeOfExcludingThis(mallocSizeOf)
                               + newTypeObjects.sizeOfExcludingThis(mallocSizeOf)
                               + lazyTypeObjects.sizeOfExcludingThis(mallocSizeOf);
     *crossCompartmentWrappersArg += crossCompartmentWrappers.sizeOfExcludingThis(mallocSizeOf);
     *regexpCompartment += regExps.sizeOfExcludingThis(mallocSizeOf);
     *debuggeesSet += debuggees.sizeOfExcludingThis(mallocSizeOf);
+    *savedStacksSet += savedStacks_.sizeOfExcludingThis(mallocSizeOf);
 }
 
 void
 JSCompartment::adoptWorkerAllocator(Allocator *workerAllocator)
 {
     zone()->allocator.arenas.adoptArenas(runtimeFromMainThread(), &workerAllocator->arenas);
 }
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -8,16 +8,17 @@
 #define jscompartment_h
 
 #include "mozilla/MemoryReporting.h"
 
 #include "builtin/TypedObject.h"
 #include "gc/Zone.h"
 #include "vm/GlobalObject.h"
 #include "vm/PIC.h"
+#include "vm/SavedStacks.h"
 
 namespace js {
 
 namespace jit {
 class JitCompartment;
 }
 
 namespace gc {
@@ -193,16 +194,18 @@ struct JSCompartment
     /* Type information about the scripts and objects in this compartment. */
     js::types::TypeCompartment   types;
 
     void                         *data;
 
   private:
     js::ObjectMetadataCallback   objectMetadataCallback;
 
+    js::SavedStacks              savedStacks_;
+
     js::WrapperMap               crossCompartmentWrappers;
 
   public:
     /* Last time at which an animation was played for a global in this compartment. */
     int64_t                      lastAnimationTime;
 
     js::RegExpCompartment        regExps;
 
@@ -219,17 +222,18 @@ struct JSCompartment
     void addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
                                 size_t *tiAllocationSiteTables,
                                 size_t *tiArrayTypeTables,
                                 size_t *tiObjectTypeTables,
                                 size_t *compartmentObject,
                                 size_t *shapesCompartmentTables,
                                 size_t *crossCompartmentWrappers,
                                 size_t *regexpCompartment,
-                                size_t *debuggeesSet);
+                                size_t *debuggeesSet,
+                                size_t *savedStacksSet);
 
     /*
      * Shared scope property tree, and arena-pool for allocating its nodes.
      */
     js::PropertyTree             propertyTree;
 
     /* Set of all unowned base shapes in the compartment. */
     js::BaseShapeSet             baseShapes;
@@ -337,16 +341,18 @@ struct JSCompartment
     void clearTables();
 
     bool hasObjectMetadataCallback() const { return objectMetadataCallback; }
     void setObjectMetadataCallback(js::ObjectMetadataCallback callback);
     bool callObjectMetadataCallback(JSContext *cx, JSObject **obj) const {
         return objectMetadataCallback(cx, obj);
     }
 
+    js::SavedStacks &savedStacks() { return savedStacks_; }
+
     void findOutgoingEdges(js::gc::ComponentFinder<JS::Zone> &finder);
 
     js::DtoaCache dtoaCache;
 
     /* Random number generator state, used by jsmath.cpp. */
     uint64_t rngState;
 
   private:
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -174,16 +174,17 @@ UNIFIED_SOURCES += [
     'vm/OldDebugAPI.cpp',
     'vm/PIC.cpp',
     'vm/Probes.cpp',
     'vm/PropertyKey.cpp',
     'vm/ProxyObject.cpp',
     'vm/RegExpObject.cpp',
     'vm/RegExpStatics.cpp',
     'vm/Runtime.cpp',
+    'vm/SavedStacks.cpp',
     'vm/ScopeObject.cpp',
     'vm/SelfHosting.cpp',
     'vm/Shape.cpp',
     'vm/SharedArrayObject.cpp',
     'vm/SPSProfiler.cpp',
     'vm/Stack.cpp',
     'vm/String.cpp',
     'vm/StringBuffer.cpp',
--- a/js/src/vm/MemoryMetrics.cpp
+++ b/js/src/vm/MemoryMetrics.cpp
@@ -254,17 +254,18 @@ StatsCompartmentCallback(JSRuntime *rt, 
     compartment->addSizeOfIncludingThis(rtStats->mallocSizeOf_,
                                         &cStats.typeInferenceAllocationSiteTables,
                                         &cStats.typeInferenceArrayTypeTables,
                                         &cStats.typeInferenceObjectTypeTables,
                                         &cStats.compartmentObject,
                                         &cStats.shapesMallocHeapCompartmentTables,
                                         &cStats.crossCompartmentWrappersTable,
                                         &cStats.regexpCompartment,
-                                        &cStats.debuggeesSet);
+                                        &cStats.debuggeesSet,
+                                        &cStats.savedStacksSet);
 }
 
 static void
 StatsArenaCallback(JSRuntime *rt, void *data, gc::Arena *arena,
                    JSGCTraceKind traceKind, size_t thingSize)
 {
     RuntimeStats *rtStats = static_cast<StatsClosure *>(data)->rtStats;
 
new file mode 100644
--- /dev/null
+++ b/js/src/vm/SavedStacks.cpp
@@ -0,0 +1,512 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+
+#include "vm/SavedStacks.h"
+
+#include "jscompartment.h"
+#include "jsnum.h"
+
+#include "vm/GlobalObject.h"
+#include "vm/StringBuffer.h"
+
+#include "jsobjinlines.h"
+
+using mozilla::AddToHash;
+using mozilla::HashString;
+
+namespace js {
+
+/* static */ HashNumber
+SavedFrame::HashPolicy::hash(const Lookup &lookup)
+{
+    return AddToHash(HashString(lookup.source->chars(), lookup.source->length()),
+                     lookup.line,
+                     lookup.column,
+                     lookup.functionDisplayName,
+                     SavedFramePtrHasher::hash(lookup.parent),
+                     JSPrincipalsPtrHasher::hash(lookup.principals));
+}
+
+/* static */ bool
+SavedFrame::HashPolicy::match(SavedFrame *existing, const Lookup &lookup)
+{
+    if (existing->getLine() != lookup.line)
+        return false;
+
+    if (existing->getColumn() != lookup.column)
+        return false;
+
+    if (existing->getParent() != lookup.parent)
+        return false;
+
+    if (existing->getPrincipals() != lookup.principals)
+        return false;
+
+    JSAtom *source = existing->getSource();
+    if (source->length() != lookup.source->length())
+        return false;
+    if (source != lookup.source)
+        return false;
+
+    JSAtom *functionDisplayName = existing->getFunctionDisplayName();
+    if (functionDisplayName) {
+        if (!lookup.functionDisplayName)
+            return false;
+        if (functionDisplayName->length() != lookup.functionDisplayName->length())
+            return false;
+        if (0 != CompareAtoms(functionDisplayName, lookup.functionDisplayName))
+            return false;
+    } else if (lookup.functionDisplayName) {
+        return false;
+    }
+
+    return true;
+}
+
+/* static */ void
+SavedFrame::HashPolicy::rekey(Key &key, const Key &newKey)
+{
+    key = newKey;
+}
+
+/* static */ const Class SavedFrame::class_ = {
+    "SavedFrame",
+    JSCLASS_HAS_PRIVATE | JSCLASS_IMPLEMENTS_BARRIERS |
+    JSCLASS_HAS_RESERVED_SLOTS(SavedFrame::JSSLOT_COUNT),
+
+    JS_PropertyStub,       // addProperty
+    JS_DeletePropertyStub, // delProperty
+    JS_PropertyStub,       // getProperty
+    JS_StrictPropertyStub, // setProperty
+    JS_EnumerateStub,      // enumerate
+    JS_ResolveStub,        // resolve
+    JS_ConvertStub,        // convert
+
+    SavedFrame::finalize   // finalize
+};
+
+/* static */ void
+SavedFrame::finalize(FreeOp *fop, JSObject *obj)
+{
+    JSPrincipals *p = obj->as<SavedFrame>().getPrincipals();
+    if (p) {
+        JSRuntime *rt = obj->runtimeFromMainThread();
+        JS_DropPrincipals(rt, p);
+    }
+}
+
+JSAtom *
+SavedFrame::getSource()
+{
+    const Value &v = getReservedSlot(JSSLOT_SOURCE);
+    JSString *s = v.toString();
+    return &s->asAtom();
+}
+
+size_t
+SavedFrame::getLine()
+{
+    const Value &v = getReservedSlot(JSSLOT_LINE);
+    return v.toInt32();
+}
+
+size_t
+SavedFrame::getColumn()
+{
+    const Value &v = getReservedSlot(JSSLOT_COLUMN);
+    return v.toInt32();
+}
+
+JSAtom *
+SavedFrame::getFunctionDisplayName()
+{
+    const Value &v = getReservedSlot(JSSLOT_FUNCTIONDISPLAYNAME);
+    if (v.isNull())
+        return nullptr;
+    JSString *s = v.toString();
+    return &s->asAtom();
+}
+
+SavedFrame *
+SavedFrame::getParent()
+{
+    const Value &v = getReservedSlot(JSSLOT_PARENT);
+    return v.isObject() ? &v.toObject().as<SavedFrame>() : nullptr;
+}
+
+JSPrincipals *
+SavedFrame::getPrincipals()
+{
+    const Value &v = getReservedSlot(JSSLOT_PRINCIPALS);
+    if (v.isUndefined())
+        return nullptr;
+    return static_cast<JSPrincipals *>(v.toPrivate());
+}
+
+void
+SavedFrame::initFromLookup(Lookup &lookup)
+{
+    JS_ASSERT(lookup.source);
+    JS_ASSERT(getReservedSlot(JSSLOT_SOURCE).isUndefined());
+    setReservedSlot(JSSLOT_SOURCE, StringValue(lookup.source));
+
+    setReservedSlot(JSSLOT_LINE, NumberValue(lookup.line));
+    setReservedSlot(JSSLOT_COLUMN, NumberValue(lookup.column));
+    setReservedSlot(JSSLOT_FUNCTIONDISPLAYNAME,
+                    lookup.functionDisplayName
+                        ? StringValue(lookup.functionDisplayName)
+                        : NullValue());
+    setReservedSlot(JSSLOT_PARENT, ObjectOrNullValue(lookup.parent));
+    setReservedSlot(JSSLOT_PRIVATE_PARENT, PrivateValue(lookup.parent));
+
+    JS_ASSERT(getReservedSlot(JSSLOT_PRINCIPALS).isUndefined());
+    if (lookup.principals)
+        JS_HoldPrincipals(lookup.principals);
+    setReservedSlot(JSSLOT_PRINCIPALS, PrivateValue(lookup.principals));
+}
+
+bool
+SavedFrame::parentMoved()
+{
+    const Value &v = getReservedSlot(JSSLOT_PRIVATE_PARENT);
+    JSObject *p = static_cast<JSObject *>(v.toPrivate());
+    return p == getParent();
+}
+
+void
+SavedFrame::updatePrivateParent()
+{
+    setReservedSlot(JSSLOT_PRIVATE_PARENT, PrivateValue(getParent()));
+}
+
+bool
+SavedFrame::isSelfHosted()
+{
+    JSAtom *source = getSource();
+    return StringEqualsAscii(source, "self-hosted");
+}
+
+/* static */ bool
+SavedFrame::construct(JSContext *cx, unsigned argc, Value *vp)
+{
+    JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
+                         "SavedFrame");
+    return false;
+}
+
+/* static */ SavedFrame *
+SavedFrame::checkThis(JSContext *cx, CallArgs &args, const char *fnName)
+{
+    const Value &thisValue = args.thisv();
+
+    if (!thisValue.isObject()) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT);
+        return nullptr;
+    }
+
+    JSObject &thisObject = thisValue.toObject();
+    if (!thisObject.is<SavedFrame>()) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                             SavedFrame::class_.name, fnName, thisObject.getClass()->name);
+        return nullptr;
+    }
+
+    // Check for SavedFrame.prototype, which has the same class as SavedFrame
+    // instances, however doesn't actually represent a captured stack frame. It
+    // is the only object that is<SavedFrame>() but doesn't have a source.
+    if (thisObject.getReservedSlot(JSSLOT_SOURCE).isNull()) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                             SavedFrame::class_.name, fnName, "prototype object");
+        return nullptr;
+    }
+
+    return &thisObject.as<SavedFrame>();
+}
+
+// Get the SavedFrame * from the current this value and handle any errors that
+// might occur therein.
+//
+// These parameters must already exist when calling this macro:
+//   - JSContext  *cx
+//   - unsigned   argc
+//   - Value      *vp
+//   - const char *fnName
+// These parameters will be defined after calling this macro:
+//   - CallArgs args
+//   - Rooted<SavedFrame *> frame (will be non-null)
+#define THIS_SAVEDFRAME(cx, argc, vp, fnName, args, frame)         \
+    CallArgs args = CallArgsFromVp(argc, vp);                      \
+    Rooted<SavedFrame *> frame(cx, checkThis(cx, args, fnName));   \
+    if (!frame)                                                    \
+        return false
+
+/* static */ bool
+SavedFrame::sourceProperty(JSContext *cx, unsigned argc, Value *vp)
+{
+    THIS_SAVEDFRAME(cx, argc, vp, "(get source)", args, frame);
+    args.rval().setString(frame->getSource());
+    return true;
+}
+
+/* static */ bool
+SavedFrame::lineProperty(JSContext *cx, unsigned argc, Value *vp)
+{
+    THIS_SAVEDFRAME(cx, argc, vp, "(get line)", args, frame);
+    uint32_t line = frame->getLine();
+    args.rval().setNumber(line);
+    return true;
+}
+
+/* static */ bool
+SavedFrame::columnProperty(JSContext *cx, unsigned argc, Value *vp)
+{
+    THIS_SAVEDFRAME(cx, argc, vp, "(get column)", args, frame);
+    uint32_t column = frame->getColumn();
+    args.rval().setNumber(column);
+    return true;
+}
+
+/* static */ bool
+SavedFrame::functionDisplayNameProperty(JSContext *cx, unsigned argc, Value *vp)
+{
+    THIS_SAVEDFRAME(cx, argc, vp, "(get functionDisplayName)", args, frame);
+    RootedAtom name(cx, frame->getFunctionDisplayName());
+    if (name)
+        args.rval().setString(name);
+    else
+        args.rval().setNull();
+    return true;
+}
+
+/* static */ bool
+SavedFrame::parentProperty(JSContext *cx, unsigned argc, Value *vp)
+{
+    THIS_SAVEDFRAME(cx, argc, vp, "(get parent)", args, frame);
+    JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes;
+    JSPrincipals *principals = cx->compartment()->principals;
+
+    do
+        frame = frame->getParent();
+    while (frame && principals && subsumes &&
+           !subsumes(principals, frame->getPrincipals()));
+
+    args.rval().setObjectOrNull(frame);
+    return true;
+}
+
+/* static */ const JSPropertySpec SavedFrame::properties[] = {
+    JS_PSG("source", SavedFrame::sourceProperty, 0),
+    JS_PSG("line", SavedFrame::lineProperty, 0),
+    JS_PSG("column", SavedFrame::columnProperty, 0),
+    JS_PSG("functionDisplayName", SavedFrame::functionDisplayNameProperty, 0),
+    JS_PSG("parent", SavedFrame::parentProperty, 0),
+    JS_PS_END
+};
+
+/* static */ bool
+SavedFrame::toStringMethod(JSContext *cx, unsigned argc, Value *vp)
+{
+    THIS_SAVEDFRAME(cx, argc, vp, "toString", args, frame);
+    StringBuffer sb(cx);
+    JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes;
+    JSPrincipals *principals = cx->compartment()->principals;
+
+    do {
+        if (principals && subsumes && !subsumes(principals, frame->getPrincipals()))
+            continue;
+        if (frame->isSelfHosted())
+            continue;
+
+        RootedAtom name(cx, frame->getFunctionDisplayName());
+        if ((name && !sb.append(name))
+            || !sb.append('@')
+            || !sb.append(frame->getSource())
+            || !sb.append(':')
+            || !NumberValueToStringBuffer(cx, NumberValue(frame->getLine()), sb)
+            || !sb.append(':')
+            || !NumberValueToStringBuffer(cx, NumberValue(frame->getColumn()), sb)
+            || !sb.append('\n')) {
+            return false;
+        }
+    } while ((frame = frame->getParent()));
+
+    args.rval().setString(sb.finishString());
+    return true;
+}
+
+/* static */ const JSFunctionSpec SavedFrame::methods[] = {
+    JS_FN("toString", SavedFrame::toStringMethod, 0, 0),
+    JS_FS_END
+};
+
+bool
+SavedStacks::init()
+{
+    return frames.init();
+}
+
+bool
+SavedStacks::saveCurrentStack(JSContext *cx, MutableHandle<SavedFrame*> frame)
+{
+    JS_ASSERT(initialized());
+    JS_ASSERT(&cx->compartment()->savedStacks() == this);
+
+    ScriptFrameIter iter(cx);
+    return insertFrames(cx, iter, frame);
+}
+
+void
+SavedStacks::sweep(JSRuntime *rt)
+{
+    if (frames.initialized()) {
+        for (SavedFrame::Set::Enum e(frames); !e.empty(); e.popFront()) {
+            JSObject *obj = static_cast<JSObject *>(e.front());
+            JSObject *temp = obj;
+
+            if (IsObjectAboutToBeFinalized(&obj)) {
+                e.removeFront();
+            } else {
+                SavedFrame *frame = &obj->as<SavedFrame>();
+                bool parentMoved = frame->parentMoved();
+
+                if (parentMoved) {
+                    frame->updatePrivateParent();
+                }
+
+                if (obj != temp || parentMoved) {
+                    Rooted<SavedFrame*> parent(rt, frame->getParent());
+                    e.rekeyFront(SavedFrame::Lookup(frame->getSource(),
+                                                    frame->getLine(),
+                                                    frame->getColumn(),
+                                                    frame->getFunctionDisplayName(),
+                                                    parent,
+                                                    frame->getPrincipals()),
+                                 frame);
+                }
+            }
+        }
+    }
+
+    if (savedFrameProto && IsObjectAboutToBeFinalized(&savedFrameProto)) {
+        savedFrameProto = nullptr;
+    }
+}
+
+uint32_t
+SavedStacks::count()
+{
+    JS_ASSERT(initialized());
+    return frames.count();
+}
+
+void
+SavedStacks::clear()
+{
+    frames.clear();
+}
+
+size_t
+SavedStacks::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf)
+{
+    return frames.sizeOfExcludingThis(mallocSizeOf);
+}
+
+bool
+SavedStacks::insertFrames(JSContext *cx, ScriptFrameIter &iter, MutableHandle<SavedFrame*> frame)
+{
+    if (iter.done()) {
+        frame.set(nullptr);
+        return true;
+    }
+
+    ScriptFrameIter thisFrame(iter);
+    Rooted<SavedFrame*> parentFrame(cx);
+    if (!insertFrames(cx, ++iter, &parentFrame))
+        return false;
+
+    RootedScript script(cx, thisFrame.script());
+    RootedFunction callee(cx, thisFrame.maybeCallee());
+    const char *filename = script->filename();
+    RootedAtom source(cx, Atomize(cx, filename, strlen(filename)));
+    if (!source)
+        return false;
+    uint32_t column;
+    uint32_t line = PCToLineNumber(script, thisFrame.pc(), &column);
+
+    SavedFrame::Lookup lookup(source,
+                              line,
+                              column,
+                              callee ? callee->displayAtom() : nullptr,
+                              parentFrame,
+                              thisFrame.compartment()->principals);
+
+    frame.set(getOrCreateSavedFrame(cx, lookup));
+    return frame.address() != nullptr;
+}
+
+SavedFrame *
+SavedStacks::getOrCreateSavedFrame(JSContext *cx, SavedFrame::Lookup &lookup)
+{
+    SavedFrame::Set::AddPtr p = frames.lookupForAdd(lookup);
+    if (p)
+        return *p;
+
+    Rooted<SavedFrame *> frame(cx, createFrameFromLookup(cx, lookup));
+    if (!frame)
+        return nullptr;
+
+    if (!frames.relookupOrAdd(p, lookup, frame))
+        return nullptr;
+
+    return frame;
+}
+
+JSObject *
+SavedStacks::getOrCreateSavedFramePrototype(JSContext *cx)
+{
+    if (savedFrameProto)
+        return savedFrameProto;
+
+    Rooted<GlobalObject *> global(cx, cx->compartment()->maybeGlobal());
+    if (!global)
+        return nullptr;
+
+    savedFrameProto = js_InitClass(cx, global, global->getOrCreateObjectPrototype(cx),
+                                   &SavedFrame::class_, SavedFrame::construct, 0,
+                                   SavedFrame::properties, SavedFrame::methods, nullptr, nullptr);
+    // The only object with the SavedFrame::class_ that doesn't have a source
+    // should be the prototype.
+    savedFrameProto->setReservedSlot(SavedFrame::JSSLOT_SOURCE, NullValue());
+    return savedFrameProto;
+}
+
+SavedFrame *
+SavedStacks::createFrameFromLookup(JSContext *cx, SavedFrame::Lookup &lookup)
+{
+    RootedObject proto(cx, getOrCreateSavedFramePrototype(cx));
+    if (!proto)
+        return nullptr;
+
+    JS_ASSERT(proto->compartment() == cx->compartment());
+
+    RootedObject global(cx, cx->compartment()->maybeGlobal());
+    if (!global)
+        return nullptr;
+
+    JS_ASSERT(global->compartment() == cx->compartment());
+
+    RootedObject frameObj(cx, NewObjectWithGivenProto(cx, &SavedFrame::class_, proto, global));
+    if (!frameObj)
+        return nullptr;
+
+    SavedFrame &f = frameObj->as<SavedFrame>();
+    f.initFromLookup(lookup);
+
+    return &f;
+}
+
+} /* namespace js */
new file mode 100644
--- /dev/null
+++ b/js/src/vm/SavedStacks.h
@@ -0,0 +1,144 @@
+
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+#ifndef vm_SavedStacks_h
+#define vm_SavedStacks_h
+
+#include "jscntxt.h"
+#include "js/HashTable.h"
+#include "vm/Stack.h"
+
+namespace js {
+
+class SavedFrame : public JSObject {
+    friend class SavedStacks;
+
+  public:
+    static const Class          class_;
+    static void finalize(FreeOp *fop, JSObject *obj);
+
+    // Prototype methods and properties to be exposed to JS.
+    static const JSPropertySpec properties[];
+    static const JSFunctionSpec methods[];
+    static bool construct(JSContext *cx, unsigned argc, Value *vp);
+    static bool sourceProperty(JSContext *cx, unsigned argc, Value *vp);
+    static bool lineProperty(JSContext *cx, unsigned argc, Value *vp);
+    static bool columnProperty(JSContext *cx, unsigned argc, Value *vp);
+    static bool functionDisplayNameProperty(JSContext *cx, unsigned argc, Value *vp);
+    static bool parentProperty(JSContext *cx, unsigned argc, Value *vp);
+    static bool toStringMethod(JSContext *cx, unsigned argc, Value *vp);
+
+    // Convenient getters for SavedFrame's reserved slots for use from C++.
+    JSAtom       *getSource();
+    size_t       getLine();
+    size_t       getColumn();
+    JSAtom       *getFunctionDisplayName();
+    SavedFrame   *getParent();
+    JSPrincipals *getPrincipals();
+
+    bool         isSelfHosted();
+
+    struct Lookup;
+    struct HashPolicy;
+
+    typedef HashSet<SavedFrame *,
+                    HashPolicy,
+                    SystemAllocPolicy> Set;
+
+  private:
+    void initFromLookup(Lookup &lookup);
+
+    enum {
+        // The reserved slots in the SavedFrame class.
+        JSSLOT_SOURCE,
+        JSSLOT_LINE,
+        JSSLOT_COLUMN,
+        JSSLOT_FUNCTIONDISPLAYNAME,
+        JSSLOT_PARENT,
+        JSSLOT_PRINCIPALS,
+        JSSLOT_PRIVATE_PARENT,
+
+        // The total number of reserved slots in the SavedFrame class.
+        JSSLOT_COUNT
+    };
+
+    // Because we hash the parent pointer, we need to rekey a saved frame
+    // whenever its parent was relocated by the GC. However, the GC doesn't
+    // notify us when this occurs. As a work around, we keep a duplicate copy of
+    // the parent pointer as a private value in a reserved slot. Whenever the
+    // private value parent pointer doesn't match the regular parent pointer, we
+    // know that GC moved the parent and we need to update our private value and
+    // rekey the saved frame in its hash set. These two methods are helpers for
+    // this process.
+    bool         parentMoved();
+    void         updatePrivateParent();
+
+    static SavedFrame *checkThis(JSContext *cx, CallArgs &args, const char *fnName);
+};
+
+struct SavedFrame::Lookup {
+    Lookup(JSAtom *source, size_t line, size_t column, JSAtom *functionDisplayName,
+           Handle<SavedFrame*> parent, JSPrincipals *principals)
+        : source(source),
+          line(line),
+          column(column),
+          functionDisplayName(functionDisplayName),
+          parent(parent),
+          principals(principals)
+    {
+        JS_ASSERT(source);
+    }
+
+    JSAtom              *source;
+    size_t              line;
+    size_t              column;
+    JSAtom              *functionDisplayName;
+    Handle<SavedFrame*> parent;
+    JSPrincipals        *principals;
+};
+
+struct SavedFrame::HashPolicy
+{
+    typedef SavedFrame::Lookup               Lookup;
+    typedef PointerHasher<SavedFrame *, 3>   SavedFramePtrHasher;
+    typedef PointerHasher<JSPrincipals *, 3> JSPrincipalsPtrHasher;
+
+    static HashNumber hash(const Lookup &lookup);
+    static bool       match(SavedFrame *existing, const Lookup &lookup);
+
+    typedef SavedFrame* Key;
+    static void rekey(Key &key, const Key &newKey);
+};
+
+class SavedStacks {
+  public:
+    SavedStacks() : frames(), savedFrameProto(nullptr) { }
+
+    bool     init();
+    bool     initialized() const { return frames.initialized(); }
+    bool     saveCurrentStack(JSContext *cx, MutableHandle<SavedFrame*> frame);
+    void     sweep(JSRuntime *rt);
+    uint32_t count();
+    void     clear();
+
+    size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf);
+
+  private:
+    SavedFrame::Set          frames;
+    JSObject                 *savedFrameProto;
+
+    bool       insertFrames(JSContext *cx, ScriptFrameIter &iter, MutableHandle<SavedFrame*> frame);
+    SavedFrame *getOrCreateSavedFrame(JSContext *cx, SavedFrame::Lookup &lookup);
+    // |SavedFrame.prototype| is created lazily and held weakly. It should only
+    // be accessed through this method.
+    JSObject   *getOrCreateSavedFramePrototype(JSContext *cx);
+    SavedFrame *createFrameFromLookup(JSContext *cx, SavedFrame::Lookup &lookup);
+};
+
+} /* namespace js */
+
+#endif /* vm_SavedStacks_h */
--- a/js/src/vm/Stack.cpp
+++ b/js/src/vm/Stack.cpp
@@ -672,17 +672,17 @@ FrameIter::FrameIter(JSContext *cx, Cont
 {
     settleOnActivation();
 }
 
 FrameIter::FrameIter(const FrameIter &other)
   : data_(other.data_)
 #ifdef JS_ION
   , ionInlineFrames_(other.data_.cx_,
-                     data_.jitFrames_.isScripted() ? &other.ionInlineFrames_ : nullptr)
+                     data_.jitFrames_.isIonJS() ? &other.ionInlineFrames_ : nullptr)
 #endif
 {
 }
 
 FrameIter::FrameIter(const Data &data)
   : data_(data)
 #ifdef JS_ION
   , ionInlineFrames_(data.cx_, data_.jitFrames_.isIonJS() ? &data_.jitFrames_ : nullptr)