Very rudimentary support for creating Debug.Frame objects, passing them to hooks, and cleaning them up afterwards.

This commit is contained in:
Jason Orendorff 2011-04-27 18:22:28 -05:00
parent 4fcb55d0aa
commit 27da170dc0
11 changed files with 335 additions and 24 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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<WithProto::Given>(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<WithProto::Given>(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_<Debug>(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;
}

View File

@ -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<JSStackFrame *, JSObject *, DefaultHasher<JSStackFrame *>, 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
{

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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
{