Add breakpoints.
authorJason Orendorff <jorendorff@mozilla.com>
Tue, 28 Jun 2011 16:06:34 -0500
changeset 74488 45f1cf2c59d200bc5e5db35001eed68d9a466a13
parent 74487 2cc9d8a133bc33a0202ec8dcdcd1b85b3df5eb9d
child 74489 63ee1fe5025c99e88e20847e1e533d2af9117cb8
push id20986
push userkhuey@mozilla.com
push dateSun, 14 Aug 2011 11:45:15 +0000
treeherdermozilla-central@2de3cff973b2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone7.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
Add breakpoints. This adds a new per-compartment implementation of breakpoints and reimplements the jsdbgapi.h "trap" entry points on top of it. Most jsdbgapi.h-using code will still work, but there is no longer a single runtime-wide trapList protected by a lock. Embeddings must follow the compartment rules for thread safety. JS_ClearAllTraps was removed, replaced by the per-compartment API JS_ClearAllTrapsForCompartment. The new implementation asserts that the PC passed to JS_SetTrap is actually an offset of an instruction, not just a random number. This caused quite a few tests to fail; fixes are included. Added Debug.Script.prototype.setBreakpoint, getBreakpoints, clearBreakpoint, and clearAllBreakpoints; and Debug.prototype.clearAllBreakpoints. In addition to tests targeting the new functionality, this changeset includes some tests for Debug.Script.prototype.getLineOffsets, which is hard to test without breakpoints.
js/src/jit-test/tests/debug/Debug-clearAllBreakpoints-01.js
js/src/jit-test/tests/debug/Frame-eval-10.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js
js/src/jit-test/tests/debug/Frame-live-04.js
js/src/jit-test/tests/debug/Frame-offset-02.js
js/src/jit-test/tests/debug/Object-02.js
js/src/jit-test/tests/debug/Script-clearBreakpoint-01.js
js/src/jit-test/tests/debug/Script-clearBreakpoint-02.js
js/src/jit-test/tests/debug/Script-clearBreakpoint-03.js
js/src/jit-test/tests/debug/Script-clearBreakpoint-04.js
js/src/jit-test/tests/debug/Script-gc-01.js
js/src/jit-test/tests/debug/Script-getBreakpoints-01.js
js/src/jit-test/tests/debug/Script-getBreakpoints-02.js
js/src/jit-test/tests/debug/Script-getLineOffsets-01.js
js/src/jit-test/tests/debug/Script-getLineOffsets-02.js
js/src/jit-test/tests/debug/Script-getLineOffsets-03.js
js/src/jit-test/tests/debug/Script-getLineOffsets-04.js
js/src/jit-test/tests/debug/Script-getLineOffsets-05.js
js/src/jit-test/tests/debug/Script-getLineOffsets-06.js
js/src/jit-test/tests/debug/Script-getOffsetLine-01.js
js/src/jit-test/tests/debug/Script-getOffsetLine-02.js
js/src/jit-test/tests/debug/breakpoint-01.js
js/src/jit-test/tests/debug/breakpoint-02.js
js/src/jit-test/tests/debug/breakpoint-03.js
js/src/jit-test/tests/debug/breakpoint-04.js
js/src/jit-test/tests/debug/breakpoint-05.js
js/src/jit-test/tests/debug/breakpoint-06.js
js/src/jit-test/tests/debug/breakpoint-07.js
js/src/jit-test/tests/debug/breakpoint-08.js
js/src/jit-test/tests/debug/breakpoint-gc-01.js
js/src/jit-test/tests/debug/breakpoint-gc-02.js
js/src/jit-test/tests/debug/breakpoint-multi-01.js
js/src/jit-test/tests/debug/breakpoint-multi-02.js
js/src/jit-test/tests/debug/breakpoint-multi-03.js
js/src/jit-test/tests/debug/breakpoint-multi-04.js
js/src/jit-test/tests/debug/breakpoint-resume-01.js
js/src/jit-test/tests/debug/breakpoint-resume-02.js
js/src/jit-test/tests/debug/breakpoint-resume-03.js
js/src/jit-test/tests/debug/surfaces-offsets.js
js/src/jit-test/tests/jaeger/bug563000/simple-trap-1.js
js/src/jit-test/tests/jaeger/bug563000/simple-trap-2.js
js/src/jit-test/tests/jaeger/bug563000/simple-untrap.js
js/src/jit-test/tests/jaeger/bug563000/trap-from-add-inline.js
js/src/jit-test/tests/jaeger/bug563000/trap-from-add-ool.js
js/src/jit-test/tests/jaeger/bug563000/trap-self.js
js/src/jit-test/tests/jaeger/bug563000/untrap-self.js
js/src/js.msg
js/src/jsapi-tests/testTrap.cpp
js/src/jsapi.cpp
js/src/jscntxt.cpp
js/src/jscntxt.h
js/src/jscompartment.cpp
js/src/jscompartment.h
js/src/jsdbg.cpp
js/src/jsdbg.h
js/src/jsdbgapi.cpp
js/src/jsdbgapi.h
js/src/jsgc.cpp
js/src/jsgc.h
js/src/jsinterp.cpp
js/src/jsprvtd.h
js/src/methodjit/StubCalls.cpp
js/src/shell/js.cpp
js/src/tests/ecma_3/extensions/regress-429248.js
js/src/tests/js1_5/extensions/regress-422137.js
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debug-clearAllBreakpoints-01.js
@@ -0,0 +1,31 @@
+// clearAllBreakpoints clears breakpoints for the current Debug object only.
+
+var g = newGlobal('new-compartment');
+
+var hits = 0;
+function attach(i) {
+    var dbg = Debug(g);
+    var handler = {
+        hit: function (frame) {
+            hits++;
+            dbg.clearAllBreakpoints(handler);
+        }
+    };
+
+    dbg.hooks = {
+        debuggerHandler: function (frame) {
+            var s = frame.script;
+            var offs = s.getLineOffsets(g.line0 + 3);
+            for (var i = 0; i < offs.length; i++)
+                s.setBreakpoint(offs[i], handler);
+        }
+    };
+}
+for (var i = 0; i < 4; i++)
+    attach(i);
+
+g.eval("var line0 = Error().lineNumber;\n" +
+       "debugger;\n" +                      // line0 + 1
+       "for (var i = 0; i < 7; i++)\n" +    // line0 + 2
+       "    Math.sin(0);\n");               // line0 + 3
+assertEq(hits, 4);
--- a/js/src/jit-test/tests/debug/Frame-eval-10.js
+++ b/js/src/jit-test/tests/debug/Frame-eval-10.js
@@ -1,14 +1,14 @@
 // frame.eval returns null if the eval code fails with an uncatchable error.
 var g = newGlobal('new-compartment');
 var dbg = Debug(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	if (hits++ === 0)
-	    assertEq(frame.eval("debugger;"), null);
-	else
-	    return null;
+        if (hits++ === 0)
+            assertEq(frame.eval("debugger;"), null);
+        else
+            return null;
     }
 };
 assertEq(g.eval("debugger; 'ok';"), "ok");
 assertEq(hits, 2);
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js
@@ -1,18 +1,18 @@
 // evalWithBindings basics
 
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	assertEq(frame.evalWithBindings("x", {x: 2}).return, 2);
-	assertEq(frame.evalWithBindings("x + y", {x: 2}).return, 5);
-	hits++;
+        assertEq(frame.evalWithBindings("x", {x: 2}).return, 2);
+        assertEq(frame.evalWithBindings("x + y", {x: 2}).return, 5);
+        hits++;
     }
 };
 
 // in global code
 g.y = 3;
 g.eval("debugger;");
 
 // in function code
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js
@@ -1,19 +1,19 @@
 // evalWithBindings to call a method of a debuggee object
 var g = newGlobal('new-compartment');
 var dbg = new Debug;
 var global = dbg.addDebuggee(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	var obj = frame.arguments[0];
-	var expected = frame.arguments[1];
-	assertEq(frame.evalWithBindings("obj.toString()", {obj: obj}).return, expected);
-	hits++;
+        var obj = frame.arguments[0];
+        var expected = frame.arguments[1];
+        assertEq(frame.evalWithBindings("obj.toString()", {obj: obj}).return, expected);
+        hits++;
     }
 };
 
 g.eval("function f(obj, expected) { debugger; }");
 
 g.eval("f(new Number(-0), '0');");
 g.eval("f(new String('ok'), 'ok');");
 g.eval("f({toString: function () { return f; }}, f);");
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js
@@ -1,18 +1,18 @@
 // arguments works in evalWithBindings (it does not interpose a function scope)
 var g = newGlobal('new-compartment');
 var dbg = new Debug;
 var global = dbg.addDebuggee(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	var argc = frame.arguments.length;
-	assertEq(argc, 7);
-	assertEq(frame.evalWithBindings("arguments[prop]", {prop: "length"}).return, argc);
-	for (var i = 0; i < argc; i++)
-	    assertEq(frame.evalWithBindings("arguments[i]", {i: i}).return, frame.arguments[i]);
-	hits++;
+        var argc = frame.arguments.length;
+        assertEq(argc, 7);
+        assertEq(frame.evalWithBindings("arguments[prop]", {prop: "length"}).return, argc);
+        for (var i = 0; i < argc; i++)
+            assertEq(frame.evalWithBindings("arguments[i]", {i: i}).return, frame.arguments[i]);
+        hits++;
     }
 };
 g.eval("function f() { debugger; }");
 g.eval("f(undefined, -0, NaN, '\uffff', Array.prototype, Math, f);");
 assertEq(hits, 1);
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js
@@ -1,19 +1,19 @@
 // evalWithBindings works on non-top frames.
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 var f1;
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	assertEq(frame.older.evalWithBindings("q + r", {r: 3}).return, 5);
+        assertEq(frame.older.evalWithBindings("q + r", {r: 3}).return, 5);
 
-	// frame.older.older is in the same function as frame, but a different activation of it
-	assertEq(frame.older.older.evalWithBindings("q + r", {r: 3}).return, 6);
-	hits++;
+        // frame.older.older is in the same function as frame, but a different activation of it
+        assertEq(frame.older.older.evalWithBindings("q + r", {r: 3}).return, 6);
+        hits++;
     }
 };
 
 g.eval("function f1(q) { if (q == 1) debugger; else f2(2); }");
 g.eval("function f2(arg) { var q = arg; f1(1); }");
 g.f1(3);
 assertEq(hits, 1);
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js
@@ -1,14 +1,14 @@
 // evalWithBindings code can assign to the bindings.
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	assertEq(frame.evalWithBindings("for (i = 0; i < 5; i++) {}  i;", {i: 10}).return, 5);
-	hits++;
+        assertEq(frame.evalWithBindings("for (i = 0; i < 5; i++) {}  i;", {i: 10}).return, 5);
+        hits++;
     }
 };
 
 g.eval("debugger;");
 assertEq("i" in g, false);
 assertEq(hits, 1);
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js
@@ -1,11 +1,11 @@
 // In evalWithBindings code, assignment to any name not in the bindings works just as in eval.
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	assertEq(frame.evalWithBindings("y = z; x = w;", {z: 2, w: 3}).return, 3);
+        assertEq(frame.evalWithBindings("y = z; x = w;", {z: 2, w: 3}).return, 3);
     }
 };
 g.eval("function f(x) { debugger; assertEq(x, 3); }");
 g.eval("var y = 0; f(0);");
 assertEq(g.y, 2);
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js
@@ -1,16 +1,16 @@
 // var statements in strict evalWithBindings code behave like strict eval.
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	assertEq(frame.evalWithBindings("var i = a*a + b*b; i === 25;", {a: 3, b: 4}).return, true);
-	hits++;
+        assertEq(frame.evalWithBindings("var i = a*a + b*b; i === 25;", {a: 3, b: 4}).return, true);
+        hits++;
     }
 };
 g.eval("'use strict'; debugger;");
 assertEq(hits, 1);
 assertEq("i" in g, false);
 
 g.eval("function die() { throw fit; }");
 g.eval("Object.defineProperty(this, 'i', {get: die, set: die});");
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js
@@ -1,15 +1,15 @@
 // evalWithBindings ignores non-enumerable and non-own properties.
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	assertEq(frame.evalWithBindings("toString + constructor + length", []).return, 112233);
-	var obj = Object.create({constructor: "FAIL"}, {length: {value: "fail"}});
-	assertEq(frame.evalWithBindings("toString + constructor + length", obj).return, 112233);
-	hits++;
+        assertEq(frame.evalWithBindings("toString + constructor + length", []).return, 112233);
+        var obj = Object.create({constructor: "FAIL"}, {length: {value: "fail"}});
+        assertEq(frame.evalWithBindings("toString + constructor + length", obj).return, 112233);
+        hits++;
     }
 };
 g.eval("function f() { var toString = 111111, constructor = 1111, length = 11; debugger; }");
 g.f();
 assertEq(hits, 1);
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js
@@ -1,29 +1,29 @@
 // evalWithBindings code is debuggee code, so it can trip the debugger. It nests!
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 var f1;
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	f1 = frame;
+        f1 = frame;
 
-	// This trips the throw hook.
-	var x = frame.evalWithBindings("wrongSpeling", {rightSpelling: 2}).throw;
+        // This trips the throw hook.
+        var x = frame.evalWithBindings("wrongSpeling", {rightSpelling: 2}).throw;
 
-	assertEq(frame.evalWithBindings("exc.name", {exc: x}).return, "ReferenceError");
-	hits++;
+        assertEq(frame.evalWithBindings("exc.name", {exc: x}).return, "ReferenceError");
+        hits++;
     },
     throw: function (frame, exc) {
-	assertEq(frame !== f1, true);
+        assertEq(frame !== f1, true);
 
-	// f1's environment does not contain the binding for the first evalWithBindings call.
-	assertEq(f1.eval("rightSpelling").return, "dependent");
-	assertEq(f1.evalWithBindings("n + rightSpelling", {n: "in"}).return, "independent");
+        // f1's environment does not contain the binding for the first evalWithBindings call.
+        assertEq(f1.eval("rightSpelling").return, "dependent");
+        assertEq(f1.evalWithBindings("n + rightSpelling", {n: "in"}).return, "independent");
 
-	// frame's environment does contain the binding.
-	assertEq(frame.eval("rightSpelling").return, 2);
-	assertEq(frame.evalWithBindings("rightSpelling + three", {three: 3}).return, 5);
-	hits++;
+        // frame's environment does contain the binding.
+        assertEq(frame.eval("rightSpelling").return, 2);
+        assertEq(frame.evalWithBindings("rightSpelling + three", {three: 3}).return, 5);
+        hits++;
     }
 };
 g.eval("(function () { var rightSpelling = 'dependent'; debugger; })();");
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js
@@ -1,17 +1,17 @@
 // Direct eval code under evalWithbindings sees both the bindings and the enclosing scope.
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	var code =
-	    "assertEq(a, 1234);\n" +
-	    "assertEq(b, null);\n" +
-	    "assertEq(c, 'ok');\n";
-	assertEq(frame.evalWithBindings("eval(s)", {s: code, a: 1234}).return, undefined);
-	hits++;
+        var code =
+            "assertEq(a, 1234);\n" +
+            "assertEq(b, null);\n" +
+            "assertEq(c, 'ok');\n";
+        assertEq(frame.evalWithBindings("eval(s)", {s: code, a: 1234}).return, undefined);
+        hits++;
     }
 };
 g.eval("function f(b) { var c = 'ok'; debugger; }");
 g.f(null);
 assertEq(hits, 1);
--- a/js/src/jit-test/tests/debug/Frame-live-04.js
+++ b/js/src/jit-test/tests/debug/Frame-live-04.js
@@ -1,25 +1,25 @@
 // Frame.live is false for frames discarded during uncatchable error unwinding.
 var g = newGlobal('new-compartment');
 var dbg = Debug(g);
 var hits = 0;
 var snapshot;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	var stack = [];
-	for (var f = frame; f; f = f.older) {
-	    if (f.type === "call")
-		stack.push(f);
-	}
-	snapshot = stack;
-	if (hits++ === 0)
-	    assertEq(frame.eval("x();"), null);
-	else
-	    return null;
+        var stack = [];
+        for (var f = frame; f; f = f.older) {
+            if (f.type === "call")
+                stack.push(f);
+        }
+        snapshot = stack;
+        if (hits++ === 0)
+            assertEq(frame.eval("x();"), null);
+        else
+            return null;
     }
 };
 
 g.eval("function z() { debugger; }");
 g.eval("function y() { z(); }");
 g.eval("function x() { y(); }");
 assertEq(g.eval("debugger; 'ok';"), "ok");
 assertEq(hits, 2);
--- a/js/src/jit-test/tests/debug/Frame-offset-02.js
+++ b/js/src/jit-test/tests/debug/Frame-offset-02.js
@@ -1,18 +1,18 @@
 // frame.offset gives different values at different points in a script.
 
 var g = newGlobal('new-compartment');
 var dbg = Debug(g);
 var s = undefined, a = []
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	if (s === undefined)
-	    s = frame.script;
-	else
-	    assertEq(s, frame.script);
-	assertEq(frame.offset !== undefined, true);
-	assertEq(a.indexOf(frame.offset), -1);
-	a.push(frame.offset);
+        if (s === undefined)
+            s = frame.script;
+        else
+            assertEq(s, frame.script);
+        assertEq(frame.offset !== undefined, true);
+        assertEq(a.indexOf(frame.offset), -1);
+        a.push(frame.offset);
     }
 };
 g.eval("debugger; debugger; debugger;");
 assertEq(a.length, 3);
--- a/js/src/jit-test/tests/debug/Object-02.js
+++ b/js/src/jit-test/tests/debug/Object-02.js
@@ -1,15 +1,15 @@
 // Debug.Object referents can be transparent wrappers of objects in the debugger compartment.
 
 var g = newGlobal('new-compartment');
 g.f = function (a, b) { return a + "/" + b; };
 var dbg = Debug(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	var f = frame.eval("f").return;
-	assertEq(f.call(null, "a", "b").return, "a/b");
-	hits++;
+        var f = frame.eval("f").return;
+        assertEq(f.call(null, "a", "b").return, "a/b");
+        hits++;
     }
 };
 g.eval("debugger;");
 assertEq(hits, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-clearBreakpoint-01.js
@@ -0,0 +1,21 @@
+// A breakpoint handler may clear itself.
+
+var g = newGlobal('new-compartment');
+var bphits = 0;
+var handler = {hit: function (frame) { frame.script.clearBreakpoint(this); bphits++; }};
+var dbg = Debug(g);
+var hits = 0;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var offs = frame.script.getLineOffsets(g.line0 + 3);
+        for (var i = 0; i < offs.length; i++)
+            frame.script.setBreakpoint(offs[i], handler);
+        hits++;
+    }
+};
+g.eval("var line0 = Error().lineNumber;\n" +
+       "debugger;\n" +                    // line0 + 1
+       "for (var i = 0; i < 4; i++)\n" +  // line0 + 2
+       "    result = 'ok';\n");           // line0 + 3
+assertEq(hits, 1);
+assertEq(bphits, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-clearBreakpoint-02.js
@@ -0,0 +1,28 @@
+// A breakpoint cleared during dispatch does not fire.
+// (Breakpoint dispatch is well-behaved even when breakpoint handlers clear other breakpoints.)
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+var log = '';
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var s = frame.script;
+        function handler(i) {
+            if (i === 1)
+                return function () { log += i; s.clearBreakpoint(h[1]); s.clearBreakpoint(h[2]); };
+            return function () { log += i; };
+        }
+        var offs = s.getLineOffsets(g.line0 + 2);
+        var h = [];
+        for (var i = 0; i < 4; i++) {
+            h[i] = {hit: handler(i)};
+            for (var j = 0; j < offs.length; j++)
+                s.setBreakpoint(offs[j], h[i]);
+        }
+    }
+};
+
+g.eval("var line0 = Error().lineNumber;\n" +
+       "debugger;\n" +          // line0 + 1
+       "result = 'ok';\n");     // line0 + 2
+assertEq(log, '013');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-clearBreakpoint-03.js
@@ -0,0 +1,27 @@
+// Clearing a breakpoint by handler can clear multiple breakpoints.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+var s;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        s = frame.eval("f").return.script;
+    }
+};
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f(a, b) {\n" +     // line0 + 1
+       "    return a + b;\n" +      // line0 + 2
+       "}\n" +
+       "debugger;\n");
+
+var hits = 0;
+var handler = {hit: function (frame) { hits++; s.clearBreakpoint(handler); }};
+var offs = s.getLineOffsets(g.line0 + 2);
+for (var i = 0; i < 4; i++) {
+    for (var j = 0; j < offs.length; j++)
+        s.setBreakpoint(offs[j], handler);
+}
+
+assertEq(g.f(2, 2), 4);
+assertEq(hits, 1);
+assertEq(s.getBreakpoints().length, 0);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-clearBreakpoint-04.js
@@ -0,0 +1,30 @@
+// clearBreakpoint clears breakpoints for the current Debug object only.
+
+var g = newGlobal('new-compartment');
+
+var hits = 0;
+var handler = {
+    hit: function (frame) {
+        hits++;
+        frame.script.clearBreakpoint(handler);
+    }
+};
+
+function attach(i) {
+    var dbg = Debug(g);
+    dbg.hooks = {
+        debuggerHandler: function (frame) {
+            var s = frame.script;
+            var offs = s.getLineOffsets(g.line0 + 2);
+            for (var i = 0; i < offs.length; i++)
+                s.setBreakpoint(offs[i], handler);
+        }
+    };
+}
+for (var i = 0; i < 4; i++)
+    attach(i);
+
+g.eval("var line0 = Error().lineNumber;\n" +
+       "debugger;\n" +      // line0 + 1
+       "Math.sin(0);\n");   // line0 + 2
+assertEq(hits, 4);
--- a/js/src/jit-test/tests/debug/Script-gc-01.js
+++ b/js/src/jit-test/tests/debug/Script-gc-01.js
@@ -2,17 +2,17 @@
 // Debug.Script instances with live referents stay alive.
 
 var N = 4;
 var g = newGlobal('new-compartment');
 var dbg = new Debug(g);
 var i;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	assertEq(frame.script instanceof Debug.Script, true);
+        assertEq(frame.script instanceof Debug.Script, true);
         frame.script.id = i;
     }
 };
 
 g.eval('var arr = [];')
 for (i = 0; i < N; i++)  // loop to defeat conservative GC
     g.eval("arr.push(function () { debugger }); arr[arr.length - 1]();");
 
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getBreakpoints-01.js
@@ -0,0 +1,42 @@
+// Basic Script.prototype.getBreakpoints tests.
+
+var g = newGlobal('new-compartment');
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f(x) {\n" +        // line0 + 1
+       "    if (x < 0)\n" +         // line0 + 2
+       "        return -x;\n" +     // line0 + 3
+       "    return x;\n" +
+       "}");
+
+var s;
+var offsets = [];
+var handlers = [];
+var dbg = Debug(g);
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        s = frame.eval("f").return.script;
+        var off;
+
+        for (var i = 0; i < 3; i++) {
+            var off = s.getLineOffsets(g.line0 + 2 + i)[0];
+            assertEq(typeof off, 'number');
+            handlers[i] = {};
+            s.setBreakpoint(off, handlers[i]);
+            offsets[i] = off;
+        }
+    }
+};
+g.eval("debugger;");
+
+// getBreakpoints without an offset gets all breakpoints in the script.
+var bps = s.getBreakpoints();
+assertEq(bps.length, handlers.length);
+for (var i = 0; i < bps.length; i++)
+    assertEq(bps.indexOf(handlers[i]) !== -1, true);
+
+// getBreakpoints with an offset finds only breakpoints at that offset.
+for (var i = 0; i < offsets.length; i++) {
+    var bps = s.getBreakpoints(offsets[i]);
+    assertEq(bps.length, 1);
+    assertEq(bps[0], handlers[i]);
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getBreakpoints-02.js
@@ -0,0 +1,44 @@
+// Script.prototype.getBreakpoints returns breakpoints for the current Debug object only.
+
+var g = newGlobal('new-compartment');
+
+var debuggers = [];
+var hits = 0;
+function attach(g, i) {
+    var dbg = Debug(g);
+    dbg.hooks = {
+        debuggerHandler: function (frame) {
+            var s = frame.script;
+            var offs = s.getLineOffsets(g.line0 + 2);
+            var hitAny = false;
+            var handler = {
+                hit: function (frame) {
+                    assertEq(this, handler);
+                    assertEq(frame.script, s);
+                    var bps = s.getBreakpoints();
+                    assertEq(bps.length, offs.length);
+                    for (var i = 0; i < bps.length; i++)
+                        assertEq(bps[i], handler);
+
+                    if (!hitAny) {
+                        hitAny = true;
+                        hits++;
+                    }
+                }
+            };
+            for (var i = 0; i < offs.length; i++)
+                s.setBreakpoint(offs[i], handler);
+            hits++;
+        }
+    };
+    debuggers[i] = dbg;
+}
+
+for (var i = 0; i < 3; i++)
+    attach(g, i);
+g.done = false;
+g.eval("var line0 = Error().lineNumber;\n" +
+       "debugger;\n" +      // line0 + 1
+       "done = true;\n");   // line0 + 2
+assertEq(hits, 6);
+assertEq(g.done, true);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-01.js
@@ -0,0 +1,15 @@
+// getLineOffsets on a line that is definitely outside a script returns an empty array.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+var hits = 0;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var offs = frame.script.getLineOffsets(g.line0 + 2);
+        assertEq(Array.isArray(offs), true);
+        assertEq(offs.length, 0);
+        hits++;
+    }
+};
+g.eval("var line0 = Error().lineNumber; debugger;");
+assertEq(hits, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-02.js
@@ -0,0 +1,35 @@
+// getLineOffsets correctly places the various parts of a ForStatement.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        function handler(line) {
+            return {hit: function (frame) { g.log += "" + line; }};
+        }
+
+        var s = frame.eval("f").return.script;
+        for (var line = 2; line <= 6; line++) {
+            var offs = s.getLineOffsets(g.line0 + line);
+            var h = handler(line);
+            for (var i = 0; i < offs.length; i++) {
+                assertEq(s.getOffsetLine(offs[i]), g.line0 + line);
+                s.setBreakpoint(offs[i], h);
+            }
+        }
+    }
+};
+
+g.log = '';
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f(n) {\n" +        // line0 + 1
+       "    for (var i = 0;\n" +    // line0 + 2
+       "         i < n;\n" +        // line0 + 3
+       "         i++)\n" +          // line0 + 4
+       "        log += '.';\n" +    // line0 + 5
+       "    log += '!';\n" +        // line0 + 6
+       "}\n" +
+       "debugger;\n");
+assertEq(g.log, "");
+g.f(3);
+assertEq(g.log, "235.435.435.436!");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-03.js
@@ -0,0 +1,38 @@
+// getLineOffsets treats one-line compound statements as having only one entry-point.
+// (A breakpoint on a line that only executes once will only hit once.)
+
+var g = newGlobal('new-compartment');
+g.line0 = null;
+var dbg = Debug(g);
+var log;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var s = frame.script;
+        var lineno = g.line0 + 2;
+        var offs = s.getLineOffsets(lineno);
+        for (var i = 0; i < offs.length; i++) {
+            assertEq(s.getOffsetLine(offs[i]), lineno);
+            s.setBreakpoint(offs[i], {hit: function () { log += 'B'; }});
+        }
+        log += 'A';
+    }
+};
+
+function test(s) {
+    log = '';
+    g.eval("line0 = Error().lineNumber;\n" +
+           "debugger;\n" +  // line0 + 1
+           s);              // line0 + 2
+    assertEq(log, 'AB');
+}
+
+g.i = 0;
+g.j = 0;
+test("{i++; i--; i++; i--; }");
+test("if (i === 0) i++; else i--;");
+test("while (i < 5) i++;");
+test("do --i; while (i > 0);");
+test("for (i = 0; i < 5; i++) j++;");
+test("for (var x in [0, 1, 2]) j++;");
+test("switch (i) { case 0: j = 0; case 1: j = 1; case 2: j = 2; default: j = i; }");
+test("switch (i) { case 'A': j = 0; case 'B': j = 1; case 'C': j = 2; default: j = i; }");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-04.js
@@ -0,0 +1,55 @@
+// getLineOffsets works with instructions reachable only by breaking out of a loop or switch.
+
+var g = newGlobal('new-compartment');
+g.line0 = null;
+var dbg = Debug(g);
+var where;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var s = frame.eval("f").return.script;
+        var lineno = g.line0 + where;
+        var offs = s.getLineOffsets(lineno);
+        for (var i = 0; i < offs.length; i++) {
+            assertEq(s.getOffsetLine(offs[i]), lineno);
+            s.setBreakpoint(offs[i], {hit: function () { g.log += 'B'; }});
+        }
+        g.log += 'A';
+    }
+};
+
+function test(s) {
+    var count = (s.split(/\n/).length - 1); // number of newlines in s
+    g.log = '';
+    where = 3 + count + 1;
+    g.eval("line0 = Error().lineNumber;\n" +
+           "debugger;\n" +          // line0 + 1
+           "function f(i) {\n" +    // line0 + 2
+           s +                      // line0 + 3 ... line0 + where - 2
+           "    log += '?';\n" +    // line0 + where - 1
+           "    log += '!';\n" +    // line0 + where
+           "}\n");
+    g.f(0);
+    assertEq(g.log, 'A?B!');
+}
+
+test("i = 128;\n" +
+     "for (;;) {\n" +
+     "    var x = i - 10;;\n" +
+     "    if (x < 0)\n" +
+     "        break;\n" +
+     "    i >>= 2;\n" +
+     "}\n");
+
+test("while (true)\n" +
+     "    if (++i === 2) break;\n");
+
+test("do {\n" +
+     "    if (++i === 2) break;\n" +
+     "} while (true);\n");
+
+test("switch (i) {\n" +
+     "  case 2: return 7;\n" +
+     "  case 1: return 8;\n" +
+     "  case 0: break;\n" +
+     "  default: return -i;\n" +
+     "}\n");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-05.js
@@ -0,0 +1,67 @@
+// getLineOffsets identifies multiple ways to land on a line.
+
+var g = newGlobal('new-compartment');
+g.line0 = null;
+var dbg = Debug(g);
+var where;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var s = frame.script, lineno, offs;
+
+        lineno = g.line0 + where;
+        offs = s.getLineOffsets(lineno);
+        for (var i = 0; i < offs.length; i++) {
+            assertEq(s.getOffsetLine(offs[i]), lineno);
+            s.setBreakpoint(offs[i], {hit: function () { g.log += 'B'; }});
+        }
+
+        lineno++;
+        offs = s.getLineOffsets(lineno);
+        for (var i = 0; i < offs.length; i++) {
+            assertEq(s.getOffsetLine(offs[i]), lineno);
+            s.setBreakpoint(offs[i], {hit: function () { g.log += 'C'; }});
+        }
+
+        g.log += 'A';
+    }
+};
+
+function test(s) {
+    assertEq(s.charAt(s.length - 1), '\n');
+    var count = (s.split(/\n/).length - 1); // number of lines in s
+    g.log = '';
+    where = 1 + count;
+    g.eval("line0 = Error().lineNumber;\n" +
+           "debugger;\n" +          // line0 + 1
+           s +                      // line0 + 2 ... line0 + where
+           "log += 'D';\n");
+    assertEq(g.log, 'AB!CD');
+}
+
+// if-statement with yes and no paths on a single line
+g.i = 0;
+test("if (i === 0)\n" +
+     "    log += '!'; else log += 'X';\n");
+test("if (i === 2)\n" +
+     "    log += 'X'; else log += '!';\n");
+
+// break to a line that has code inside and outside the loop
+g.i = 2;
+test("while (1) {\n" +
+     "    if (i === 2) break;\n" +
+     "    log += 'X'; } log += '!';\n");
+
+// leaving a while loop by failing the test, when the last line has stuff both inside and outside the loop
+g.i = 0;
+test("while (i > 0) {\n" +
+     "    if (i === 70) log += 'X';\n" +
+     "    --i;  } log += '!';\n");
+
+// multiple case-labels on the same line
+g.i = 0;
+test("switch (i) {\n" +
+     "    case 0: case 1: log += '!'; break; }\n");
+test("switch ('' + i) {\n" +
+     "    case '0': case '1': log += '!'; break; }\n");
+test("switch (i) {\n" +
+     "    case 'ok' + i: case i - i: log += '!'; break; }\n");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-06.js
@@ -0,0 +1,101 @@
+// getLineOffsets works with extended instructions, such as JSOP_GOTOX.
+
+var g = newGlobal('new-compartment');
+g.line0 = null;
+var dbg = Debug(g);
+var where;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var s = frame.script;
+        var offs;
+        var lineno = g.line0 + where;
+        offs = s.getLineOffsets(lineno);
+        for (var i = 0; i < offs.length; i++) {
+            assertEq(s.getOffsetLine(offs[i]), lineno);
+            s.setBreakpoint(offs[i], {hit: function (frame) { g.log += 'B'; }});
+        }
+        g.log += 'A';
+    }
+};
+
+function test(s) {
+    assertEq(s.charAt(s.length - 1) !== '\n', true);
+    var count = s.split(/\n/).length;  // number of lines in s
+    g.i = 0;
+    g.log = '';
+    where = 1 + count;
+    g.eval("line0 = Error().lineNumber;\n" +
+           "debugger;\n" +          // line0 + 1
+           s +                      // line0 + 2 ... line0 + where
+           " log += 'C';\n");
+    assertEq(g.log, 'ABC');
+}
+
+function repeat(s) {
+    return Array((1 << 14) + 1).join(s);  // 16K copies of s
+}
+var long_expr = "i" + repeat(" + i");
+var long_throw_stmt = "throw " + long_expr + ";\n";
+
+// long break (JSOP_GOTOX)
+test("for (;;) {\n" +
+     "    if (i === 0)\n" +
+     "        break;\n" +
+     "    " + long_throw_stmt +
+     "}");
+
+// long continue (JSOP_GOTOX)
+test("do {\n" +
+     "    if (i === 0)\n" +
+     "        continue;\n" +
+     "    " + long_throw_stmt +
+     "} while (i !== 0);");
+
+// long if consequent (JSOP_IFEQX)
+test("if (i === 2) {\n" +
+     "    " + long_throw_stmt +
+     "}");
+
+// long catch-block with finally (JSOP_GOSUBX)
+test("try {\n" +
+     "    i = 0;\n" +
+     "} catch (exc) {\n" +
+     "    throw " + long_expr + ";\n" +
+     "} finally {\n" +
+     "    i = 1;\n" +
+     "}");
+
+// long case (JSOP_TABLESWITCHX)
+test("switch (i) {\n" +
+     "  default:\n" +
+     "  case 1: " + long_throw_stmt +
+     "  case 0: i++; }");
+
+test("switch (i) {\n" +
+     "  case 1: case 2: case 3: " + long_throw_stmt +
+     "  default: i++; }");
+
+// long case (JSOP_LOOKUPSWITCHX)
+test("switch ('' + i) {\n" +
+     "  default:\n" +
+     "  case '1': " + long_throw_stmt +
+     "  case '0': i++; }");
+
+test("switch (i) {\n" +
+     "  case '1': case '2': case '3': " + long_throw_stmt +
+     "  default: i++; }");
+
+// long case or case-expression (JSOP_CASEX)
+test("switch (i) {\n" +
+     "  case i + 1 - i:\n" +
+     "  default:\n" +
+     "    " + long_throw_stmt +
+     "  case i + i:\n" +
+     "    i++; break; }");
+
+// long case when JSOP_CASE is used (JSOP_DEFAULTX)
+test("switch (i) {\n" +
+     "  case i + 1 - i:\n" +
+     "    " + long_throw_stmt +
+     "  default:\n" +
+     "    i++; break; }");
--- a/js/src/jit-test/tests/debug/Script-getOffsetLine-01.js
+++ b/js/src/jit-test/tests/debug/Script-getOffsetLine-01.js
@@ -1,18 +1,18 @@
 // Basic getOffsetLine test, using Error.lineNumber as the gold standard.
 
 var g = newGlobal('new-compartment');
 var dbg = Debug(g);
 var hits;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	var knownLine = frame.eval("line").return;
-	assertEq(frame.script.getOffsetLine(frame.offset), knownLine);
-	hits++;
+        var knownLine = frame.eval("line").return;
+        assertEq(frame.script.getOffsetLine(frame.offset), knownLine);
+        hits++;
     }
 };
 
 hits = 0;
 g.eval("var line = new Error().lineNumber; debugger;");
 assertEq(hits, 1);
 
 hits = 0;
--- a/js/src/jit-test/tests/debug/Script-getOffsetLine-02.js
+++ b/js/src/jit-test/tests/debug/Script-getOffsetLine-02.js
@@ -1,20 +1,20 @@
 // Frame.script/offset and Script.getOffsetLine work in non-top frames.
 
 var g = newGlobal('new-compartment');
 var dbg = Debug(g);
 var hits = 0;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	var a = [];
-	for (; frame.type == "call"; frame = frame.older)
-	    a.push(frame.script.getOffsetLine(frame.offset) - g.line0);
-	assertEq(a.join(","), "1,2,3,4");
-	hits++;
+        var a = [];
+        for (; frame.type == "call"; frame = frame.older)
+            a.push(frame.script.getOffsetLine(frame.offset) - g.line0);
+        assertEq(a.join(","), "1,2,3,4");
+        hits++;
     }
 };
 g.eval("var line0 = Error().lineNumber;\n" +
        "function f0() { debugger; }\n" +
        "function f1() { f0(); }\n" +
        "function f2() { f1(); }\n" +
        "function f3() { f2(); }\n" +
        "f3();\n");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-01.js
@@ -0,0 +1,24 @@
+// Basic breakpoint test.
+
+var g = newGlobal('new-compartment');
+g.s = '';
+var handler = {
+    hit: function (frame) {
+        assertEq(this, handler);
+        g.s += '1';
+    }
+};
+var dbg = Debug(g);
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        g.s += '0';
+        var line0 = frame.script.getOffsetLine(frame.offset);
+        var offs = frame.script.getLineOffsets(line0 + 2);
+        for (var i = 0; i < offs.length; i++)
+            frame.script.setBreakpoint(offs[i], handler);
+    }
+};
+g.eval("debugger;\n" +
+       "s += 'a';\n" +  // line0 + 1
+       "s += 'b';\n");  // line0 + 2
+assertEq(g.s, "0a1b");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-02.js
@@ -0,0 +1,15 @@
+// Setting a breakpoint in a non-debuggee Script is an error.
+
+load(libdir + "asserts.js");
+
+var g1 = newGlobal('new-compartment');
+var g2 = g1.eval("newGlobal('same-compartment')");
+g2.eval("function f() { return 2; }");
+g1.f = g2.f;
+
+var dbg = Debug(g1);
+var s;
+dbg.hooks = {debuggerHandler: function (frame) { s = frame.eval("f").return.script; }};
+g1.eval("debugger;");
+
+assertThrowsInstanceOf(function () { s.setBreakpoint(0, {}); }, Error);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-03.js
@@ -0,0 +1,17 @@
+// Setting a breakpoint in a script we are no longer debugging is an error.
+
+load(libdir + "asserts.js");
+
+var g = newGlobal('new-compartment');
+var dbg = Debug();
+var gobj = dbg.addDebuggee(g);
+g.eval("function f() { return 2; }");
+
+var s;
+dbg.hooks = {debuggerHandler: function (frame) { s = frame.eval("f").return.script; }};
+g.eval("debugger;");
+assertEq(s.live, true);
+s.setBreakpoint(0, {});  // ok
+
+dbg.removeDebuggee(gobj);
+assertThrowsInstanceOf(function () { s.setBreakpoint(0, {}); }, Error);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-04.js
@@ -0,0 +1,32 @@
+// Hitting a breakpoint with no hit method does nothing.
+
+var g = newGlobal('new-compartment');
+g.s = '';
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f() {\n" +   // line0 + 1
+       "    debugger;\n" +  // line0 + 2
+       "    s += 'x';\n" +  // line0 + 3
+       "}\n")
+var dbg = Debug(g);
+var bp = [];
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        g.s += 'D';
+        var arr = frame.script.getLineOffsets(g.line0 + 3);
+        for (var i = 0; i < arr.length; i++) {
+            var obj = {};
+            bp[i] = obj;
+            frame.script.setBreakpoint(arr[i], obj);
+        }
+    }
+};
+
+g.f();
+assertEq(g.s, "Dx");
+
+delete dbg.hooks.debuggerHandler;
+
+for (var i = 0; i < bp.length; i++)
+    bp[i].hit = function () { g.s += 'B'; };
+g.f();
+assertEq(g.s, "DxBx");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-05.js
@@ -0,0 +1,21 @@
+// If the offset parameter to setBreakpoint is invalid, throw an error.
+
+load(libdir + "asserts.js");
+
+var g = newGlobal('new-compartment');
+var dbg = new Debug(g);
+var hits = 0;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        // We assume at least one offset between 0 and frame.offset is invalid.
+        assertThrowsInstanceOf(
+            function () {
+                for (var i = 0; i < frame.offset; i++)
+                    frame.script.setBreakpoint(i, {});
+            },
+            Error);
+        hits++;
+    }
+};
+g.eval("x = 256; debugger;");
+assertEq(hits, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-06.js
@@ -0,0 +1,22 @@
+// The argument to a breakpoint hit method is a frame.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+var hits = 0;
+dbg.hooks = {
+    debuggerHandler: function (frame1) {
+        function hit(frame2) {
+            assertEq(frame2, frame1);
+            hits++;
+        }
+        var s = frame1.script;
+        var offs = s.getLineOffsets(g.line0 + 2);
+        for (var i = 0; i < offs.length; i++)
+            s.setBreakpoint(offs[i], {hit: hit});
+    }
+};
+g.eval("var line0 = Error().lineNumber;\n" +
+       "debugger;\n" +  // line0 + 1
+       "x = 1;\n");     // line0 + 2
+assertEq(hits, 1);
+assertEq(g.x, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-07.js
@@ -0,0 +1,32 @@
+// Code runs fine if do-nothing breakpoints are set on every line.
+
+var g = newGlobal('new-compartment');
+var src = ("var line0 = Error().lineNumber;\n" +
+           "function gcd(a, b) {\n" +       // line0 + 1
+           "    if (a > b)\n" +             // line0 + 2
+           "            return gcd(b, a);\n" +  // line0 + 3
+           "    var c = b % a;\n" +         // line0 + 4
+           "    if (c === 0)\n" +           // line0 + 5
+           "            return a;\n" +          // line0 + 6
+           "    return gcd(c, a);\n" +      // line0 + 7
+           "}\n");                          // line0 + 8
+g.eval(src);
+
+var dbg = Debug(g);
+var hits = 0 ;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var s = frame.eval("gcd").return.script;
+        var offs;
+        for (var lineno = g.line0 + 2; (offs = s.getLineOffsets(lineno)).length > 0; lineno++) {
+            for (var i = 0; i < offs.length; i++)
+                s.setBreakpoint(offs[i], {hit: function (f) { hits++; }});
+        }
+        assertEq(lineno > g.line0 + 7, true);
+        assertEq(lineno <= g.line0 + 9, true);
+    }
+};
+
+g.eval("debugger;");
+assertEq(g.gcd(31 * 7 * 5 * 3 * 2, 11 * 3 * 3 * 2), 6);
+assertEq(hits >= 18, true);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-08.js
@@ -0,0 +1,33 @@
+// Breakpoints are dropped from eval scripts when they finish executing.
+// (The eval cache doesn't cache breakpoints.)
+
+var g = newGlobal('new-compartment');
+
+g.line0 = undefined;
+g.eval("function f() {\n" +
+       "    return eval(s);\n" +
+       "}\n");
+g.s = ("line0 = Error().lineNumber;\n" +
+       "debugger;\n" +          // line0 + 1
+       "result = 'ok';\n");     // line0 + 2
+
+var dbg = Debug(g);
+var hits = 0, bphits = 0;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        assertEq(frame.type, 'eval');
+        assertEq(frame.script.getBreakpoints().length, 0);
+        var h = {hit: function (frame) { bphits++; }};
+        var offs = frame.script.getLineOffsets(g.line0 + 2);
+        for (var i = 0; i < offs.length; i++)
+            frame.script.setBreakpoint(offs[i], h);
+        hits++;
+    }
+};
+
+for (var i = 0; i < 3; i++) {
+    assertEq(g.f(), 'ok');
+    assertEq(g.result, 'ok');
+}
+assertEq(hits, 3);
+assertEq(bphits, 3);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-gc-01.js
@@ -0,0 +1,27 @@
+// Handlers for breakpoints in an eval script are live as long as the script is on the stack.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+var log = '';
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        function handler(i) {
+            return {hit: function () { log += '' + i; }};
+        }
+
+        var s = frame.script;
+        var offs = s.getLineOffsets(g.line0 + 2);
+        for (var i = 0; i < 7; i++) {
+            var h = handler(i);
+            for (var j = 0; j < offs.length; j++)
+                s.setBreakpoint(offs[j], h);
+        }
+        gc();
+    }
+};
+
+
+g.eval("var line0 = Error().lineNumber;\n" +
+       "debugger;\n" +  // line0 + 1
+       "x = 1;\n");     // line0 + 2
+assertEq(log, '0123456');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-gc-02.js
@@ -0,0 +1,30 @@
+// A Debug with live breakpoints is live.
+
+var g = newGlobal('new-compartment');
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f() {\n" +     // line0 + 1
+       "    return 2;\n" +      // line0 + 2
+       "}\n");
+
+var N = 4;
+var hits = 0;
+for (var i = 0; i < N; i++) {
+    var dbg = Debug(g);
+    dbg.hooks = {
+        debuggerHandler: function (frame) {
+            var handler = {hit: function () { hits++; }};
+            var s = frame.eval("f").return.script;
+            var offs = s.getLineOffsets(g.line0 + 2);
+            for (var j = 0; j < offs.length; j++)
+                s.setBreakpoint(offs[j], handler);
+        }
+    }
+    g.eval('debugger;');
+    dbg.hooks = {};
+    dbg = null;
+}
+
+gc();
+
+assertEq(g.f(), 2);
+assertEq(hits, N);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-multi-01.js
@@ -0,0 +1,30 @@
+// A single Debug object can set multiple breakpoints at an instruction.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+var log = '';
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        log += 'D';
+        function handler(i) {
+            return {hit: function (frame) { log += '' + i; }};
+        }
+        var f = frame.eval("f").return;
+        var s = f.script;
+        var offs = s.getLineOffsets(g.line0 + 2);
+        for (var i = 0; i < 10; i++) {
+            var bp = handler(i);
+            for (var j = 0; j < offs.length; j++)
+                s.setBreakpoint(offs[j], bp);
+        }
+        assertEq(f.call().return, 42);
+        log += 'X';
+    }
+};
+
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f() {\n" +  // line0 + 1
+       "    return 42;\n" +  // line0 + 2
+       "}\n" +
+       "debugger;\n");
+assertEq(log, 'D0123456789X');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-multi-02.js
@@ -0,0 +1,44 @@
+// After clearing one breakpoint, another breakpoint at the same instruction still works.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+var script = null;
+var handlers = [];
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        function handler(i) {
+            return {hit: function (frame) { g.log += '' + i; }};
+        }
+        var f = frame.eval("f").return;
+        var s = f.script;
+        if (script === null)
+            script = s;
+        else
+            assertEq(s, script);
+
+        var offs = s.getLineOffsets(g.line0 + 3);
+        for (var i = 0; i < 3; i++) {
+            handlers[i] = handler(i);
+            for (var j = 0; j < offs.length; j++)
+                s.setBreakpoint(offs[j], handlers[i]);
+        }
+    }
+};
+
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f() {\n" +     // line0 + 1
+       "    log += 'x';\n" +    // line0 + 2
+       "    log += 'y';\n" +    // line0 + 3
+       "}\n" +
+       "debugger;\n");
+assertEq(handlers.length, 3);
+
+g.log = '';
+g.f();
+assertEq(g.log, 'x012y');
+
+script.clearBreakpoint(handlers[0]);
+
+g.log = '';
+g.f();
+assertEq(g.log, 'x12y');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-multi-03.js
@@ -0,0 +1,29 @@
+// Multiple Debug objects can set breakpoints at the same instruction.
+
+var g = newGlobal('new-compartment');
+function attach(g, i) {
+    var dbg = Debug(g);
+    dbg.hooks = {
+        debuggerHandler: function (frame) {
+            var s = frame.eval("f").return.script;
+            var offs = s.getLineOffsets(g.line0 + 3);
+            for (var j = 0; j < offs.length; j++)
+                s.setBreakpoint(offs[j], {hit: function () { g.log += "" + i; }});
+        }
+    };
+}
+
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f() {\n" +     // line0 + 1
+       "    log += 'a';\n" +    // line0 + 2
+       "    log += 'b';\n" +    // line0 + 3
+       "}\n");
+
+for (var i = 0; i < 3; i++)
+    attach(g, i);
+
+g.log = '';
+g.eval('debugger;');
+g.log += 'x';
+g.f();
+assertEq(g.log, 'xa012b');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-multi-04.js
@@ -0,0 +1,50 @@
+// After clearing one breakpoint, another breakpoint from a different Debug object at the same instruction still works.
+
+function test(which) {
+    var g = newGlobal('new-compartment');
+    g.eval("var line0 = Error().lineNumber;\n" +
+           "function f() {\n" +             // line0 + 1
+           "    return " + which + ";\n" +  // line0 + 2
+           "}\n");
+
+    var log;
+    var scripts = [];
+    var handlers = [];
+    function addDebugger(g, i) {
+        var dbg = Debug(g);
+        dbg.hooks = {
+            debuggerHandler: function (frame) {
+                var s = frame.eval("f").return.script;
+                scripts[i] = s;
+                var offs = s.getLineOffsets(g.line0 + 2);
+                var handler = {hit: function (frame) { log += '' + i; } };
+                s.setBreakpoint(0, handler);
+                handlers[i] = handler;
+            }
+        };
+    }
+
+    var expected = '';
+    for (var i = 0; i < 3; i++) {
+        addDebugger(g, i);
+        if (i !== which)
+            expected += '' + i;
+    }
+    g.eval('debugger;');
+
+    for (var i = 0; i < 3; i++)
+        assertEq(scripts[i].getBreakpoints()[0], handlers[i]);
+
+    log = '';
+    g.f();
+    assertEq(log, '012');
+
+    scripts[which].clearAllBreakpoints();
+
+    log = '';
+    g.f();
+    assertEq(log, expected);
+}
+
+for (var j = 0; j < 3; j++)
+    test(j);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-resume-01.js
@@ -0,0 +1,27 @@
+// A breakpoint handler hit method can return {throw: exc} to throw an exception.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        function hit(frame) {
+            return {throw: frame.eval("new Error('PASS')").return};
+        }
+
+        var s = frame.script;
+        var offs = s.getLineOffsets(g.line0 + 3);
+        for (var i = 0; i < offs.length; i++)
+            s.setBreakpoint(offs[i], {hit: hit});
+    }
+};
+
+g.s = null;
+g.eval("line0 = Error().lineNumber;\n" +
+       "debugger;\n" +          // line0 + 1
+       "try {\n" +              // line0 + 2
+       "    s = 'FAIL';\n" +    // line0 + 3
+       "} catch (exc) {\n" +
+       "    assertEq(exc.constructor, Error);\n" +
+       "    s = exc.message;\n" +
+       "}\n");
+assertEq(g.s, 'PASS');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-resume-02.js
@@ -0,0 +1,36 @@
+// A breakpoint handler hit method can return null to raise an uncatchable error.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        g.log += 'D';
+
+        function hit(frame) {
+            g.log += 'H';
+            return null;
+        }
+
+        var f = frame.eval("f").return;
+        var s = f.script;
+        var offs = s.getLineOffsets(g.line0 + 3);
+        for (var i = 0; i < offs.length; i++)
+            s.setBreakpoint(offs[i], {hit: hit});
+
+        var rv = f.call();
+        assertEq(rv, null);
+        g.log += 'X';
+    }
+};
+
+g.log = '';
+g.eval("line0 = Error().lineNumber;\n" +
+       "function f() {\n" +         // line0 + 1
+       "    try {\n" +              // line0 + 2
+       "        log += '3';\n" +    // line0 + 3
+       "    } catch (exc) {\n" +
+       "        log += '5';\n" +
+       "    }\n" +
+       "}\n" +
+       "debugger;\n");
+assertEq(g.log, 'DHX');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-resume-03.js
@@ -0,0 +1,32 @@
+// A breakpoint handler hit method can return {return: val} to force the frame to return.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        g.log += 'D';
+
+        function hit(frame) {
+            g.log += 'H';
+            return {return: 'ok'};
+        }
+
+        var f = frame.eval("f").return;
+        var s = f.script;
+        var offs = s.getLineOffsets(g.line0 + 2);
+        for (var i = 0; i < offs.length; i++)
+            s.setBreakpoint(offs[i], {hit: hit});
+
+        var rv = f.call();
+        assertEq(rv.return, 'ok');
+        g.log += 'X';
+    }
+};
+
+g.log = '';
+g.eval("line0 = Error().lineNumber;\n" +
+       "function f() {\n" +     // line0 + 1
+       "    log += '2';\n" +    // line0 + 2
+       "}\n" +
+       "debugger;\n");
+assertEq(g.log, 'DHX');
--- a/js/src/jit-test/tests/debug/surfaces-offsets.js
+++ b/js/src/jit-test/tests/debug/surfaces-offsets.js
@@ -2,29 +2,38 @@
 
 load(libdir + "asserts.js");
 
 var g = newGlobal('new-compartment');
 var dbg = Debug(g);
 var hits;
 dbg.hooks = {
     debuggerHandler: function (frame) {
-	assertEq(frame.script.getOffsetLine(frame.offset), g.line);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(String(frame.offset)); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(Object(frame.offset)); }, Error);
+        assertEq(frame.script.getOffsetLine(frame.offset), g.line);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(String(frame.offset)); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(Object(frame.offset)); }, Error);
+
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(-1); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(1000000); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(0.25); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(+Infinity); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(-Infinity); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(NaN); }, Error);
 
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(-1); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(1000000); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(0.25); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(+Infinity); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(-Infinity); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(NaN); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(false); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(true); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(undefined); }, Error);
+        assertThrowsInstanceOf(function () { frame.script.getOffsetLine(); }, Error);
 
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(false); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(true); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(undefined); }, Error);
-	assertThrowsInstanceOf(function () { frame.script.getOffsetLine(); }, Error);
-	hits++;
+        // We assume that at least one whole number between 0 and frame.offset is invalid.
+        assertThrowsInstanceOf(
+            function () {
+                for (var i = 0; i < frame.offset; i++)
+                    frame.script.getOffsetLine(i);
+            },
+            Error);
+
+        hits++;
     }
 };
 
 hits = 0;
 g.eval("var line = new Error().lineNumber; debugger;");
--- a/js/src/jit-test/tests/jaeger/bug563000/simple-trap-1.js
+++ b/js/src/jit-test/tests/jaeger/bug563000/simple-trap-1.js
@@ -1,10 +1,10 @@
 // |jit-test| debug
 setDebug(true);
 var x = "failure";
 function main() { x = "success"; }
 
-/* The JSOP_STOP in a. */
-trap(main, 11, "");
+/* The JSOP_STOP in main. */
+trap(main, 10, "");
 main();
 
 assertEq(x, "success");
--- a/js/src/jit-test/tests/jaeger/bug563000/simple-trap-2.js
+++ b/js/src/jit-test/tests/jaeger/bug563000/simple-trap-2.js
@@ -1,11 +1,11 @@
 // |jit-test| debug
 setDebug(true);
 var x = "notset";
 function main() { x = "failure"; }
 function success() { x = "success"; }
 
-/* The JSOP_STOP in a. */
+/* The JSOP_STOP in main. */
 trap(main, 10, "success()");
 main();
 
 assertEq(x, "success");
--- a/js/src/jit-test/tests/jaeger/bug563000/simple-untrap.js
+++ b/js/src/jit-test/tests/jaeger/bug563000/simple-untrap.js
@@ -1,12 +1,12 @@
 // |jit-test| debug
 setDebug(true);
 var x = "notset";
 function main() { x = "success"; }
 function failure() { x = "failure"; }
 
-/* The JSOP_STOP in a. */
-trap(main, 8, "failure()");
-untrap(main, 8);
+/* The JSOP_STOP in main. */
+trap(main, 10, "failure()");
+untrap(main, 10);
 main();
 
 assertEq(x, "success");
--- a/js/src/jit-test/tests/jaeger/bug563000/trap-from-add-inline.js
+++ b/js/src/jit-test/tests/jaeger/bug563000/trap-from-add-inline.js
@@ -1,13 +1,13 @@
 // |jit-test| debug
 setDebug(true);
 x = "notset";
 function main() {
-  /* The JSOP_STOP in a. */
+  /* The JSOP_STOP in main. */
   a = { valueOf: function () { trap(main, 36, "success()"); } };
   a + "";
   x = "failure";
 }
 function success() { x = "success"; }
 
 main();
 assertEq(x, "success");
--- a/js/src/jit-test/tests/jaeger/bug563000/trap-from-add-ool.js
+++ b/js/src/jit-test/tests/jaeger/bug563000/trap-from-add-ool.js
@@ -1,14 +1,14 @@
 // |jit-test| debug
 setDebug(true);
 x = "notset";
 function main() {
-  /* The JSOP_STOP in a. */
-  a = { valueOf: function () { trap(main, 57, "success()"); } };
+  /* The JSOP_STOP in main. */
+  a = { valueOf: function () { trap(main, 58, "success()"); } };
   b = "";
   eval();
   a + b;
   x = "failure";
 }
 function success() { x = "success"; }
 
 main();
--- a/js/src/jit-test/tests/jaeger/bug563000/trap-self.js
+++ b/js/src/jit-test/tests/jaeger/bug563000/trap-self.js
@@ -1,12 +1,12 @@
 // |jit-test| debug
 setDebug(true);
 x = "notset";
 function main() {
-  /* The JSOP_STOP in a. */
+  /* The JSOP_STOP in main. */
   trap(main, 25, "success()");
   x = "failure";
 }
 function success() { x = "success"; }
 
 main();
 assertEq(x, "success");
--- a/js/src/jit-test/tests/jaeger/bug563000/untrap-self.js
+++ b/js/src/jit-test/tests/jaeger/bug563000/untrap-self.js
@@ -1,14 +1,14 @@
 // |jit-test| debug
 setDebug(true);
 x = "notset";
 function main() {
   /* JSOP_STOP in main. */
-  untrap(main, 23);
+  untrap(main, 22);
   x = "success";
 }
 function failure() { x = "failure"; }
 
 /* JSOP_STOP in main. */
-trap(main, 23, "failure()");
+trap(main, 22, "failure()");
 main();
 assertEq(x, "success");
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -355,8 +355,9 @@ MSG_DEF(JSMSG_DEBUG_BAD_RESUMPTION,   27
 MSG_DEF(JSMSG_ASSIGN_FUNCTION_OR_NULL, 273, 1, JSEXN_TYPEERR, "value assigned to {0} must be a function or null")
 MSG_DEF(JSMSG_DEBUG_NOT_LIVE,         274, 1, JSEXN_ERR, "{0} is not live")
 MSG_DEF(JSMSG_DEBUG_OBJECT_WRONG_OWNER, 275, 0, JSEXN_TYPEERR, "Debug.Object belongs to a different Debug")
 MSG_DEF(JSMSG_DEBUG_OBJECT_PROTO,     276, 0, JSEXN_TYPEERR, "Debug.Object.prototype is not a valid Debug.Object")
 MSG_DEF(JSMSG_DEBUG_LOOP,             277, 0, JSEXN_TYPEERR, "cannot debug an object in same compartment as debugger or a compartment that is already debugging the debugger")
 MSG_DEF(JSMSG_DEBUG_NOT_IDLE,         278, 0, JSEXN_ERR, "can't start debugging: a debuggee script is on the stack")
 MSG_DEF(JSMSG_DEBUG_BAD_OFFSET,       279, 0, JSEXN_TYPEERR, "invalid script offset")
 MSG_DEF(JSMSG_DEBUG_BAD_LINE,         280, 0, JSEXN_TYPEERR, "invalid line number")
+MSG_DEF(JSMSG_DEBUG_NOT_DEBUGGING,    281, 0, JSEXN_ERR, "can't set breakpoint: script global is not a debuggee")
--- a/js/src/jsapi-tests/testTrap.cpp
+++ b/js/src/jsapi-tests/testTrap.cpp
@@ -49,27 +49,27 @@ BEGIN_TEST(testTrap_gc)
 
     // scope JSScript  usage to make sure that it is not used after
     // JS_ExecuteScript. This way we avoid using Anchor.
     JSString *trapClosure;
     {
         JSScript *script = JS_GetScriptFromObject(scriptObj);
         jsbytecode *line2 = JS_LineNumberToPC(cx, script, 1);
         CHECK(line2);
-        
+
         jsbytecode *line6 = JS_LineNumberToPC(cx, script, 5);
         CHECK(line2);
-        
+
         trapClosure = JS_NewStringCopyZ(cx, trapClosureText);
         CHECK(trapClosure);
         JS_SetTrap(cx, script, line2, EmptyTrapHandler, STRING_TO_JSVAL(trapClosure));
         JS_SetTrap(cx, script, line6, EmptyTrapHandler, STRING_TO_JSVAL(trapClosure));
-        
+
         JS_GC(cx);
-        
+
         CHECK(JS_FlatStringEqualsAscii(JS_ASSERT_STRING_IS_FLAT(trapClosure), trapClosureText));
     }
 
     // execute
     CHECK(JS_ExecuteScript(cx, global, scriptObj, v2.addr()));
     CHECK(emptyTrapCallCount == 11);
 
     JS_GC(cx);
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -642,17 +642,16 @@ JS_IsBuiltinFunctionConstructor(JSFuncti
 static JSBool js_NewRuntimeWasCalled = JS_FALSE;
 #endif
 
 JSRuntime::JSRuntime()
   : gcChunkAllocator(&defaultGCChunkAllocator)
 {
     /* Initialize infallibly first, so we can goto bad and JS_DestroyRuntime. */
     JS_INIT_CLIST(&contextList);
-    JS_INIT_CLIST(&trapList);
     JS_INIT_CLIST(&watchPointList);
     JS_INIT_CLIST(&debuggerList);
 }
 
 bool
 JSRuntime::init(uint32 maxbytes)
 {
 #ifdef JS_METHODJIT_SPEW
--- a/js/src/jscntxt.cpp
+++ b/js/src/jscntxt.cpp
@@ -615,17 +615,18 @@ js_DestroyContext(JSContext *cx, JSDestr
 #endif
 
             js_FinishRuntimeNumberState(cx);
 
             /* Unpin all common atoms before final GC. */
             js_FinishCommonAtoms(cx);
 
             /* Clear debugging state to remove GC roots. */
-            JS_ClearAllTraps(cx);
+            for (JSCompartment **c = rt->compartments.begin(); c != rt->compartments.end(); c++)
+                (*c)->clearTraps(cx, NULL);
             JS_ClearAllWatchPoints(cx);
         }
 
 #ifdef JS_THREADSAFE
         /*
          * Destroying a context implicitly calls JS_EndRequest().  Also, we must
          * end our request here in case we are "last" -- in that event, another
          * js_DestroyContext that was not last might be waiting in the GC for our
--- a/js/src/jscntxt.h
+++ b/js/src/jscntxt.h
@@ -539,17 +539,16 @@ struct JSRuntime {
     /* True if any debug hooks not supported by the JIT are enabled. */
     bool debuggerInhibitsJIT() const {
         return (globalDebugHooks.interruptHook ||
                 globalDebugHooks.callHook);
     }
 #endif
 
     /* More debugging state, see jsdbgapi.c. */
-    JSCList             trapList;
     JSCList             watchPointList;
 
     /*
      * Linked list of all js::Debug objects. This may be accessed by the GC
      * thread, if any, or a thread that is in a request and holds gcLock.
      */
     JSCList             debuggerList;
 
@@ -571,18 +570,18 @@ struct JSRuntime {
 #ifdef DEBUG
     void *              rtLockOwner;
 #endif
 
     /* Used to synchronize down/up state change; protected by gcLock. */
     PRCondVar           *stateChange;
 
     /*
-     * Lock serializing trapList and watchPointList accesses, and count of all
-     * mutations to trapList and watchPointList made by debugger threads.  To
+     * Lock serializing watchPointList accesses, and count of all
+     * mutations to watchPointList made by debugger threads.  To
      * keep the code simple, we define debuggerMutations for the thread-unsafe
      * case too.
      */
     PRLock              *debuggerLock;
 
     JSThread::Map       threads;
 #endif /* JS_THREADSAFE */
     uint32              debuggerMutations;
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -35,16 +35,17 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "jscntxt.h"
 #include "jscompartment.h"
+#include "jsdbg.h"
 #include "jsgc.h"
 #include "jsgcmark.h"
 #include "jsiter.h"
 #include "jsproxy.h"
 #include "jsscope.h"
 #include "jstracer.h"
 #include "jswrapper.h"
 #include "assembler/wtf/Platform.h"
@@ -84,17 +85,18 @@ JSCompartment::JSCompartment(JSRuntime *
     emptyBlockShape(NULL),
     emptyCallShape(NULL),
     emptyDeclEnvShape(NULL),
     emptyEnumeratorShape(NULL),
     emptyWithShape(NULL),
     initialRegExpShape(NULL),
     initialStringShape(NULL),
     debugModeBits(rt->debugMode * DebugFromC),
-    mathCache(NULL)
+    mathCache(NULL),
+    breakpointSites(rt)
 {
     JS_INIT_CLIST(&scripts);
 
     PodArrayZero(scriptsToGC);
 }
 
 JSCompartment::~JSCompartment()
 {
@@ -144,17 +146,17 @@ JSCompartment::init()
         return false;
 
 #ifdef JS_METHODJIT
     jaegerCompartment = rt->new_<mjit::JaegerCompartment>();
     if (!jaegerCompartment || !jaegerCompartment->Initialize())
         return false;
 #endif
 
-    return debuggees.init();
+    return debuggees.init() && breakpointSites.init();
 }
 
 #ifdef JS_METHODJIT
 size_t
 JSCompartment::getMjitCodeSize() const
 {
     return jaegerCompartment->execAlloc()->getCodeSize();
 }
@@ -483,16 +485,18 @@ JSCompartment::sweep(JSContext *cx, uint
     if (emptyWithShape && IsAboutToBeFinalized(cx, emptyWithShape))
         emptyWithShape = NULL;
 
     if (initialRegExpShape && IsAboutToBeFinalized(cx, initialRegExpShape))
         initialRegExpShape = NULL;
     if (initialStringShape && IsAboutToBeFinalized(cx, initialStringShape))
         initialStringShape = NULL;
 
+    sweepBreakpoints(cx);
+
 #ifdef JS_TRACER
     traceMonitor.sweep(cx);
 #endif
 
 #if defined JS_METHODJIT && defined JS_MONOIC
 
     /*
      * The release interval is the frequency with which we should try to destroy
@@ -702,8 +706,124 @@ JSCompartment::removeDebuggee(JSContext 
         debuggees.remove(global);
 
     if (debuggees.empty()) {
         debugModeBits &= ~DebugFromJS;
         if (wasEnabled && !debugMode())
             updateForDebugMode(cx);
     }
 }
+
+BreakpointSite *
+JSCompartment::getBreakpointSite(jsbytecode *pc)
+{
+    BreakpointSiteMap::Ptr p = breakpointSites.lookup(pc);
+    return p ? p->value : NULL;
+}
+
+BreakpointSite *
+JSCompartment::getOrCreateBreakpointSite(JSContext *cx, JSScript *script, jsbytecode *pc, JSObject *scriptObject)
+{
+    JS_ASSERT(script->code <= pc);
+    JS_ASSERT(pc < script->code + script->length);
+    BreakpointSiteMap::AddPtr p = breakpointSites.lookupForAdd(pc);
+    if (!p) {
+        BreakpointSite *site = cx->runtime->new_<BreakpointSite>(script, pc);
+        if (!site || !breakpointSites.add(p, pc, site)) {
+            js_ReportOutOfMemory(cx);
+            return NULL;
+        }
+    }
+
+    BreakpointSite *site = p->value;
+    JS_ASSERT_IF(scriptObject, scriptObject->isScript() || scriptObject->isFunction());
+    JS_ASSERT_IF(scriptObject && scriptObject->isFunction(),
+                 scriptObject->getFunctionPrivate()->script() == script);
+    JS_ASSERT_IF(scriptObject && scriptObject->isScript(), scriptObject->getScript() == script);
+    if (site->scriptObject)
+        JS_ASSERT_IF(scriptObject, site->scriptObject == scriptObject);
+    else
+        site->scriptObject = scriptObject;
+
+    return site;
+}
+
+void
+JSCompartment::clearBreakpointsIn(JSContext *cx, js::Debug *dbg, JSScript *script,
+                                  JSObject *handler)
+{
+    JS_ASSERT_IF(script, script->compartment == this);
+
+    for (BreakpointSiteMap::Enum e(breakpointSites); !e.empty(); e.popFront()) {
+        BreakpointSite *site = e.front().value;
+        if (!script || site->script == script) {
+            Breakpoint *next;
+            for (Breakpoint *bp = site->firstBreakpoint(); bp; bp = next) {
+                next = bp->nextInSite();
+                if ((!dbg || bp->debugger == dbg) && (!handler || bp->getHandler() == handler))
+                    bp->destroy(cx, &e);
+            }
+        }
+    }
+}
+
+void
+JSCompartment::clearTraps(JSContext *cx, JSScript *script)
+{
+    for (BreakpointSiteMap::Enum e(breakpointSites); !e.empty(); e.popFront()) {
+        BreakpointSite *site = e.front().value;
+        if (!script || site->script == script)
+            site->clearTrap(cx, &e);
+    }
+}
+
+bool
+JSCompartment::markBreakpointsIteratively(JSTracer *trc)
+{
+    bool markedAny = false;
+    for (BreakpointSiteMap::Range r = breakpointSites.all(); !r.empty(); r.popFront()) {
+        BreakpointSite *site = r.front().value;
+
+        // Mark jsdbgapi state if any. But if we know the scriptObject, put off
+        // marking trap state until we know the scriptObject is live.
+        if (site->trapHandler && (!site->scriptObject || site->scriptObject->isMarked())) {
+            if (site->trapClosure.isObject() && !site->trapClosure.toObject().isMarked())
+                markedAny = true;
+            MarkValue(trc, site->trapClosure, "trap closure");
+        }
+
+        // Mark jsdbg breakpoints. If either the debugger or the script is
+        // collected, then the breakpoint is collected along with it. So do not
+        // mark the handler in that case.
+        //
+        // If scriptObject is nonnull, examine it to see if the script will be
+        // collected. If scriptObject is null, then site->script is an eval
+        // script on the stack, so it is definitely live.
+        //
+        if (!site->scriptObject || site->scriptObject->isMarked()) {
+            for (Breakpoint *bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) {
+                if (bp->debugger->toJSObject()->isMarked() &&
+                    bp->handler &&
+                    !bp->handler->isMarked())
+                {
+                    MarkObject(trc, *bp->handler, "breakpoint handler");
+                    markedAny = true;
+                }
+            }
+        }
+    }
+    return markedAny;
+}
+
+void
+JSCompartment::sweepBreakpoints(JSContext *cx)
+{
+    for (BreakpointSiteMap::Enum e(breakpointSites); !e.empty(); e.popFront()) {
+        BreakpointSite *site = e.front().value;
+        if (site->scriptObject) {
+            bool scriptGone = IsAboutToBeFinalized(cx, site->scriptObject);
+            for (Breakpoint *bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) {
+                if (scriptGone || IsAboutToBeFinalized(cx, bp->debugger->toJSObject()))
+                    bp->destroy(cx, &e);
+            }
+        }
+    }
+}
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -517,16 +517,20 @@ struct JS_FRIEND_API(JSCompartment) {
     BackEdgeMap                  backEdgeTable;
 
     /*
      * Weak reference to each global in this compartment that is a debuggee.
      * Each global has its own list of debuggers.
      */
     js::GlobalObjectSet              debuggees;
 
+  public:
+    js::BreakpointSiteMap            breakpointSites;
+
+  private:
     JSCompartment *thisForCtor() { return this; }
   public:
     js::MathCache *getMathCache(JSContext *cx) {
         return mathCache ? mathCache : allocMathCache(cx);
     }
 
     size_t backEdgeCount(jsbytecode *pc) const;
     size_t incBackEdgeCount(jsbytecode *pc);
@@ -548,23 +552,29 @@ struct JS_FRIEND_API(JSCompartment) {
     bool hasScriptsOnStack(JSContext *cx);
 
   private:
     /* This is called only when debugMode() has just toggled. */
     void updateForDebugMode(JSContext *cx);
 
   public:
     js::GlobalObjectSet &getDebuggees() { return debuggees; }
-
     bool addDebuggee(JSContext *cx, js::GlobalObject *global);
-
     void removeDebuggee(JSContext *cx, js::GlobalObject *global,
                         js::GlobalObjectSet::Enum *debuggeesEnum = NULL);
+    bool setDebugModeFromC(JSContext *cx, bool b);
 
-    bool setDebugModeFromC(JSContext *cx, bool b);
+    js::BreakpointSite *getBreakpointSite(jsbytecode *pc);
+    js::BreakpointSite *getOrCreateBreakpointSite(JSContext *cx, JSScript *script, jsbytecode *pc,
+                                                  JSObject *scriptObject);
+    void clearBreakpointsIn(JSContext *cx, js::Debug *dbg, JSScript *script, JSObject *handler);
+    void clearTraps(JSContext *cx, JSScript *script);
+    bool markBreakpointsIteratively(JSTracer *trc);
+  private:
+    void sweepBreakpoints(JSContext *cx);
 };
 
 #define JS_SCRIPTS_TO_GC(cx)    ((cx)->compartment->scriptsToGC)
 #define JS_PROPERTY_TREE(cx)    ((cx)->compartment->propertyTree)
 
 #ifdef DEBUG
 #define JS_COMPARTMENT_METER(x) x
 #else
--- a/js/src/jsdbg.cpp
+++ b/js/src/jsdbg.cpp
@@ -44,16 +44,17 @@
 #include "jscntxt.h"
 #include "jsemit.h"
 #include "jsgcmark.h"
 #include "jsobj.h"
 #include "jswrapper.h"
 #include "jsinterpinlines.h"
 #include "jsobjinlines.h"
 #include "jsopcodeinlines.h"
+#include "methodjit/Retcon.h"
 #include "vm/Stack-inl.h"
 
 using namespace js;
 
 
 // === Forward declarations
 
 extern Class DebugFrame_class;
@@ -143,16 +144,199 @@ CheckThisClass(JSContext *cx, Value *vp,
 #define THISOBJ(cx, vp, classname, fnname, thisobj, private)                 \
     JSObject *thisobj =                                                      \
         CheckThisClass(cx, vp, &js::classname::jsclass, fnname);             \
     if (!thisobj)                                                            \
         return false;                                                        \
     js::classname *private = (classname *) thisobj->getPrivate();
 
 
+// === Breakpoints
+
+BreakpointSite::BreakpointSite(JSScript *script, jsbytecode *pc)
+    : script(script), pc(pc), realOpcode(JSOp(*pc)), scriptObject(NULL), enabledCount(0),
+      trapHandler(NULL), trapClosure(UndefinedValue())
+{
+    JS_ASSERT(realOpcode != JSOP_TRAP);
+    JS_INIT_CLIST(&breakpoints);
+}
+
+// Precondition: script is live, meaning either it is an eval script that is on
+// the stack or a non-eval script that hasn't been GC'd.
+static JSObject *
+ScriptScope(JSContext *cx, JSScript *script, JSObject *holder)
+{
+    if (holder)
+        return holder;
+
+    // The referent is an eval script. There is no direct reference from script
+    // to the scope, so find it on the stack.
+    for (AllFramesIter i(cx->stack.space()); ; ++i) {
+        JS_ASSERT(!i.done());
+        if (i.fp()->maybeScript() == script)
+            return &i.fp()->scopeChain();
+    }
+    JS_NOT_REACHED("ScriptScope: live eval script not on stack");
+}
+
+bool
+BreakpointSite::recompile(JSContext *cx, bool forTrap)
+{
+#ifdef JS_METHODJIT
+    if (script->hasJITCode()) {
+        Maybe<AutoCompartment> ac;
+        if (!forTrap) {
+            ac.construct(cx, ScriptScope(cx, script, scriptObject));
+            if (!ac.ref().enter())
+                return false;
+        }
+        js::mjit::Recompiler recompiler(cx, script);
+        if (!recompiler.recompile())
+            return false;
+    }
+#endif
+    return true;
+}
+
+bool
+BreakpointSite::inc(JSContext *cx)
+{
+    if (enabledCount == 0 && !trapHandler) {
+        JS_ASSERT(*pc == realOpcode);
+        *pc = JSOP_TRAP;
+        if (!recompile(cx, false)) {
+            *pc = realOpcode;
+            return false;
+        }
+    }
+    enabledCount++;
+    return true;
+}
+
+void
+BreakpointSite::dec(JSContext *cx)
+{
+    JS_ASSERT(enabledCount > 0);
+    JS_ASSERT(*pc == JSOP_TRAP);
+    enabledCount--;
+    if (enabledCount == 0 && !trapHandler) {
+        *pc = realOpcode;
+        recompile(cx, false);  // errors ignored
+    }
+}
+
+bool
+BreakpointSite::setTrap(JSContext *cx, JSTrapHandler handler, const Value &closure)
+{
+    if (enabledCount == 0) {
+        *pc = JSOP_TRAP;
+        if (!recompile(cx, true)) {
+            *pc = realOpcode;
+            return false;
+        }
+    }
+    trapHandler = handler;
+    trapClosure = closure;
+    return true;
+}
+
+void
+BreakpointSite::clearTrap(JSContext *cx, BreakpointSiteMap::Enum *e,
+                          JSTrapHandler *handlerp, Value *closurep)
+{
+    if (handlerp)
+        *handlerp = trapHandler;
+    if (closurep)
+        *closurep = trapClosure;
+
+    trapHandler = NULL;
+    trapClosure.setUndefined();
+    if (enabledCount == 0) {
+        *pc = realOpcode;
+        recompile(cx, true);  // ignore failure
+        destroyIfEmpty(cx->runtime, e);
+    }
+}
+
+void
+BreakpointSite::destroyIfEmpty(JSRuntime *rt, BreakpointSiteMap::Enum *e)
+{
+    if (JS_CLIST_IS_EMPTY(&breakpoints) && !trapHandler) {
+        if (e)
+            e->removeFront();
+        else
+            script->compartment->breakpointSites.remove(pc);
+        rt->delete_(this);
+    }
+}
+
+Breakpoint *
+BreakpointSite::firstBreakpoint() const
+{
+    if (JS_CLIST_IS_EMPTY(&breakpoints))
+        return NULL;
+    return Breakpoint::fromSiteLinks(JS_NEXT_LINK(&breakpoints));
+}
+
+bool
+BreakpointSite::hasBreakpoint(Breakpoint *bp)
+{
+    for (Breakpoint *p = firstBreakpoint(); p; p = p->nextInSite())
+        if (p == bp)
+            return true;
+    return false;
+}
+
+
+Breakpoint::Breakpoint(Debug *debugger, BreakpointSite *site, JSObject *handler)
+    : debugger(debugger), site(site), handler(handler)
+{
+    JS_APPEND_LINK(&debuggerLinks, &debugger->breakpoints);
+    JS_APPEND_LINK(&siteLinks, &site->breakpoints);
+}
+
+Breakpoint *
+Breakpoint::fromDebuggerLinks(JSCList *links)
+{
+    return (Breakpoint *) ((unsigned char *) links - offsetof(Breakpoint, debuggerLinks));
+}
+
+Breakpoint *
+Breakpoint::fromSiteLinks(JSCList *links)
+{
+    return (Breakpoint *) ((unsigned char *) links - offsetof(Breakpoint, siteLinks));
+}
+
+void
+Breakpoint::destroy(JSContext *cx, BreakpointSiteMap::Enum *e)
+{
+    if (debugger->enabled)
+        site->dec(cx);
+    JS_REMOVE_LINK(&debuggerLinks);
+    JS_REMOVE_LINK(&siteLinks);
+    JSRuntime *rt = cx->runtime;
+    site->destroyIfEmpty(rt, e);
+    rt->delete_(this);
+}
+
+Breakpoint *
+Breakpoint::nextInDebugger()
+{
+    JSCList *link = JS_NEXT_LINK(&debuggerLinks);
+    return (link == &debugger->breakpoints) ? NULL : fromDebuggerLinks(link);
+}
+
+Breakpoint *
+Breakpoint::nextInSite()
+{
+    JSCList *link = JS_NEXT_LINK(&siteLinks);
+    return (link == &site->breakpoints) ? NULL : fromSiteLinks(link);
+}
+
+
 // === Debug hook dispatch
 
 enum {
     JSSLOT_DEBUG_FRAME_PROTO,
     JSSLOT_DEBUG_OBJECT_PROTO,
     JSSLOT_DEBUG_SCRIPT_PROTO,
     JSSLOT_DEBUG_COUNT
 };
@@ -161,16 +345,17 @@ Debug::Debug(JSObject *dbg, JSObject *ho
   : object(dbg), hooksObject(hooks), uncaughtExceptionHook(NULL), enabled(true),
     hasDebuggerHandler(false), hasThrowHandler(false),
     objects(dbg->compartment()->rt), heldScripts(dbg->compartment()->rt)
 {
     // This always happens within a request on some cx.
     JSRuntime *rt = dbg->compartment()->rt;
     AutoLockGC lock(rt);
     JS_APPEND_LINK(&link, &rt->debuggerList);
+    JS_INIT_CLIST(&breakpoints);
 }
 
 Debug::~Debug()
 {
     JS_ASSERT(debuggees.empty());
 
     // This always happens in the GC thread, so no locking is required.
     JS_ASSERT(object->compartment()->rt->gcRunning);
@@ -240,16 +425,23 @@ Debug::slowPathLeaveStackFrame(JSContext
             Debug *dbg = *p;
             if (FrameMap::Ptr p = dbg->frames.lookup(fp)) {
                 JSObject *frameobj = p->value;
                 frameobj->setPrivate(NULL);
                 dbg->frames.remove(p);
             }
         }
     }
+
+    // If this is an eval frame, then from the debugger's perspective the
+    // script is about to be destroyed. Remove any breakpoints in it.
+    if (fp->isEvalFrame()) {
+        JSScript *script = fp->script();
+        script->compartment->clearBreakpointsIn(cx, NULL, script, NULL);
+    }
 }
 
 bool
 Debug::wrapDebuggeeValue(JSContext *cx, Value *vp)
 {
     assertSameCompartment(cx, object);
 
     if (vp->isObject()) {
@@ -455,17 +647,16 @@ bool
 Debug::observesThrow() const
 {
     return enabled && hasThrowHandler;
 }
 
 JSTrapStatus
 Debug::handleThrow(JSContext *cx, Value *vp)
 {
-    // Grab cx->fp() and the exception value before preparing to call the hook.
     StackFrame *fp = cx->fp();
     Value exc = cx->getPendingException();
 
     cx->clearPendingException();
     JS_ASSERT(hasThrowHandler);
     AutoCompartment ac(cx, hooksObject);
     if (!ac.enter())
         return JSTRAP_ERROR;
@@ -512,16 +703,72 @@ Debug::dispatchHook(JSContext *cx, js::V
             JSTrapStatus st = (dbg->*handleEvent)(cx, vp);
             if (st != JSTRAP_CONTINUE)
                 return st;
         }
     }
     return JSTRAP_CONTINUE;
 }
 
+JSTrapStatus
+Debug::onTrap(JSContext *cx, Value *vp)
+{
+    StackFrame *fp = cx->fp();
+    GlobalObject *scriptGlobal = fp->scopeChain().getGlobal();
+    jsbytecode *pc = cx->regs().pc;
+    BreakpointSite *site = cx->compartment->getBreakpointSite(pc);
+    JSOp op = site->realOpcode;
+
+    // Build list of breakpoint handlers.
+    Vector<Breakpoint *> triggered(cx);
+    for (Breakpoint *bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) {
+        if (!triggered.append(bp))
+            return JSTRAP_ERROR;
+    }
+
+    Value frame = UndefinedValue();
+    for (Breakpoint **p = triggered.begin(); p != triggered.end(); p++) {
+        Breakpoint *bp = *p;
+
+        // Handlers can clear breakpoints. Check that bp still exists.
+        if (!site || !site->hasBreakpoint(bp))
+            continue;
+
+        Debug *dbg = bp->debugger;
+        if (dbg->enabled && dbg->debuggees.lookup(scriptGlobal)) {
+            AutoCompartment ac(cx, dbg->object);
+            if (!ac.enter())
+                return JSTRAP_ERROR;
+
+            Value argv[1];
+            if (!dbg->getScriptFrame(cx, fp, &argv[0]))
+                return dbg->handleUncaughtException(ac, vp, false);
+            Value rv;
+            bool ok = CallMethodIfPresent(cx, bp->handler, "hit", 1, argv, &rv);
+            JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rv, vp, true);
+            if (st != JSTRAP_CONTINUE)
+                return st;
+
+            // Calling JS code invalidates site. Reload it.
+            site = cx->compartment->getBreakpointSite(pc);
+        }
+    }
+
+    if (site && site->trapHandler) {
+        JSTrapStatus st = site->trapHandler(cx, fp->script(), pc, Jsvalify(vp),
+                                            Jsvalify(site->trapClosure));
+        if (st != JSTRAP_CONTINUE)
+            return st;
+    }
+
+    // By convention, return the true op to the interpreter in vp.
+    vp->setInt32(op);
+    return JSTRAP_CONTINUE;
+}
+
 
 // === Debug JSObjects
 
 void
 Debug::markCrossCompartmentDebugObjectReferents(JSTracer *tracer)
 {
     JSCompartment *comp = tracer->context->runtime->gcCurrentCompartment;
 
@@ -535,18 +782,20 @@ Debug::markCrossCompartmentDebugObjectRe
             if (dbg->object->compartment() != comp) {
                 dbg->objects.markKeysInCompartment(tracer);
                 dbg->heldScripts.markKeysInCompartment(tracer);
             }
         }
     }
 }
 
-// Mark Debug objects that are unreachable except for debugger hooks that may
-// yet be called.
+// This method has two tasks:
+//   1. Mark Debug objects that are unreachable except for debugger hooks that
+//      may yet be called.
+//   2. Mark breakpoint handlers.
 //
 // This happens during the incremental long tail of the GC mark phase. This
 // method returns true if it has to mark anything; GC calls it repeatedly until
 // it returns false.
 //
 // If |comp| is non-null, we're collecting that compartment only; if |comp| is null,
 // we're collecting all compartments.
 //
@@ -556,16 +805,20 @@ Debug::mark(GCMarker *trc, JSCompartment
     bool markedAny = false;
 
     // We must find all Debug objects in danger of GC. This code is a little
     // convoluted since the easiest way to find them is via their debuggees.
     JSRuntime *rt = trc->context->runtime;
     for (JSCompartment **c = rt->compartments.begin(); c != rt->compartments.end(); c++) {
         JSCompartment *dc = *c;
 
+        // If dc is being collected, mark breakpoint handlers in it.
+        if (!comp || dc == comp)
+            markedAny = markedAny | dc->markBreakpointsIteratively(trc);
+
         // If this is a single-compartment GC, no compartment can debug itself, so skip
         // |comp|. If it's a global GC, then search every live compartment.
         if (comp ? dc != comp : !dc->isAboutToBeCollected(gckind)) {
             const GlobalObjectSet &debuggees = dc->getDebuggees();
             for (GlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
                 GlobalObject *global = r.front();
 
                 // Every debuggee has at least one debugger, so in this case
@@ -745,17 +998,35 @@ Debug::getEnabled(JSContext *cx, uintN a
     return true;
 }
 
 JSBool
 Debug::setEnabled(JSContext *cx, uintN argc, Value *vp)
 {
     REQUIRE_ARGC("Debug.set enabled", 1);
     THISOBJ(cx, vp, Debug, "set enabled", thisobj, dbg);
-    dbg->enabled = js_ValueToBoolean(vp[2]);
+    bool enabled = js_ValueToBoolean(vp[2]);
+
+    if (enabled != dbg->enabled) {
+        for (Breakpoint *bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
+            if (enabled) {
+                if (!bp->site->inc(cx)) {
+                    // Roll back the changes on error to keep the
+                    // BreakpointSite::enabledCount counters correct.
+                    for (Breakpoint *bp2 = dbg->firstBreakpoint(); bp2 != bp; bp2 = bp2->nextInDebugger())
+                        bp->site->dec(cx);
+                    return false;
+                }
+            } else {
+                bp->site->dec(cx);
+            }
+        }
+    }
+
+    dbg->enabled = enabled;
     vp->setUndefined();
     return true;
 }
 
 JSBool
 Debug::getUncaughtExceptionHook(JSContext *cx, uintN argc, Value *vp)
 {
     THISOBJ(cx, vp, Debug, "get uncaughtExceptionHook", thisobj, dbg);
@@ -877,16 +1148,25 @@ Debug::getYoungestFrame(JSContext *cx, u
         if (dbg->observesFrame(i.fp()))
             return dbg->getScriptFrame(cx, i.fp(), vp);
     }
     vp->setNull();
     return true;
 }
 
 JSBool
+Debug::clearAllBreakpoints(JSContext *cx, uintN argc, Value *vp)
+{
+    THISOBJ(cx, vp, Debug, "clearAllBreakpoints", thisobj, dbg);
+    for (GlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront())
+        r.front()->compartment()->clearBreakpointsIn(cx, dbg, NULL, NULL);
+    return true;
+}
+
+JSBool
 Debug::construct(JSContext *cx, uintN argc, Value *vp)
 {
     // Check that the arguments, if any, are cross-compartment wrappers.
     Value *argv = vp + 2, *argvEnd = argv + argc;
     for (Value *p = argv; p != argvEnd; p++) {
         const Value &arg = *p;
         if (!arg.isObject())
             return ReportObjectRequired(cx);
@@ -1059,16 +1339,17 @@ JSPropertySpec Debug::properties[] = {
 };
 
 JSFunctionSpec Debug::methods[] = {
     JS_FN("addDebuggee", Debug::addDebuggee, 1, 0),
     JS_FN("removeDebuggee", Debug::removeDebuggee, 1, 0),
     JS_FN("hasDebuggee", Debug::hasDebuggee, 1, 0),
     JS_FN("getDebuggees", Debug::getDebuggees, 0, 0),
     JS_FN("getYoungestFrame", Debug::getYoungestFrame, 0, 0),
+    JS_FN("clearAllBreakpoints", Debug::clearAllBreakpoints, 1, 0),
     JS_FS_END
 };
 
 
 // === Debug.Script
 
 // JSScripts' lifetimes fall into to two categories:
 //
@@ -1102,26 +1383,38 @@ JSFunctionSpec Debug::methods[] = {
 // is not traced; the holding object reference, if present, is traced via
 // DebugScript_trace.
 //
 // (We consider a script saved in and retrieved from the eval cache to have
 // been destroyed, and then --- mirabile dictu --- re-created at the same
 // address. The newScriptHook and destroyScriptHook hooks cooperate with this
 // view.)
 
-static inline JSScript *GetScriptReferent(JSObject *obj) {
+static inline JSScript *
+GetScriptReferent(JSObject *obj)
+{
     JS_ASSERT(obj->getClass() == &DebugScript_class);
     return (JSScript *) obj->getPrivate();
 }
 
-static inline void ClearScriptReferent(JSObject *obj) {
+static inline void
+ClearScriptReferent(JSObject *obj)
+{
     JS_ASSERT(obj->getClass() == &DebugScript_class);
     obj->setPrivate(NULL);
 }
 
+static inline JSObject *
+GetScriptHolder(JSObject *obj)
+{
+    JS_ASSERT(obj->getClass() == &DebugScript_class);
+    Value v = obj->getReservedSlot(JSSLOT_DEBUGSCRIPT_HOLDER);
+    return (JSObject *) v.toPrivate();
+}
+
 static void
 DebugScript_trace(JSTracer *trc, JSObject *obj)
 {
     if (!trc->context->runtime->gcCurrentCompartment) {
         Value v = obj->getReservedSlot(JSSLOT_DEBUGSCRIPT_HOLDER);
         if (!v.isUndefined()) {
             if (JSObject *obj = (JSObject *) v.toPrivate())
                 MarkObject(trc, *obj, "Debug.Script referent holder");
@@ -1222,46 +1515,52 @@ Debug::destroyEvalScript(JSScript *scrip
     if (p) {
         JS_ASSERT(GetScriptReferent(p->value) == script);
         ClearScriptReferent(p->value);
         evalScripts.remove(p);
     }
 }
 
 static JSObject *
-DebugScript_checkThis(JSContext *cx, Value *vp, const char *fnname, bool checkLive)
+DebugScript_check(JSContext *cx, const Value &v, const char *clsname, const char *fnname, bool checkLive)
 {
-    if (!vp[1].isObject()) {
+    if (!v.isObject()) {
         ReportObjectRequired(cx);
         return NULL;
     }
-    JSObject *thisobj = &vp[1].toObject();
+    JSObject *thisobj = &v.toObject();
     if (thisobj->clasp != &DebugScript_class) {
         JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO,
-                             "Debug.Script", fnname, thisobj->getClass()->name);
+                             clsname, fnname, thisobj->getClass()->name);
         return NULL;
     }
 
     // Check for Debug.Script.prototype, which is of class DebugScript_class
     // but whose holding object is undefined.
     if (thisobj->getReservedSlot(JSSLOT_DEBUGSCRIPT_HOLDER).isUndefined()) {
         JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO,
-                             "Debug.Script", fnname, "prototype object");
+                             clsname, fnname, "prototype object");
         return NULL;
     }
 
     if (checkLive && !GetScriptReferent(thisobj)) {
         JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NOT_LIVE,
-                             "Debug.Script", fnname, "script");
+                             clsname, fnname, "script");
         return NULL;
     }
 
     return thisobj;
 }
 
+static JSObject *
+DebugScript_checkThis(JSContext *cx, Value *vp, const char *fnname, bool checkLive)
+{
+    return DebugScript_check(cx, vp[1], "Debug.Script", fnname, checkLive);
+}
+
 #define THIS_DEBUGSCRIPT_SCRIPT_NEEDLIVE(cx, vp, fnname, obj, script, checkLive)    \
     JSObject *obj = DebugScript_checkThis(cx, vp, fnname, checkLive);               \
     if (!obj)                                                                       \
         return false;                                                               \
     JSScript *script = GetScriptReferent(obj)
 
 #define THIS_DEBUGSCRIPT_SCRIPT(cx, vp, fnname, obj, script)                  \
     THIS_DEBUGSCRIPT_SCRIPT_NEEDLIVE(cx, vp, fnname, obj, script, false)
@@ -1550,32 +1849,136 @@ DebugScript_getLineOffsets(JSContext *cx
                 return false;
         }
     }
 
     vp->setObject(*result);
     return true;
 }
 
+JSBool
+DebugScript_setBreakpoint(JSContext *cx, uintN argc, Value *vp)
+{
+    REQUIRE_ARGC("Debug.Script.setBreakpoint", 2);
+    THIS_DEBUGSCRIPT_LIVE_SCRIPT(cx, vp, "setBreakpoint", obj, script);
+    Debug *dbg = Debug::fromChildJSObject(obj);
+
+    JSObject *holder = GetScriptHolder(obj);
+    if (!dbg->observesScope(ScriptScope(cx, script, holder))) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NOT_DEBUGGING);
+        return false;
+    }
+
+    size_t offset;
+    if (!ScriptOffset(cx, script, vp[2], &offset))
+        return false;
+
+    JSObject *handler = NonNullObject(cx, vp[3]);
+    if (!handler)
+        return false;
+
+    JSCompartment *comp = script->compartment;
+    jsbytecode *pc = script->code + offset;
+    BreakpointSite *site = comp->getOrCreateBreakpointSite(cx, script, pc, holder);
+    if (!site->inc(cx))
+        goto fail1;
+    if (!cx->runtime->new_<Breakpoint>(dbg, site, handler))
+        goto fail2;
+    vp->setUndefined();
+    return true;
+
+fail2:
+    site->dec(cx);
+fail1:
+    site->destroyIfEmpty(cx->runtime, NULL);
+    return false;
+}
+
+JSBool
+DebugScript_getBreakpoints(JSContext *cx, uintN argc, Value *vp)
+{
+    THIS_DEBUGSCRIPT_LIVE_SCRIPT(cx, vp, "getBreakpoints", obj, script);
+    Debug *dbg = Debug::fromChildJSObject(obj);
+
+    jsbytecode *pc;
+    if (argc > 0) {
+        size_t offset;
+        if (!ScriptOffset(cx, script, vp[2], &offset))
+            return false;
+        pc = script->code + offset;
+    } else {
+        pc = NULL;
+    }
+
+    JSObject *arr = NewDenseEmptyArray(cx);
+    if (!arr)
+        return false;
+    JSCompartment *comp = script->compartment;
+    for (BreakpointSiteMap::Range r = comp->breakpointSites.all(); !r.empty(); r.popFront()) {
+        BreakpointSite *site = r.front().value;
+        if (site->script == script && (!pc || site->pc == pc)) {
+            for (Breakpoint *bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) {
+                if (bp->debugger == dbg &&
+                    !js_ArrayCompPush(cx, arr, ObjectValue(*bp->getHandler())))
+                {
+                    return false;
+                }
+            }
+        }
+    }
+    vp->setObject(*arr);
+    return true;
+}
+
+JSBool
+DebugScript_clearBreakpoint(JSContext *cx, uintN argc, Value *vp)
+{
+    REQUIRE_ARGC("Debug.Script.clearBreakpoint", 1);
+    THIS_DEBUGSCRIPT_LIVE_SCRIPT(cx, vp, "clearBreakpoint", obj, script);
+    Debug *dbg = Debug::fromChildJSObject(obj);
+
+    JSObject *handler = NonNullObject(cx, vp[2]);
+    if (!handler)
+        return false;
+
+    script->compartment->clearBreakpointsIn(cx, dbg, script, handler);
+    vp->setUndefined();
+    return true;
+}
+
+JSBool
+DebugScript_clearAllBreakpoints(JSContext *cx, uintN argc, Value *vp)
+{
+    THIS_DEBUGSCRIPT_LIVE_SCRIPT(cx, vp, "clearBreakpoint", obj, script);
+    Debug *dbg = Debug::fromChildJSObject(obj);
+    script->compartment->clearBreakpointsIn(cx, dbg, script, NULL);
+    vp->setUndefined();
+    return true;
+}
+
 static JSBool
 DebugScript_construct(JSContext *cx, uintN argc, Value *vp)
 {
     JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NO_CONSTRUCTOR, "Debug.Script");
     return false;
 }
 
 static JSPropertySpec DebugScript_properties[] = {
     JS_PSG("live", DebugScript_getLive, 0),
     JS_PS_END
 };
 
 static JSFunctionSpec DebugScript_methods[] = {
     JS_FN("getAllOffsets", DebugScript_getAllOffsets, 0, 0),
     JS_FN("getLineOffsets", DebugScript_getLineOffsets, 1, 0),
     JS_FN("getOffsetLine", DebugScript_getOffsetLine, 0, 0),
+    JS_FN("setBreakpoint", DebugScript_setBreakpoint, 2, 0),
+    JS_FN("getBreakpoints", DebugScript_getBreakpoints, 1, 0),
+    JS_FN("clearBreakpoint", DebugScript_clearBreakpoint, 1, 0),
+    JS_FN("clearAllBreakpoints", DebugScript_clearAllBreakpoints, 0, 0),
     JS_FS_END
 };
 
 
 // === Debug.Frame
 
 Class DebugFrame_class = {
     "Frame", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGFRAME_COUNT),
--- a/js/src/jsdbg.h
+++ b/js/src/jsdbg.h
@@ -38,42 +38,46 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef jsdbg_h__
 #define jsdbg_h__
 
 #include "jsapi.h"
+#include "jsclist.h"
 #include "jscompartment.h"
 #include "jsgc.h"
 #include "jshashtable.h"
 #include "jsweakmap.h"
 #include "jswrapper.h"
 #include "jsvalue.h"
 #include "vm/GlobalObject.h"
 
 namespace js {
 
 class Debug {
+    friend class js::Breakpoint;
     friend JSBool (::JS_DefineDebugObject)(JSContext *cx, JSObject *obj);
 
   private:
     JSCList link;                       // See JSRuntime::debuggerList.
     JSObject *object;                   // The Debug object. Strong reference.
     GlobalObjectSet debuggees;          // Debuggee globals. Cross-compartment weak references.
     JSObject *hooksObject;              // See Debug.prototype.hooks. Strong reference.
     JSObject *uncaughtExceptionHook;    // Strong reference.
     bool enabled;
 
     // True if hooksObject had a property of the respective name when the hooks
     // property was set.
     bool hasDebuggerHandler;            // hooks.debuggerHandler
     bool hasThrowHandler;               // hooks.throw
 
+    JSCList breakpoints;                // cyclic list of all js::Breakpoints in this debugger
+
     // Weak references to stack frames that are currently on the stack and thus
     // necessarily alive. We drop them as soon as they leave the stack (see
     // slowPathLeaveStackFrame) and in removeDebuggee.
     typedef HashMap<StackFrame *, JSObject *, DefaultHasher<StackFrame *>, SystemAllocPolicy>
         FrameMap;
     FrameMap frames;
 
     // The map from debuggee objects to their Debug.Object instances.
@@ -122,16 +126,17 @@ class Debug {
     static JSBool setEnabled(JSContext *cx, uintN argc, Value *vp);
     static JSBool getUncaughtExceptionHook(JSContext *cx, uintN argc, Value *vp);
     static JSBool setUncaughtExceptionHook(JSContext *cx, uintN argc, Value *vp);
     static JSBool addDebuggee(JSContext *cx, uintN argc, Value *vp);
     static JSBool removeDebuggee(JSContext *cx, uintN argc, Value *vp);
     static JSBool hasDebuggee(JSContext *cx, uintN argc, Value *vp);
     static JSBool getDebuggees(JSContext *cx, uintN argc, Value *vp);
     static JSBool getYoungestFrame(JSContext *cx, uintN argc, Value *vp);
+    static JSBool clearAllBreakpoints(JSContext *cx, uintN argc, Value *vp);
     static JSBool construct(JSContext *cx, uintN argc, Value *vp);
     static JSPropertySpec properties[];
     static JSFunctionSpec methods[];
 
     inline bool hasAnyLiveHooks() const;
 
     static void slowPathLeaveStackFrame(JSContext *cx);
     static void slowPathOnDestroyScript(JSScript *script);
@@ -154,16 +159,18 @@ class Debug {
     JSObject *newDebugScript(JSContext *cx, JSScript *script, JSObject *obj);
 
     // Helper function for wrapFunctionScript and wrapJSAPIscript.
     JSObject *wrapHeldScript(JSContext *cx, JSScript *script, JSObject *obj);
 
     // Remove script from our table of eval scripts.
     void destroyEvalScript(JSScript *script);
 
+    inline Breakpoint *firstBreakpoint() const;
+
   public:
     Debug(JSObject *dbg, JSObject *hooks);
     ~Debug();
 
     bool init(JSContext *cx);
     inline JSObject *toJSObject() const;
     static inline Debug *fromJSObject(JSObject *obj);
     static Debug *fromChildJSObject(JSObject *obj);
@@ -189,16 +196,17 @@ class Debug {
     static void sweepAll(JSContext *cx);
     static void detachAllDebuggersFromGlobal(JSContext *cx, GlobalObject *global,
                                              GlobalObjectSet::Enum *compartmentEnum);
 
     static inline void leaveStackFrame(JSContext *cx);
     static inline JSTrapStatus onDebuggerStatement(JSContext *cx, js::Value *vp);
     static inline JSTrapStatus onThrow(JSContext *cx, js::Value *vp);
     static inline void onDestroyScript(JSScript *script);
+    static JSTrapStatus onTrap(JSContext *cx, Value *vp);
 
     /**************************************** Functions for use by jsdbg.cpp. */
 
     inline bool observesScope(JSObject *obj) const;
     inline bool observesFrame(StackFrame *fp) const;
 
     // Precondition: *vp is a value from a debuggee compartment and cx is in
     // the debugger's compartment.
@@ -258,31 +266,97 @@ class Debug {
     // Prohibit copying.
     Debug(const Debug &);
     Debug & operator=(const Debug &);
 };
 
 bool
 Debug::hasAnyLiveHooks() const
 {
-    return enabled && (hasDebuggerHandler || hasThrowHandler);
+    return enabled && (hasDebuggerHandler || hasThrowHandler || !JS_CLIST_IS_EMPTY(&breakpoints));
 }
 
+class BreakpointSite {
+    friend class js::Breakpoint;
+    friend class ::JSCompartment;
+    friend class js::Debug;
+
+  public:
+    JSScript * const script;
+    jsbytecode * const pc;
+    const JSOp realOpcode;
+
+  private:
+    // The holder object for script, if known, else NULL.  This is NULL for
+    // eval scripts and for JSD1 traps. It is always non-null for JSD2
+    // breakpoints in non-eval scripts.
+    JSObject *scriptObject;
+
+    JSCList breakpoints;  // cyclic list of all js::Breakpoints at this instruction
+    size_t enabledCount;  // number of breakpoints in the list that are enabled
+    JSTrapHandler trapHandler;  // jsdbgapi trap state
+    Value trapClosure;
+
+    bool recompile(JSContext *cx, bool forTrap);
+
+  public:
+    BreakpointSite(JSScript *script, jsbytecode *pc);
+    Breakpoint *firstBreakpoint() const;
+    bool hasBreakpoint(Breakpoint *bp);
+
+    bool inc(JSContext *cx);
+    void dec(JSContext *cx);
+    bool setTrap(JSContext *cx, JSTrapHandler handler, const Value &closure);
+    void clearTrap(JSContext *cx, BreakpointSiteMap::Enum *e = NULL,
+                   JSTrapHandler *handlerp = NULL, Value *closurep = NULL);
+    void destroyIfEmpty(JSRuntime *rt, BreakpointSiteMap::Enum *e);
+};
+
+class Breakpoint {
+    friend class ::JSCompartment;
+    friend class js::Debug;
+
+  public:
+    Debug * const debugger;
+    BreakpointSite * const site;
+  private:
+    JSObject *handler;
+    JSCList debuggerLinks;
+    JSCList siteLinks;
+
+  public:
+    static Breakpoint *fromDebuggerLinks(JSCList *links);
+    static Breakpoint *fromSiteLinks(JSCList *links);
+    Breakpoint(Debug *debugger, BreakpointSite *site, JSObject *handler);
+    void destroy(JSContext *cx, BreakpointSiteMap::Enum *e = NULL);
+    Breakpoint *nextInDebugger();
+    Breakpoint *nextInSite();
+    JSObject *getHandler() const { return handler; }
+};
+
 bool
 Debug::observesScope(JSObject *obj) const
 {
     return debuggees.has(obj->getGlobal());
 }
 
 bool
 Debug::observesFrame(StackFrame *fp) const
 {
     return observesScope(&fp->scopeChain());
 }
 
+Breakpoint *
+Debug::firstBreakpoint() const
+{
+    if (JS_CLIST_IS_EMPTY(&breakpoints))
+        return NULL;
+    return Breakpoint::fromDebuggerLinks(JS_NEXT_LINK(&breakpoints));
+}
+
 JSObject *
 Debug::toJSObject() const
 {
     JS_ASSERT(object);
     return object;
 }
 
 Debug *
@@ -290,17 +364,17 @@ Debug::fromJSObject(JSObject *obj)
 {
     JS_ASSERT(obj->getClass() == &jsclass);
     return (Debug *) obj->getPrivate();
 }
 
 void
 Debug::leaveStackFrame(JSContext *cx)
 {
-    if (!cx->compartment->getDebuggees().empty())
+    if (!cx->compartment->getDebuggees().empty() || !cx->compartment->breakpointSites.empty())
         slowPathLeaveStackFrame(cx);
 }
 
 JSTrapStatus
 Debug::onDebuggerStatement(JSContext *cx, js::Value *vp)
 {
     return cx->compartment->getDebuggees().empty()
            ? JSTRAP_CONTINUE
--- a/js/src/jsdbgapi.cpp
+++ b/js/src/jsdbgapi.cpp
@@ -80,25 +80,16 @@
 #include "jsautooplen.h"
 
 #include "methodjit/MethodJIT.h"
 #include "methodjit/Retcon.h"
 
 using namespace js;
 using namespace js::gc;
 
-typedef struct JSTrap {
-    JSCList         links;
-    JSScript        *script;
-    jsbytecode      *pc;
-    JSOp            op;
-    JSTrapHandler   handler;
-    jsval           closure;
-} JSTrap;
-
 #define DBG_LOCK(rt)            JS_ACQUIRE_LOCK((rt)->debuggerLock)
 #define DBG_UNLOCK(rt)          JS_RELEASE_LOCK((rt)->debuggerLock)
 #define DBG_LOCK_EVAL(rt,expr)  (DBG_LOCK(rt), (expr), DBG_UNLOCK(rt))
 
 JS_PUBLIC_API(JSBool)
 JS_GetDebugMode(JSContext *cx)
 {
     return cx->compartment->debugMode();
@@ -211,288 +202,91 @@ JS_SetSingleStepMode(JSContext *cx, JSSc
 {
     assertSameCompartment(cx, script);
     if (!CheckDebugMode(cx))
         return JS_FALSE;
 
     return js_SetSingleStepMode(cx, script, singleStep);
 }
 
-/*
- * NB: FindTrap must be called with rt->debuggerLock acquired.
- */
-static JSTrap *
-FindTrap(JSRuntime *rt, JSScript *script, jsbytecode *pc)
-{
-    JSTrap *trap;
-
-    for (trap = (JSTrap *)rt->trapList.next;
-         &trap->links != &rt->trapList;
-         trap = (JSTrap *)trap->links.next) {
-        if (trap->script == script && trap->pc == pc)
-            return trap;
-    }
-    return NULL;
-}
-
 jsbytecode *
 js_UntrapScriptCode(JSContext *cx, JSScript *script)
 {
-    jsbytecode *code;
-    JSRuntime *rt;
-    JSTrap *trap;
+    jsbytecode *code = script->code;
 
-    code = script->code;
-    rt = cx->runtime;
-    DBG_LOCK(rt);
-    for (trap = (JSTrap *)rt->trapList.next;
-         &trap->links !=
-                &rt->trapList;
-         trap = (JSTrap *)trap->links.next) {
-        if (trap->script == script &&
-            (size_t)(trap->pc - script->code) < script->length) {
+    BreakpointSiteMap &sites = script->compartment->breakpointSites;
+    for (BreakpointSiteMap::Range r = sites.all(); !r.empty(); r.popFront()) {
+        BreakpointSite *site = r.front().value;
+        if (site->script == script && size_t(site->pc - script->code) < script->length) {
             if (code == script->code) {
-                jssrcnote *sn, *notes;
-                size_t nbytes;
-
-                nbytes = script->length * sizeof(jsbytecode);
-                notes = script->notes();
+                size_t nbytes = script->length * sizeof(jsbytecode);
+                jssrcnote *notes = script->notes();
+                jssrcnote *sn;
                 for (sn = notes; !SN_IS_TERMINATOR(sn); sn = SN_NEXT(sn))
                     continue;
                 nbytes += (sn - notes + 1) * sizeof *sn;
 
                 code = (jsbytecode *) cx->malloc_(nbytes);
                 if (!code)
                     break;
                 memcpy(code, script->code, nbytes);
                 GetGSNCache(cx)->purge();
             }
-            code[trap->pc - script->code] = trap->op;
+            code[site->pc - script->code] = site->realOpcode;
         }
     }
-    DBG_UNLOCK(rt);
     return code;
 }
 
 JS_PUBLIC_API(JSBool)
-JS_SetTrap(JSContext *cx, JSScript *script, jsbytecode *pc,
-           JSTrapHandler handler, jsval closure)
+JS_SetTrap(JSContext *cx, JSScript *script, jsbytecode *pc, JSTrapHandler handler, jsval closure)
 {
-    JSTrap *junk, *trap, *twin;
-    JSRuntime *rt;
-    uint32 sample;
-
+    assertSameCompartment(cx, script);
     if (!CheckDebugMode(cx))
-        return JS_FALSE;
+        return false;
 
-    JS_ASSERT((JSOp) *pc != JSOP_TRAP);
-    junk = NULL;
-    rt = cx->runtime;
-    DBG_LOCK(rt);
-    trap = FindTrap(rt, script, pc);
-    if (trap) {
-        JS_ASSERT(trap->script == script && trap->pc == pc);
-        JS_ASSERT(*pc == JSOP_TRAP);
-    } else {
-        sample = rt->debuggerMutations;
-        DBG_UNLOCK(rt);
-        trap = (JSTrap *) cx->malloc_(sizeof *trap);
-        if (!trap)
-            return JS_FALSE;
-        trap->closure = JSVAL_NULL;
-        DBG_LOCK(rt);
-        twin = (rt->debuggerMutations != sample)
-               ? FindTrap(rt, script, pc)
-               : NULL;
-        if (twin) {
-            junk = trap;
-            trap = twin;
-        } else {
-            JS_APPEND_LINK(&trap->links, &rt->trapList);
-            ++rt->debuggerMutations;
-            trap->script = script;
-            trap->pc = pc;
-            trap->op = (JSOp)*pc;
-            *pc = JSOP_TRAP;
-        }
-    }
-    trap->handler = handler;
-    trap->closure = closure;
-    DBG_UNLOCK(rt);
-    if (junk)
-        cx->free_(junk);
-
-#ifdef JS_METHODJIT
-    if (script->hasJITCode()) {
-        js::mjit::Recompiler recompiler(cx, script);
-        if (!recompiler.recompile())
-            return JS_FALSE;
-    }
-#endif
-
-    return JS_TRUE;
+    BreakpointSite *site = script->compartment->getOrCreateBreakpointSite(cx, script, pc, NULL);
+    if (!site)
+        return false;
+    site->setTrap(cx, handler, Valueify(closure));
+    return true;
 }
 
 JS_PUBLIC_API(JSOp)
 JS_GetTrapOpcode(JSContext *cx, JSScript *script, jsbytecode *pc)
 {
-    JSRuntime *rt;
-    JSTrap *trap;
-    JSOp op;
-
-    rt = cx->runtime;
-    DBG_LOCK(rt);
-    trap = FindTrap(rt, script, pc);
-    op = trap ? trap->op : (JSOp) *pc;
-    DBG_UNLOCK(rt);
-    return op;
-}
-
-static void
-DestroyTrapAndUnlock(JSContext *cx, JSTrap *trap)
-{
-    ++cx->runtime->debuggerMutations;
-    JS_REMOVE_LINK(&trap->links);
-    *trap->pc = (jsbytecode)trap->op;
-    DBG_UNLOCK(cx->runtime);
-    cx->free_(trap);
+    BreakpointSite *site = script->compartment->getBreakpointSite(pc);
+    return site ? site->realOpcode : JSOp(*pc);
 }
 
 JS_PUBLIC_API(void)
 JS_ClearTrap(JSContext *cx, JSScript *script, jsbytecode *pc,
              JSTrapHandler *handlerp, jsval *closurep)
 {
-    JSTrap *trap;
-    
-    DBG_LOCK(cx->runtime);
-    trap = FindTrap(cx->runtime, script, pc);
-    if (handlerp)
-        *handlerp = trap ? trap->handler : NULL;
-    if (closurep)
-        *closurep = trap ? trap->closure : JSVAL_NULL;
-    if (trap)
-        DestroyTrapAndUnlock(cx, trap);
-    else
-        DBG_UNLOCK(cx->runtime);
-
-#ifdef JS_METHODJIT
-    if (script->hasJITCode()) {
-        mjit::Recompiler recompiler(cx, script);
-        recompiler.recompile();
+    if (BreakpointSite *site = script->compartment->getBreakpointSite(pc)) {
+        site->clearTrap(cx, NULL, handlerp, Valueify(closurep));
+    } else {
+        if (handlerp)
+            *handlerp = NULL;
+        if (closurep)
+            *closurep = JSVAL_VOID;
     }
-#endif
 }
 
 JS_PUBLIC_API(void)
 JS_ClearScriptTraps(JSContext *cx, JSScript *script)
 {
-    JSRuntime *rt;
-    JSTrap *trap, *next;
-    uint32 sample;
-
-    rt = cx->runtime;
-    DBG_LOCK(rt);
-    for (trap = (JSTrap *)rt->trapList.next;
-         &trap->links != &rt->trapList;
-         trap = next) {
-        next = (JSTrap *)trap->links.next;
-        if (trap->script == script) {
-            sample = rt->debuggerMutations;
-            DestroyTrapAndUnlock(cx, trap);
-            DBG_LOCK(rt);
-            if (rt->debuggerMutations != sample + 1)
-                next = (JSTrap *)rt->trapList.next;
-        }
-    }
-    DBG_UNLOCK(rt);
+    assertSameCompartment(cx, script);
+    script->compartment->clearTraps(cx, script);
 }
 
 JS_PUBLIC_API(void)
-JS_ClearAllTraps(JSContext *cx)
-{
-    JSRuntime *rt;
-    JSTrap *trap, *next;
-    uint32 sample;
-
-    rt = cx->runtime;
-    DBG_LOCK(rt);
-    for (trap = (JSTrap *)rt->trapList.next;
-         &trap->links != &rt->trapList;
-         trap = next) {
-        next = (JSTrap *)trap->links.next;
-        sample = rt->debuggerMutations;
-        DestroyTrapAndUnlock(cx, trap);
-        DBG_LOCK(rt);
-        if (rt->debuggerMutations != sample + 1)
-            next = (JSTrap *)rt->trapList.next;
-    }
-    DBG_UNLOCK(rt);
-}
-
-/*
- * NB: js_MarkTraps does not acquire cx->runtime->debuggerLock, since the
- * debugger should never be racing with the GC (i.e., the debugger must
- * respect the request model).
- */
-void
-js_MarkTraps(JSTracer *trc)
-{
-    JSRuntime *rt = trc->context->runtime;
-
-    for (JSTrap *trap = (JSTrap *) rt->trapList.next;
-         &trap->links != &rt->trapList;
-         trap = (JSTrap *) trap->links.next) {
-        MarkValue(trc, Valueify(trap->closure), "trap->closure");
-    }
-}
-
-JS_PUBLIC_API(JSTrapStatus)
-JS_HandleTrap(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval)
+JS_ClearAllTrapsForCompartment(JSContext *cx)
 {
-    JSTrap *trap;
-    jsint op;
-    JSTrapStatus status;
-
-    assertSameCompartment(cx, script);
-    DBG_LOCK(cx->runtime);
-    trap = FindTrap(cx->runtime, script, pc);
-    JS_ASSERT(!trap || trap->handler);
-    if (!trap) {
-        op = (JSOp) *pc;
-        DBG_UNLOCK(cx->runtime);
-
-        /* Defend against "pc for wrong script" API usage error. */
-        JS_ASSERT(op != JSOP_TRAP);
-
-#ifdef JS_THREADSAFE
-        /* If the API was abused, we must fail for want of the real op. */
-        if (op == JSOP_TRAP)
-            return JSTRAP_ERROR;
-
-        /* Assume a race with a debugger thread and try to carry on. */
-        *rval = INT_TO_JSVAL(op);
-        return JSTRAP_CONTINUE;
-#else
-        /* Always fail if single-threaded (must be an API usage error). */
-        return JSTRAP_ERROR;
-#endif
-    }
-    DBG_UNLOCK(cx->runtime);
-
-    /*
-     * It's important that we not use 'trap->' after calling the callback --
-     * the callback might remove the trap!
-     */
-    op = (jsint)trap->op;
-    status = trap->handler(cx, script, pc, rval, trap->closure);
-    if (status == JSTRAP_CONTINUE) {
-        /* By convention, return the true op to the interpreter in rval. */
-        *rval = INT_TO_JSVAL(op);
-    }
-    return status;
+    cx->compartment->clearTraps(cx, NULL);
 }
 
 #ifdef JS_TRACER
 static void
 JITInhibitingHookChange(JSRuntime *rt, bool wasInhibited)
 {
     if (wasInhibited) {
         if (!rt->debuggerInhibitsJIT()) {
--- a/js/src/jsdbgapi.h
+++ b/js/src/jsdbgapi.h
@@ -147,20 +147,17 @@ JS_GetTrapOpcode(JSContext *cx, JSScript
 extern JS_PUBLIC_API(void)
 JS_ClearTrap(JSContext *cx, JSScript *script, jsbytecode *pc,
              JSTrapHandler *handlerp, jsval *closurep);
 
 extern JS_PUBLIC_API(void)
 JS_ClearScriptTraps(JSContext *cx, JSScript *script);
 
 extern JS_PUBLIC_API(void)
-JS_ClearAllTraps(JSContext *cx);
-
-extern JS_PUBLIC_API(JSTrapStatus)
-JS_HandleTrap(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval);
+JS_ClearAllTrapsForCompartment(JSContext *cx);
 
 extern JS_PUBLIC_API(JSBool)
 JS_SetInterrupt(JSRuntime *rt, JSInterruptHook handler, void *closure);
 
 extern JS_PUBLIC_API(JSBool)
 JS_ClearInterrupt(JSRuntime *rt, JSInterruptHook *handlerp, void **closurep);
 
 /************************************************************************/
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -1819,17 +1819,16 @@ MarkRuntime(JSTracer *trc)
 
     for (RootRange r = rt->gcRootsHash.all(); !r.empty(); r.popFront())
         gc_root_traversal(trc, r.front());
 
     for (GCLocks::Range r = rt->gcLocksHash.all(); !r.empty(); r.popFront())
         gc_lock_traversal(r.front(), trc);
 
     js_TraceAtomState(trc);
-    js_MarkTraps(trc);
 
     JSContext *iter = NULL;
     while (JSContext *acx = js_ContextIterator(rt, JS_TRUE, &iter))
         MarkContext(trc, acx);
 
 #ifdef JS_TRACER
     for (JSCompartment **c = rt->compartments.begin(); c != rt->compartments.end(); ++c)
         (*c)->traceMonitor.mark(trc);
@@ -2324,35 +2323,34 @@ MarkAndSweep(JSContext *cx, JSCompartmen
     /* Finalize unreachable (key,value) pairs in all weak maps. */
     WeakMapBase::sweepAll(&gcmarker);
 
     js_SweepAtomState(cx);
 
     /* Finalize watch points associated with unreachable objects. */
     js_SweepWatchPoints(cx);
 
-    if (!comp)
-        Debug::sweepAll(cx);
-
     /*
      * We finalize objects before other GC things to ensure that object's finalizer
      * can access them even if they will be freed. Sweep the runtime's property trees
      * after finalizing objects, in case any had watchpoints referencing tree nodes.
      * Do this before sweeping compartments, so that we sweep all shapes in
      * unreachable compartments.
      */
     if (comp) {
         comp->sweep(cx, 0);
         comp->finalizeObjectArenaLists(cx);
         GCTIMESTAMP(sweepObjectEnd);
         comp->finalizeStringArenaLists(cx);
         GCTIMESTAMP(sweepStringEnd);
         comp->finalizeShapeArenaLists(cx);
         GCTIMESTAMP(sweepShapeEnd);
     } else {
+        Debug::sweepAll(cx);
+
         SweepCrossCompartmentWrappers(cx);
         for (JSCompartment **c = rt->compartments.begin(); c != rt->compartments.end(); c++)
             (*c)->finalizeObjectArenaLists(cx);
 
         GCTIMESTAMP(sweepObjectEnd);
 
         for (JSCompartment **c = rt->compartments.begin(); c != rt->compartments.end(); c++)
             (*c)->finalizeStringArenaLists(cx);
--- a/js/src/jsgc.h
+++ b/js/src/jsgc.h
@@ -1263,23 +1263,16 @@ IterateCells(JSContext *cx, JSCompartmen
              void *data, IterateCallback callback);
 
 } /* namespace js */
 
 extern void
 js_FinalizeStringRT(JSRuntime *rt, JSString *str);
 
 /*
- * This function is defined in jsdbgapi.cpp but is declared here to avoid
- * polluting jsdbgapi.h, a public API header, with internal functions.
- */
-extern void
-js_MarkTraps(JSTracer *trc);
-
-/*
  * Macro to test if a traversal is the marking phase of the GC.
  */
 #define IS_GC_MARKING_TRACER(trc) ((trc)->callback == NULL)
 
 #if JS_HAS_XML_SUPPORT
 # define JS_IS_VALID_TRACE_KIND(kind) ((uint32)(kind) < JSTRACE_LIMIT)
 #else
 # define JS_IS_VALID_TRACE_KIND(kind) ((uint32)(kind) <= JSTRACE_SHAPE)
--- a/js/src/jsinterp.cpp
+++ b/js/src/jsinterp.cpp
@@ -4991,17 +4991,17 @@ BEGIN_CASE(JSOP_LOOKUPSWITCH)
           : GET_JUMPX_OFFSET(pc2);
 }
 END_VARLEN_CASE
 }
 
 BEGIN_CASE(JSOP_TRAP)
 {
     Value rval;
-    JSTrapStatus status = JS_HandleTrap(cx, script, regs.pc, Jsvalify(&rval));
+    JSTrapStatus status = Debug::onTrap(cx, &rval);
     switch (status) {
       case JSTRAP_ERROR:
         goto error;
       case JSTRAP_RETURN:
         regs.fp()->setReturnValue(rval);
         interpReturnOK = JS_TRUE;
         goto forced_return;
       case JSTRAP_THROW:
--- a/js/src/jsprvtd.h
+++ b/js/src/jsprvtd.h
@@ -154,16 +154,17 @@ class CallArgs;
 struct Compiler;
 struct Parser;
 class TokenStream;
 struct Token;
 struct TokenPos;
 struct TokenPtr;
 
 class ContextAllocPolicy;
+class RuntimeAllocPolicy;
 class SystemAllocPolicy;
 
 template <class T,
           size_t MinInlineCapacity = 0,
           class AllocPolicy = ContextAllocPolicy>
 class Vector;
 
 template <class>
@@ -182,18 +183,23 @@ class HashSet;
 
 class PropertyCache;
 struct PropertyCacheEntry;
 
 struct Shape;
 struct EmptyShape;
 class Bindings;
 
+class Breakpoint;
+class BreakpointSite;
 class Debug;
 
+typedef HashMap<jsbytecode *, BreakpointSite *, DefaultHasher<jsbytecode *>, RuntimeAllocPolicy>
+    BreakpointSiteMap;
+
 } /* namespace js */
 
 } /* export "C++" */
 
 #else
 
 typedef struct JSAtom JSAtom;
 
--- a/js/src/methodjit/StubCalls.cpp
+++ b/js/src/methodjit/StubCalls.cpp
@@ -1200,17 +1200,17 @@ stubs::Trap(VMFrame &f, uint32 trapTypes
          */
         JSInterruptHook hook = f.cx->debugHooks->interruptHook;
         if (hook)
             result = hook(f.cx, f.cx->fp()->script(), pc, Jsvalify(&rval),
                           f.cx->debugHooks->interruptHookData);
     }
 
     if (result == JSTRAP_CONTINUE && (trapTypes & JSTRAP_TRAP))
-        result = JS_HandleTrap(f.cx, f.cx->fp()->script(), pc, Jsvalify(&rval));
+        result = Debug::onTrap(f.cx, &rval);
 
     switch (result) {
       case JSTRAP_THROW:
         f.cx->setPendingException(rval);
         THROW();
 
       case JSTRAP_RETURN:
         f.cx->clearPendingException();
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -1903,19 +1903,20 @@ SetDebug(JSContext *cx, uintN argc, jsva
 
     /*
      * Debug mode can only be set when there is no JS code executing on the
      * stack. Unfortunately, that currently means that this call will fail
      * unless debug mode is already set to what you're trying to set it to.
      * In the future, this restriction may be lifted.
      */
 
-    JSBool rv = JS_SetDebugMode(cx, JSVAL_TO_BOOLEAN(argv[0]));
-    JS_SET_RVAL(cx, vp, rv ? JSVAL_TRUE : JSVAL_FALSE);
-    return JS_TRUE;
+    JSBool ok = JS_SetDebugMode(cx, JSVAL_TO_BOOLEAN(argv[0]));
+    if (ok)
+        JS_SET_RVAL(cx, vp, JSVAL_TRUE);
+    return ok;
 }
 
 static JSBool
 GetTrapArgs(JSContext *cx, uintN argc, jsval *argv, JSScript **scriptp,
             int32 *ip)
 {
     jsval v;
     uintN intarg;
--- a/js/src/tests/ecma_3/extensions/regress-429248.js
+++ b/js/src/tests/ecma_3/extensions/regress-429248.js
@@ -49,19 +49,18 @@ test();
 function test()
 {
   enterFunc ('test');
   printBugNumber(BUGNUMBER);
   printStatus (summary);
  
   function c() { do{}while(0) }
 
-  if (typeof trap == 'function' && typeof setDebug == 'function')
+  if (typeof trap == 'function')
   {
-    setDebug(true);
     trap(c, 0, "");
   }
   c + '';
 
   reportCompare(expect, actual, summary);
 
   exitFunc ('test');
 }
--- a/js/src/tests/js1_5/extensions/regress-422137.js
+++ b/js/src/tests/js1_5/extensions/regress-422137.js
@@ -49,19 +49,18 @@ test();
 function test()
 {
   enterFunc ('test');
   printBugNumber(BUGNUMBER);
   printStatus (summary);
  
   function f() { return a(); }
 
-  if (typeof trap == 'function' && typeof setDebug == 'function')
+  if (typeof trap == 'function')
   {
-    setDebug(true);
     trap(f, 0, "print('trap')");
   }
   f + '';
 
   reportCompare(expect, actual, summary);
 
   exitFunc ('test');
 }