Bug 1083210 - Part 1: Add a Debugger.prototype.onNewPromise hook. r=shu

This commit is contained in:
Nick Fitzgerald 2014-11-17 10:42:00 +01:00
parent 323ef5978b
commit 019d6491e6
13 changed files with 327 additions and 13 deletions

View File

@ -263,6 +263,23 @@ class BuilderOrigin : public Builder {
// malloc'd blocks.
void SetDebuggerMallocSizeOf(JSRuntime *runtime, mozilla::MallocSizeOf mallocSizeOf);
// Handlers for observing Promises
// -------------------------------
//
// The Debugger wants to observe behavior of promises, which are implemented by
// Gecko with webidl and which SpiderMonkey knows nothing about. On the other
// hand, Gecko knows nothing about which (if any) debuggers are observing a
// promise's global. The compromise is that Gecko is responsible for calling
// these handlers at the appropriate times, and SpiderMonkey will handle
// notifying any Debugger instances that are observing the given promise's
// global.
// Notify any Debugger instances observing this promise's global that a new
// promise was allocated.
JS_PUBLIC_API(void)
onNewPromise(JSContext *cx, HandleObject promise);
} // namespace dbg
} // namespace JS

View File

@ -21,6 +21,7 @@
#include "asmjs/AsmJSLink.h"
#include "asmjs/AsmJSValidate.h"
#include "js/Debug.h"
#include "js/HashTable.h"
#include "js/StructuredClone.h"
#include "js/UbiNode.h"
@ -956,6 +957,34 @@ OOMAfterAllocations(JSContext *cx, unsigned argc, jsval *vp)
}
#endif
static const JSClass FakePromiseClass = {
"Promise", JSCLASS_IS_ANONYMOUS,
JS_PropertyStub, /* addProperty */
JS_DeletePropertyStub, /* delProperty */
JS_PropertyStub, /* getProperty */
JS_StrictPropertyStub, /* setProperty */
JS_EnumerateStub,
JS_ResolveStub,
JS_ConvertStub
};
static bool
MakeFakePromise(JSContext *cx, unsigned argc, jsval *vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
RootedObject scope(cx, cx->global());
if (!scope)
return false;
RootedObject obj(cx, JS_NewObjectWithGivenProto(cx, &FakePromiseClass, JS::NullPtr(), scope));
if (!obj)
return false;
JS::dbg::onNewPromise(cx, obj);
args.rval().setObject(*obj);
return true;
}
static unsigned finalizeCount = 0;
static void
@ -2235,6 +2264,12 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = {
" (return NULL)."),
#endif
JS_FN_HELP("makeFakePromise", MakeFakePromise, 0, 0,
"makeFakePromise()",
" Create an object whose [[Class]] name is 'Promise' and call\n"
" JS::dbg::onNewPromise on it before returning it. It doesn't actually have\n"
" any of the other behavior associated with promises."),
JS_FN_HELP("makeFinalizeObserver", MakeFinalizeObserver, 0, 0,
"makeFinalizeObserver()",
" Get a special object whose finalization increases the counter returned\n"

View File

@ -81,12 +81,20 @@ compartment.
<code>onNewScript(<i>script</i>, <i>global</i>)</code>
: New code, represented by the [`Debugger.Script`][script] instance
<i>script</i>, has been loaded in the scope of the debuggee global
object <i>global</i>. <i>global</i> is a [`Debugger.Object`][object]
instance whose referent is the global object.
<i>script</i>, has been loaded in the scope of the debuggees.
This method's return value is ignored.
<code>onNewPromise(<i>promise</i>)</code>
: A new Promise object, referenced by the [`Debugger.Object`][object] instance
*promise*, has been allocated in the scope of the debuggees.
This handler method should return a [resumption value][rv] specifying how
the debuggee's execution should proceed. However, note that a <code>{ return:
<i>value</i> }</code> resumption value is treated like `undefined` ("continue
normally"); <i>value</i> is ignored. (Allowing the handler to substitute
its own value for the new global object doesn't seem useful.)
<code>onDebuggerStatement(<i>frame</i>)</code>
: Debuggee code has executed a <i>debugger</i> statement in <i>frame</i>.
This method should return a [resumption value][rv] specifying how the

View File

@ -0,0 +1,17 @@
// Test that the onNewPromise hook gets called when promises are allocated in
// the scope of debuggee globals.
var g = newGlobal();
var dbg = new Debugger();
var gw = dbg.addDebuggee(g);
let promisesFound = [];
dbg.onNewPromise = p => { promisesFound.push(p); };
let p1 = g.makeFakePromise()
dbg.enabled = false;
let p2 = g.makeFakePromise();
assertEq(promisesFound.indexOf(gw.makeDebuggeeValue(p1)) != -1, true);
assertEq(promisesFound.indexOf(gw.makeDebuggeeValue(p2)) == -1, true);

View File

@ -0,0 +1,24 @@
// onNewPromise handlers fire, until they are removed.
var g = newGlobal();
var dbg = new Debugger(g);
var log;
log = '';
g.makeFakePromise();
assertEq(log, '');
dbg.onNewPromise = function (promise) {
log += 'n';
assertEq(promise.seen, undefined);
promise.seen = true;
};
log = '';
g.makeFakePromise();
assertEq(log, 'n');
log = '';
dbg.onNewPromise = undefined;
g.makeFakePromise();
assertEq(log, '');

View File

@ -0,0 +1,41 @@
// onNewPromise handlers on different Debugger instances are independent.
var g = newGlobal();
var dbg1 = new Debugger(g);
var log1;
function h1(promise) {
log1 += 'n';
assertEq(promise.seen, undefined);
promise.seen = true;
}
var dbg2 = new Debugger(g);
var log2;
function h2(promise) {
log2 += 'n';
assertEq(promise.seen, undefined);
promise.seen = true;
}
log1 = log2 = '';
g.makeFakePromise();
assertEq(log1, '');
assertEq(log2, '');
log1 = log2 = '';
dbg1.onNewPromise = h1;
g.makeFakePromise();
assertEq(log1, 'n');
assertEq(log2, '');
log1 = log2 = '';
dbg2.onNewPromise = h2;
g.makeFakePromise();
assertEq(log1, 'n');
assertEq(log2, 'n');
log1 = log2 = '';
dbg1.onNewPromise = undefined;
g.makeFakePromise();
assertEq(log1, '');
assertEq(log2, 'n');

View File

@ -0,0 +1,15 @@
// An onNewPromise handler can disable itself.
var g = newGlobal();
var dbg = new Debugger(g);
var log;
dbg.onNewPromise = function (promise) {
log += 'n';
dbg.onNewPromise = undefined;
};
log = '';
g.makeFakePromise();
g.makeFakePromise();
assertEq(log, 'n');

View File

@ -0,0 +1,24 @@
// Creating a promise within an onNewPromise handler causes a recursive handler
// invocation.
var g = newGlobal();
var dbg = new Debugger(g);
var log;
var depth;
dbg.onNewPromise = function (promise) {
log += '('; depth++;
assertEq(promise.seen, undefined);
promise.seen = true;
if (depth < 3)
g.makeFakePromise();
log += ')'; depth--;
};
log = '';
depth = 0;
g.makeFakePromise();
assertEq(log, '((()))');

View File

@ -0,0 +1,35 @@
// Resumption values from onNewPromise handlers are disallowed.
load(libdir + 'asserts.js');
var g = newGlobal();
var dbg = new Debugger(g);
var log;
dbg.onNewPromise = function (g) { log += 'n'; return undefined; };
log = '';
assertEq(typeof g.makeFakePromise(), "object");
assertEq(log, 'n');
dbg.uncaughtExceptionHook = function (ex) { assertEq(/disallowed/.test(ex), true); log += 'u'; }
dbg.onNewPromise = function (g) { log += 'n'; return { return: "snoo" }; };
log = '';
assertEq(typeof g.makeFakePromise(), "object");
assertEq(log, 'nu');
dbg.onNewPromise = function (g) { log += 'n'; return { throw: "snoo" }; };
log = '';
assertEq(typeof g.makeFakePromise(), "object");
assertEq(log, 'nu');
dbg.onNewPromise = function (g) { log += 'n'; return null; };
log = '';
assertEq(typeof g.makeFakePromise(), "object");
assertEq(log, 'nu');
dbg.uncaughtExceptionHook = function (ex) { assertEq(/foopy/.test(ex), true); log += 'u'; }
dbg.onNewPromise = function (g) { log += 'n'; throw "foopy"; };
log = '';
assertEq(typeof g.makeFakePromise(), "object");
assertEq(log, 'nu');

View File

@ -0,0 +1,13 @@
// Errors in onNewPromise handlers are reported correctly, and don't mess up the
// promise creation.
var g = newGlobal();
var dbg = new Debugger(g);
let e;
dbg.uncaughtExceptionHook = ee => { e = ee; };
dbg.onNewPromise = () => { throw new Error("woops!"); };
assertEq(typeof g.makeFakePromise(), "object");
assertEq(!!e, true);
assertEq(!!e.message.match(/woops/), true);

View File

@ -46,7 +46,7 @@ js::Debugger::onDebuggerStatement(JSContext *cx, AbstractFramePtr frame, Mutable
{
MOZ_ASSERT_IF(frame.script()->isDebuggee(), frame.isDebuggee());
return frame.isDebuggee()
? dispatchHook(cx, vp, OnDebuggerStatement)
? dispatchHook(cx, vp, OnDebuggerStatement, NullPtr())
: JSTRAP_CONTINUE;
}

View File

@ -20,7 +20,6 @@
#include "gc/Marking.h"
#include "jit/BaselineDebugModeOSR.h"
#include "jit/BaselineJIT.h"
#include "js/Debug.h"
#include "js/GCAPI.h"
#include "js/UbiNodeTraverse.h"
#include "js/Vector.h"
@ -691,7 +690,7 @@ JSTrapStatus
Debugger::slowPathOnExceptionUnwind(JSContext *cx, AbstractFramePtr frame)
{
RootedValue rval(cx);
JSTrapStatus status = dispatchHook(cx, &rval, OnExceptionUnwind);
JSTrapStatus status = dispatchHook(cx, &rval, OnExceptionUnwind, NullPtr());
switch (status) {
case JSTRAP_CONTINUE:
@ -1197,9 +1196,11 @@ Debugger::fireNewScript(JSContext *cx, HandleScript script)
}
/* static */ JSTrapStatus
Debugger::dispatchHook(JSContext *cx, MutableHandleValue vp, Hook which)
Debugger::dispatchHook(JSContext *cx, MutableHandleValue vp, Hook which, HandleObject payload)
{
MOZ_ASSERT(which == OnDebuggerStatement || which == OnExceptionUnwind);
MOZ_ASSERT(which == OnDebuggerStatement ||
which == OnExceptionUnwind ||
which == OnNewPromise);
/*
* Determine which debuggers will receive this event, and in what order.
@ -1228,9 +1229,20 @@ Debugger::dispatchHook(JSContext *cx, MutableHandleValue vp, Hook which)
for (Value *p = triggered.begin(); p != triggered.end(); p++) {
Debugger *dbg = Debugger::fromJSObject(&p->toObject());
if (dbg->debuggees.has(global) && dbg->enabled && dbg->getHook(which)) {
JSTrapStatus st = (which == OnDebuggerStatement)
? dbg->fireDebuggerStatement(cx, vp)
: dbg->fireExceptionUnwind(cx, vp);
JSTrapStatus st;
switch (which) {
case OnDebuggerStatement:
st = dbg->fireDebuggerStatement(cx, vp);
break;
case OnExceptionUnwind:
st = dbg->fireExceptionUnwind(cx, vp);
break;
case OnNewPromise:
st = dbg->fireNewPromise(cx, payload, vp);
break;
default:
MOZ_ASSERT_UNREACHABLE("Unexpected debugger hook");
}
if (st != JSTRAP_CONTINUE)
return st;
}
@ -1586,6 +1598,47 @@ Debugger::emptyAllocationsLog()
allocationsLogLength = 0;
}
/* static */ void
Debugger::slowPathOnNewPromise(JSContext *cx, HandleObject promise)
{
RootedValue rval(cx);
JSTrapStatus status = dispatchHook(cx, &rval, OnNewPromise, promise);
MOZ_ASSERT(status == JSTRAP_CONTINUE);
MOZ_ASSERT(!cx->isExceptionPending());
}
JSTrapStatus
Debugger::fireNewPromise(JSContext *cx, HandleObject promise, MutableHandleValue vp)
{
RootedObject hook(cx, getHook(OnNewPromise));
MOZ_ASSERT(hook);
MOZ_ASSERT(hook->isCallable());
Maybe<AutoCompartment> ac;
ac.emplace(cx, object);
RootedValue dbgObj(cx, ObjectValue(*promise));
if (!wrapDebuggeeValue(cx, &dbgObj))
return handleUncaughtException(ac, false);
// Like onNewGlobalObject, onNewPromise is infallible and the comments in
// |Debugger::fireNewGlobalObject| apply here as well.
RootedValue rv(cx);
bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, dbgObj.address(), &rv);
if (ok && !rv.isUndefined()) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr,
JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED);
ok = false;
}
JSTrapStatus status = ok ? JSTRAP_CONTINUE
: handleUncaughtException(ac, vp, true);
MOZ_ASSERT(!cx->isExceptionPending());
return status;
}
/*** Debugger code invalidation for observing execution ******************************************/
@ -1979,6 +2032,7 @@ Debugger::setObservesAllExecution(JSContext *cx, IsObserving observing)
return updateExecutionObservability(cx, obs, observing);
}
/*** Debugger JSObjects **************************************************************************/
@ -2430,6 +2484,18 @@ Debugger::setOnNewScript(JSContext *cx, unsigned argc, Value *vp)
return setHookImpl(cx, argc, vp, OnNewScript);
}
/* static */ bool
Debugger::getOnNewPromise(JSContext *cx, unsigned argc, Value *vp)
{
return getHookImpl(cx, argc, vp, OnNewPromise);
}
/* static */ bool
Debugger::setOnNewPromise(JSContext *cx, unsigned argc, Value *vp)
{
return setHookImpl(cx, argc, vp, OnNewPromise);
}
/* static */ bool
Debugger::getOnEnterFrame(JSContext *cx, unsigned argc, Value *vp)
{
@ -3662,6 +3728,7 @@ const JSPropertySpec Debugger::properties[] = {
JS_PSGS("onExceptionUnwind", Debugger::getOnExceptionUnwind,
Debugger::setOnExceptionUnwind, 0),
JS_PSGS("onNewScript", Debugger::getOnNewScript, Debugger::setOnNewScript, 0),
JS_PSGS("onNewPromise", Debugger::getOnNewPromise, Debugger::setOnNewPromise, 0),
JS_PSGS("onEnterFrame", Debugger::getOnEnterFrame, Debugger::setOnEnterFrame, 0),
JS_PSGS("onNewGlobalObject", Debugger::getOnNewGlobalObject, Debugger::setOnNewGlobalObject, 0),
JS_PSGS("uncaughtExceptionHook", Debugger::getUncaughtExceptionHook,
@ -7116,3 +7183,13 @@ JS_DefineDebuggerObject(JSContext *cx, HandleObject obj)
debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_PROTO, ObjectValue(*memoryProto));
return true;
}
JS_PUBLIC_API(void)
JS::dbg::onNewPromise(JSContext *cx, HandleObject promise)
{
MOZ_ASSERT(!!promise);
assertSameCompartment(cx, promise);
MOZ_ASSERT(strcmp(promise->getClass()->name, "Promise") == 0 ||
strcmp(promise->getClass()->name, "MozAbortablePromise") == 0);
Debugger::slowPathOnNewPromise(cx, promise);
}

View File

@ -17,6 +17,7 @@
#include "jswrapper.h"
#include "gc/Barrier.h"
#include "js/Debug.h"
#include "js/HashTable.h"
#include "vm/GlobalObject.h"
#include "vm/SavedStacks.h"
@ -177,6 +178,7 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
friend class mozilla::LinkedListElement<Debugger>;
friend bool (::JS_DefineDebuggerObject)(JSContext *cx, JS::HandleObject obj);
friend bool SavedStacksMetadataCallback(JSContext *cx, JSObject **pmetadata);
friend void JS::dbg::onNewPromise(JSContext *cx, HandleObject promise);
public:
enum Hook {
@ -185,6 +187,7 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
OnNewScript,
OnEnterFrame,
OnNewGlobalObject,
OnNewPromise,
HookCount
};
enum {
@ -369,6 +372,8 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
static bool setOnEnterFrame(JSContext *cx, unsigned argc, Value *vp);
static bool getOnNewGlobalObject(JSContext *cx, unsigned argc, Value *vp);
static bool setOnNewGlobalObject(JSContext *cx, unsigned argc, Value *vp);
static bool getOnNewPromise(JSContext *cx, unsigned argc, Value *vp);
static bool setOnNewPromise(JSContext *cx, unsigned argc, Value *vp);
static bool getUncaughtExceptionHook(JSContext *cx, unsigned argc, Value *vp);
static bool setUncaughtExceptionHook(JSContext *cx, unsigned argc, Value *vp);
static bool getMemory(JSContext *cx, unsigned argc, Value *vp);
@ -420,12 +425,15 @@ class Debugger : private mozilla::LinkedListElement<Debugger>
static void slowPathOnNewGlobalObject(JSContext *cx, Handle<GlobalObject *> global);
static bool slowPathOnLogAllocationSite(JSContext *cx, HandleSavedFrame frame,
int64_t when, GlobalObject::DebuggerVector &dbgs);
static JSTrapStatus dispatchHook(JSContext *cx, MutableHandleValue vp, Hook which);
static void slowPathOnNewPromise(JSContext *cx, HandleObject promise);
static JSTrapStatus dispatchHook(JSContext *cx, MutableHandleValue vp, Hook which,
HandleObject payload);
JSTrapStatus fireDebuggerStatement(JSContext *cx, MutableHandleValue vp);
JSTrapStatus fireExceptionUnwind(JSContext *cx, MutableHandleValue vp);
JSTrapStatus fireEnterFrame(JSContext *cx, AbstractFramePtr frame, MutableHandleValue vp);
JSTrapStatus fireNewGlobalObject(JSContext *cx, Handle<GlobalObject *> global, MutableHandleValue vp);
JSTrapStatus fireNewPromise(JSContext *cx, HandleObject promise, MutableHandleValue vp);
/*
* Allocate and initialize a Debugger.Script instance whose referent is
@ -826,7 +834,7 @@ Debugger::observesGlobal(GlobalObject *global) const
return debuggees.has(global);
}
void
/* static */ void
Debugger::onNewScript(JSContext *cx, HandleScript script, GlobalObject *compileAndGoGlobal)
{
MOZ_ASSERT_IF(script->compileAndGo(), compileAndGoGlobal);