Add breakpoints.
authorJason Orendorff <jorendorff@mozilla.com>
Tue, 28 Jun 2011 16:06:34 -0500
changeset 74490 45f1cf2c59d200bc5e5db35001eed68d9a466a13
parent 74489 2cc9d8a133bc33a0202ec8dcdcd1b85b3df5eb9d
child 74491 63ee1fe5025c99e88e20847e1e533d2af9117cb8
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
milestone7.0a1
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');
 }