From 27da170dc098674402ccccd5f6f9e95efed5d91f Mon Sep 17 00:00:00 2001 From: Jason Orendorff Date: Wed, 27 Apr 2011 18:22:28 -0500 Subject: [PATCH] Very rudimentary support for creating Debug.Frame objects, passing them to hooks, and cleaning them up afterwards. --- .../jit-test/tests/debug/debug-object-20.js | 2 +- .../jit-test/tests/debug/debug-object-22.js | 51 +++++ .../jit-test/tests/debug/debug-object-23.js | 35 ++++ .../jit-test/tests/debug/debug-object-24.js | 26 +++ js/src/jsdbg.cpp | 179 ++++++++++++++++-- js/src/jsdbg.h | 34 +++- js/src/jsdbgapi.cpp | 2 + js/src/jsgc.cpp | 5 + js/src/jsobj.cpp | 10 +- js/src/jsobj.h | 9 +- js/src/jsobjinlines.h | 6 + 11 files changed, 335 insertions(+), 24 deletions(-) create mode 100644 js/src/jit-test/tests/debug/debug-object-22.js create mode 100644 js/src/jit-test/tests/debug/debug-object-23.js create mode 100644 js/src/jit-test/tests/debug/debug-object-24.js diff --git a/js/src/jit-test/tests/debug/debug-object-20.js b/js/src/jit-test/tests/debug/debug-object-20.js index 534f737757f..f2fbda68ab4 100644 --- a/js/src/jit-test/tests/debug/debug-object-20.js +++ b/js/src/jit-test/tests/debug/debug-object-20.js @@ -8,7 +8,7 @@ var log; function makeDebug(g, name) { var dbg = new Debug(g); dbg.hooks = { - debuggerHandler: function () { + debuggerHandler: function (frame) { log += name; throw new Error(name); } diff --git a/js/src/jit-test/tests/debug/debug-object-22.js b/js/src/jit-test/tests/debug/debug-object-22.js new file mode 100644 index 00000000000..a2095b43c70 --- /dev/null +++ b/js/src/jit-test/tests/debug/debug-object-22.js @@ -0,0 +1,51 @@ +// |jit-test| debug +// Test .type and .generator fields of topmost stack frame passed to debuggerHandler. + +var g = newGlobal('new-compartment'); +g.debuggeeGlobal = this; +g.eval("var hits;"); +g.eval("(" + function () { + var dbg = Debug(debuggeeGlobal); + dbg.hooks = { + debuggerHandler: function (f) { + assertEq(Object.getPrototypeOf(f), Debug.Frame.prototype); + assertEq(f.type, ftype); + assertEq(f.generator, fgen); + hits++; + } + }; + } + ")()"); + +g.ftype = "global"; +g.fgen = false; +g.hits = 0; +debugger; +assertEq(g.hits, 1); + +g.ftype = "call"; +g.hits = 0; +(function () { debugger; })(); +assertEq(g.hits, 1); + +g.ftype = "eval"; +g.hits = 0; +eval("debugger;"); +assertEq(g.hits, 1); + +g.ftype = "eval"; +g.hits = 0; +this.eval("debugger;"); // indirect eval +assertEq(g.hits, 1); + +g.ftype = "eval"; +g.hits = 0; +(function () { eval("debugger;"); })(); +assertEq(g.hits, 1); + +g.ftype = "call"; +g.fgen = true; +g.hits = 0; +function gen() { debugger; yield 1; debugger; } +for (var x in gen()) { +} +assertEq(g.hits, 2); diff --git a/js/src/jit-test/tests/debug/debug-object-23.js b/js/src/jit-test/tests/debug/debug-object-23.js new file mode 100644 index 00000000000..0ebad76a26e --- /dev/null +++ b/js/src/jit-test/tests/debug/debug-object-23.js @@ -0,0 +1,35 @@ +// |jit-test| debug +// When the debugger is triggered twice from the same stack frame, the same +// Debug.Frame object must be passed to the hook both times. + +var g = newGlobal('new-compartment'); +g.debuggeeGlobal = this; +g.eval("var hits, frame;"); +g.eval("(" + function () { + var dbg = Debug(debuggeeGlobal); + dbg.hooks = { + debuggerHandler: function (f) { + if (hits++ == 0) + frame = f; + else + assertEq(f, frame); + } + }; + } + ")()"); + +g.hits = 0; +debugger; +debugger; +assertEq(g.hits, 2); + +g.hits = 0; +function f() { + debugger; + debugger; +} +f(); +assertEq(g.hits, 2); + +g.hits = 0; +eval("debugger; debugger;"); +assertEq(g.hits, 2); diff --git a/js/src/jit-test/tests/debug/debug-object-24.js b/js/src/jit-test/tests/debug/debug-object-24.js new file mode 100644 index 00000000000..55ef8d86e78 --- /dev/null +++ b/js/src/jit-test/tests/debug/debug-object-24.js @@ -0,0 +1,26 @@ +// |jit-test| debug +// When the debugger is triggered from different stack frames that happen to +// occupy the same memory, it must deliver different Debug.Frame objects. + +var g = newGlobal('new-compartment'); +g.debuggeeGlobal = this; +g.eval("var hits;"); +g.eval("(" + function () { + var a = []; + var dbg = Debug(debuggeeGlobal); + dbg.hooks = { + debuggerHandler: function (frame) { + for (var i = 0; i < a.length; i++) + assertEq(a[i] === frame, false); + a.push(frame); + hits++; + } + }; + } + ")()"); + +function f() { debugger; } +function h() { debugger; f(); } +g.hits = 0; +for (var i = 0; i < 4; i++) + h(); +assertEq(g.hits, 8); diff --git a/js/src/jsdbg.cpp b/js/src/jsdbg.cpp index ad3b7b507d9..02f4363b699 100644 --- a/js/src/jsdbg.cpp +++ b/js/src/jsdbg.cpp @@ -49,6 +49,17 @@ using namespace js; +// === Forward declarations + +extern Class Frame_class; + +enum { + JSSLOT_FRAME_OWNER, + JSSLOT_FRAME_COUNT +}; + +// === Utils + static bool NotImplemented(JSContext *cx) { @@ -114,12 +125,59 @@ CheckThisClass(JSContext *cx, Value *vp, Class *clasp, const char *fnname) // === Debug hook dispatch +enum { + JSSLOT_DEBUG_FRAME_PROTO, + JSSLOT_DEBUG_COUNT +}; + Debug::Debug(JSObject *dbg, JSObject *hooks, JSCompartment *compartment) : object(dbg), debuggeeCompartment(compartment), hooksObject(hooks), uncaughtExceptionHook(NULL), enabled(true), hasDebuggerHandler(false) { } +bool +Debug::init() +{ + return frames.init(); +} + +bool +Debug::getScriptFrame(JSContext *cx, JSStackFrame *fp, Value *vp) +{ + FrameMap::AddPtr p = frames.lookupForAdd(fp); + if (!p) { + JSObject *proto = &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject(); + JSObject *frameobj = NewNonFunction(cx, &Frame_class, proto, NULL); + if (!frameobj || !frameobj->ensureClassReservedSlots(cx)) + return false; + frameobj->setPrivate(fp); + frameobj->setReservedSlot(JSSLOT_FRAME_OWNER, ObjectValue(*object)); + if (!frames.add(p, fp, frameobj)) { + js_ReportOutOfMemory(cx); + return false; + } + } + vp->setObject(*p->value); + return true; +} + +void +Debug::slowPathLeaveStackFrame(JSContext *cx) +{ + JSStackFrame *fp = cx->regs->fp; + JSCompartment *compartment = cx->compartment; + const JSCompartment::DebugVector &debuggers = compartment->getDebuggers(); + for (Debug **p = debuggers.begin(); p != debuggers.end(); p++) { + Debug *dbg = *p; + if (FrameMap::Ptr p = dbg->frames.lookup(fp)) { + JSObject *frameobj = p->value; + frameobj->setPrivate(NULL); + dbg->frames.remove(p); + } + } +} + JSTrapStatus Debug::handleUncaughtException(AutoCompartment &ac, Value *vp, bool callHook) { @@ -133,7 +191,7 @@ Debug::handleUncaughtException(AutoCompartment &ac, Value *vp, bool callHook) if (ExternalInvoke(cx, ObjectValue(*object), fval, 1, &exc, &rv)) return parseResumptionValue(ac, true, rv, vp, false); } - + if (cx->isExceptionPending()) { JS_ReportPendingException(cx); cx->clearPendingException(); @@ -209,14 +267,20 @@ CallMethodIfPresent(JSContext *cx, JSObject *obj, const char *name, int argc, Va JSTrapStatus Debug::handleDebuggerStatement(JSContext *cx, Value *vp) { + // Grab cx->regs->fp before pushing a dummy frame. + JSStackFrame *fp = cx->regs->fp; + JS_ASSERT(hasDebuggerHandler); AutoCompartment ac(cx, hooksObject); if (!ac.enter()) return JSTRAP_ERROR; - // XXX debuggerHandler should receive a Frame. + Value argv[1]; + if (!getScriptFrame(cx, fp, argv)) + return JSTRAP_ERROR; + Value rv; - bool ok = CallMethodIfPresent(cx, hooksObject, "debuggerHandler", 0, NULL, &rv); + bool ok = CallMethodIfPresent(cx, hooksObject, "debuggerHandler", 1, argv, &rv); return parseResumptionValue(ac, ok, rv, vp); } @@ -252,7 +316,6 @@ Debug::dispatchDebuggerStatement(JSContext *cx, js::Value *vp) return JSTRAP_CONTINUE; } - // === Debug JSObjects bool @@ -299,6 +362,34 @@ Debug::trace(JSTracer *trc, JSObject *obj) MarkObject(trc, *dbg->hooksObject, "hooks"); if (dbg->uncaughtExceptionHook) MarkObject(trc, *dbg->uncaughtExceptionHook, "hooks"); + + // Mark Debug.Frame objects that are reachable from JS if we look them up + // again (because the corresponding JSStackFrame is still on the stack). + for (FrameMap::Enum e(dbg->frames); !e.empty(); e.popFront()) { + if (e.front().value->getPrivate()) + MarkObject(trc, *obj, "live Debug.Frame"); + } + } +} + +void +Debug::sweepAll(JSRuntime *rt) +{ + for (JSCompartment **c = rt->compartments.begin(); c != rt->compartments.end(); c++) + sweepCompartment(*c); +} + +void +Debug::sweepCompartment(JSCompartment *compartment) +{ + // Sweep FrameMap entries for objects being collected. + const JSCompartment::DebugVector &debuggers = compartment->getDebuggers(); + for (Debug **p = debuggers.begin(); p != debuggers.end(); p++) { + Debug *dbg = *p; + for (FrameMap::Enum e(dbg->frames); !e.empty(); e.popFront()) { + if (!e.front().value->isMarked()) + e.removeFront(); + } } } @@ -319,7 +410,7 @@ Debug::detachFrom(JSCompartment *c) } Class Debug::jsclass = { - "Debug", JSCLASS_HAS_PRIVATE, + "Debug", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUG_COUNT), PropertyStub, PropertyStub, PropertyStub, StrictPropertyStub, EnumerateStub, ResolveStub, ConvertStub, Debug::finalize, NULL, /* reserved0 */ @@ -432,17 +523,20 @@ Debug::construct(JSContext *cx, uintN argc, Value *vp) JSObject *proto = &v.toObject(); JS_ASSERT(proto->getClass() == &Debug::jsclass); - // Make the new Debug object. + // Make the new Debug object. Each one has a reference to + // Debug.Frame.prototype in a reserved slot. JSObject *obj = NewNonFunction(cx, &Debug::jsclass, proto, NULL); - if (!obj) + if (!obj || !obj->ensureClassReservedSlots(cx)) return false; + obj->setReservedSlot(JSSLOT_DEBUG_FRAME_PROTO, + proto->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO)); JSObject *hooks = NewBuiltinClassInstance(cx, &js_ObjectClass); if (!hooks) return false; Debug *dbg = cx->new_(obj, hooks, debuggeeCompartment); if (!dbg) return false; - if (!debuggeeCompartment->addDebug(dbg)) { + if (!dbg->init() || !debuggeeCompartment->addDebug(dbg)) { js_ReportOutOfMemory(cx); return false; } @@ -460,14 +554,77 @@ JSPropertySpec Debug::properties[] = { JS_PS_END }; +// === Debug.Frame + +Class Frame_class = { + "Frame", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_FRAME_COUNT), + PropertyStub, PropertyStub, PropertyStub, StrictPropertyStub, + EnumerateStub, ResolveStub, ConvertStub, FinalizeStub, +}; + +#define THIS_FRAME(cx, vp, fnname, thisobj, fp) \ + JSObject *thisobj = CheckThisClass(cx, vp, &Frame_class, fnname); \ + if (!thisobj) \ + return false; \ + JSStackFrame *fp = (JSStackFrame *) thisobj->getPrivate() + +JSBool +Frame_getType(JSContext *cx, uintN argc, Value *vp) +{ + THIS_FRAME(cx, vp, "get type", thisobj, fp); + + // Indirect eval frames are both isGlobalFrame() and isEvalFrame(), so the + // order of checks here is significant. + vp->setString(fp->isEvalFrame() + ? cx->runtime->atomState.evalAtom + : fp->isGlobalFrame() + ? cx->runtime->atomState.globalAtom + : cx->runtime->atomState.callAtom); + return true; +} + +JSBool +Frame_getGenerator(JSContext *cx, uintN argc, Value *vp) +{ + THIS_FRAME(cx, vp, "get generator", thisobj, fp); + vp->setBoolean(fp->isGeneratorFrame()); + return true; +} + +JSBool +Frame_construct(JSContext *cx, uintN argc, Value *vp) +{ + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NO_CONSTRUCTOR, "Debug.Frame"); + return false; +} + +JSPropertySpec Frame_properties[] = { + JS_PSG("type", Frame_getType, 0), + JS_PSG("generator", Frame_getGenerator, 0), + JS_PS_END +}; + +// === Glue extern JS_PUBLIC_API(JSBool) JS_DefineDebugObject(JSContext *cx, JSObject *obj) { JSObject *objProto; if (!js_GetClassPrototype(cx, obj, JSProto_Object, &objProto)) - return NULL; + return false; - return !!js_InitClass(cx, obj, objProto, &Debug::jsclass, Debug::construct, 1, - Debug::properties, NULL, NULL, NULL); + JSObject *debugCtor; + JSObject *debugProto = js_InitClass(cx, obj, objProto, &Debug::jsclass, Debug::construct, 1, + Debug::properties, NULL, NULL, NULL, &debugCtor); + if (!debugProto || !debugProto->ensureClassReservedSlots(cx)) + return false; + + JSObject *frameCtor; + JSObject *frameProto = js_InitClass(cx, debugCtor, objProto, &Frame_class, Frame_construct, 0, + Frame_properties, NULL, NULL, NULL, &frameCtor); + if (!frameProto) + return false; + debugProto->setReservedSlot(JSSLOT_DEBUG_FRAME_PROTO, ObjectValue(*frameProto)); + + return true; } diff --git a/js/src/jsdbg.h b/js/src/jsdbg.h index 97d61ebfc46..635a61b6d18 100644 --- a/js/src/jsdbg.h +++ b/js/src/jsdbg.h @@ -45,6 +45,7 @@ #include "jsapi.h" #include "jscompartment.h" #include "jsgc.h" +#include "jshashtable.h" #include "jswrapper.h" #include "jsvalue.h" @@ -64,6 +65,10 @@ class Debug { // property was set. bool hasDebuggerHandler; + typedef HashMap, SystemAllocPolicy> + FrameMap; + FrameMap frames; + JSTrapStatus handleUncaughtException(AutoCompartment &ac, Value *vp, bool callHook); JSTrapStatus parseResumptionValue(AutoCompartment &ac, bool ok, const Value &rv, Value *vp, bool callHook = true); @@ -83,14 +88,22 @@ class Debug { inline bool hasAnyLiveHooks() const; + bool getScriptFrame(JSContext *cx, JSStackFrame *fp, Value *vp); + static void slowPathLeaveStackFrame(JSContext *cx); + inline bool observesDebuggerStatement() const; static JSTrapStatus dispatchDebuggerStatement(JSContext *cx, Value *vp); JSTrapStatus handleDebuggerStatement(JSContext *cx, Value *vp); public: Debug(JSObject *dbg, JSObject *hooks, JSCompartment *compartment); + bool init(); + inline JSObject *toJSObject() const; + static inline Debug *fromJSObject(JSObject *obj); - // Mark some Debug objects. A Debug object is live if: + // Methods for interaction with the GC. + // + // A Debug object is live if: // * the Debug JSObject is live (Debug::trace handles this case); OR // * it is in the middle of dispatching an event (the event dispatching // code roots it in this case); OR @@ -100,18 +113,18 @@ class Debug { // - it has a breakpoint set on a live script // - it has a watchpoint set on a live object. // - // The last case is handled by this method. If it finds any Debug objects - // that are definitely live but not yet marked, it marks them and returns - // true. If not, it returns false. + // The last case is handled by the mark() method. If it finds any Debug + // objects that are definitely live but not yet marked, it marks them and + // returns true. If not, it returns false. // static bool mark(GCMarker *trc, JSCompartment *compartment, JSGCInvocationKind gckind); - - inline JSObject *toJSObject() const; - static inline Debug *fromJSObject(JSObject *obj); + static void sweepAll(JSRuntime *rt); + static void sweepCompartment(JSCompartment *compartment); inline bool observesCompartment(JSCompartment *c) const; void detachFrom(JSCompartment *c); + static inline void leaveStackFrame(JSContext *cx); static inline JSTrapStatus onDebuggerStatement(JSContext *cx, js::Value *vp); }; @@ -142,6 +155,13 @@ Debug::fromJSObject(JSObject *obj) return (Debug *) obj->getPrivate(); } +void +Debug::leaveStackFrame(JSContext *cx) +{ + if (!cx->compartment->getDebuggers().empty()) + slowPathLeaveStackFrame(cx); +} + bool Debug::observesDebuggerStatement() const { diff --git a/js/src/jsdbgapi.cpp b/js/src/jsdbgapi.cpp index f4c92027e80..6bbc27c1700 100644 --- a/js/src/jsdbgapi.cpp +++ b/js/src/jsdbgapi.cpp @@ -133,6 +133,7 @@ ScriptDebugPrologue(JSContext *cx, JSStackFrame *fp) bool ScriptDebugEpilogue(JSContext *cx, JSStackFrame *fp, bool okArg) { + JS_ASSERT(fp == cx->fp()); JSBool ok = okArg; Probes::exitJSFun(cx, fp->maybeFun(), fp->script()); @@ -146,6 +147,7 @@ ScriptDebugEpilogue(JSContext *cx, JSStackFrame *fp, bool okArg) hook(cx, fp, false, &ok, hookData); } } + Debug::leaveStackFrame(cx); return ok; } diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index d3726452363..3f478456601 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -2527,6 +2527,11 @@ MarkAndSweep(JSContext *cx, JSCompartment *comp, JSGCInvocationKind gckind GCTIM /* Finalize watch points associated with unreachable objects. */ js_SweepWatchPoints(cx); + if (comp) + Debug::sweepCompartment(comp); + else + Debug::sweepAll(rt); + /* * 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 diff --git a/js/src/jsobj.cpp b/js/src/jsobj.cpp index 91ad0ee2a74..fc13ba6bd05 100644 --- a/js/src/jsobj.cpp +++ b/js/src/jsobj.cpp @@ -3827,7 +3827,8 @@ DefineConstructorAndPrototype(JSContext *cx, JSObject *obj, JSProtoKey key, JSAt JSObject *protoProto, Class *clasp, Native constructor, uintN nargs, JSPropertySpec *ps, JSFunctionSpec *fs, - JSPropertySpec *static_ps, JSFunctionSpec *static_fs) + JSPropertySpec *static_ps, JSFunctionSpec *static_fs, + JSObject **ctorp) { /* * Create a prototype object for this class. @@ -3966,6 +3967,8 @@ DefineConstructorAndPrototype(JSContext *cx, JSObject *obj, JSProtoKey key, JSAt if (key != JSProto_Null && !js_SetClassObject(cx, obj, key, ctor, proto)) goto bad; + if (ctorp) + *ctorp = ctor; return proto; bad: @@ -3982,7 +3985,8 @@ JSObject * js_InitClass(JSContext *cx, JSObject *obj, JSObject *protoProto, Class *clasp, Native constructor, uintN nargs, JSPropertySpec *ps, JSFunctionSpec *fs, - JSPropertySpec *static_ps, JSFunctionSpec *static_fs) + JSPropertySpec *static_ps, JSFunctionSpec *static_fs, + JSObject **ctorp) { JSAtom *atom = js_Atomize(cx, clasp->name, strlen(clasp->name), 0); if (!atom) @@ -4008,7 +4012,7 @@ js_InitClass(JSContext *cx, JSObject *obj, JSObject *protoProto, } return DefineConstructorAndPrototype(cx, obj, key, atom, protoProto, clasp, constructor, nargs, - ps, fs, static_ps, static_fs); + ps, fs, static_ps, static_fs, ctorp); } bool diff --git a/js/src/jsobj.h b/js/src/jsobj.h index 72cc047eb9e..952c56cd5d0 100644 --- a/js/src/jsobj.h +++ b/js/src/jsobj.h @@ -663,6 +663,9 @@ struct JSObject : js::gc::Cell { inline js::Value getReservedSlot(uintN index) const; + /* Call this only after the appropriate ensure{Class,Instance}ReservedSlots call. */ + inline void setReservedSlot(uintN index, const js::Value &v); + /* Defined in jsscopeinlines.h to avoid including implementation dependencies here. */ inline void updateShape(JSContext *cx); inline void updateFlags(const js::Shape *shape, bool isDefinitelyAtom = false); @@ -1548,14 +1551,16 @@ DefineConstructorAndPrototype(JSContext *cx, JSObject *obj, JSProtoKey key, JSAt JSObject *protoProto, Class *clasp, Native constructor, uintN nargs, JSPropertySpec *ps, JSFunctionSpec *fs, - JSPropertySpec *static_ps, JSFunctionSpec *static_fs); + JSPropertySpec *static_ps, JSFunctionSpec *static_fs, + JSObject **ctorp = NULL); } extern JSObject * js_InitClass(JSContext *cx, JSObject *obj, JSObject *parent_proto, js::Class *clasp, js::Native constructor, uintN nargs, JSPropertySpec *ps, JSFunctionSpec *fs, - JSPropertySpec *static_ps, JSFunctionSpec *static_fs); + JSPropertySpec *static_ps, JSFunctionSpec *static_fs, + JSObject **ctorp = NULL); /* * Select Object.prototype method names shared between jsapi.cpp and jsobj.cpp. diff --git a/js/src/jsobjinlines.h b/js/src/jsobjinlines.h index 7b70d47cf58..e0c5f005108 100644 --- a/js/src/jsobjinlines.h +++ b/js/src/jsobjinlines.h @@ -298,6 +298,12 @@ JSObject::getReservedSlot(uintN index) const return (index < numSlots()) ? getSlot(index) : js::UndefinedValue(); } +inline void +JSObject::setReservedSlot(uintN index, const js::Value &v) +{ + setSlot(index, v); +} + inline bool JSObject::canHaveMethodBarrier() const {