Bug 911216 - Part 24: Use promise rejection tracking to report unhandled rejections to the console. r=bz,Paolo

This commit is contained in:
Till Schneidereit 2016-03-22 16:22:24 +01:00
parent 0f6f35176e
commit b0d2250794
8 changed files with 191 additions and 12 deletions

View File

@ -940,6 +940,35 @@ Promise::MaybeRejectWithNull()
MaybeSomething(JS::NullHandleValue, &Promise::MaybeReject);
}
#ifdef SPIDERMONKEY_PROMISE
void
Promise::ReportRejectedPromise(JSContext* aCx, JS::HandleObject aPromise)
{
MOZ_ASSERT(!js::IsWrapper(aPromise));
MOZ_ASSERT(JS::GetPromiseState(aPromise) == JS::PromiseState::Rejected);
JS::Rooted<JS::Value> result(aCx, JS::GetPromiseResult(aPromise));
js::ErrorReport report(aCx);
if (!report.init(aCx, result, js::ErrorReport::NoSideEffects)) {
JS_ClearPendingException(aCx);
return;
}
RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
bool isMainThread = MOZ_LIKELY(NS_IsMainThread());
bool isChrome = isMainThread ? nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(aPromise))
: GetCurrentThreadWorkerPrivate()->IsChromeWorker();
nsGlobalWindow* win = isMainThread ? xpc::WindowGlobalOrNull(aPromise) : nullptr;
xpcReport->Init(report.report(), report.message(), isChrome, win ? win->AsInner()->WindowID() : 0);
// Now post an event to do the real reporting async
NS_DispatchToMainThread(new AsyncErrorReporter(xpcReport));
}
#endif // defined(SPIDERMONKEY_PROMISE)
bool
Promise::PerformMicroTaskCheckpoint()
{

View File

@ -26,7 +26,7 @@
// that need to be removed once clients have been put together
// to take advantage of the new mechanism. New code should not
// depend on code #ifdefed to this #define.
#define DOM_PROMISE_DEPRECATED_REPORTING 1
#define DOM_PROMISE_DEPRECATED_REPORTING !SPIDERMONKEY_PROMISE
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
#include "mozilla/dom/workers/bindings/WorkerFeature.h"
@ -112,6 +112,9 @@ public:
#ifdef SPIDERMONKEY_PROMISE
static already_AddRefed<Promise>
Create(nsIGlobalObject* aGlobal, ErrorResult& aRv);
// Reports a rejected Promise by sending an error report.
static void ReportRejectedPromise(JSContext* aCx, JS::HandleObject aPromise);
#else
static already_AddRefed<Promise>
Create(nsIGlobalObject* aGlobal, ErrorResult& aRv,

View File

@ -230,11 +230,8 @@ PromiseDebugging::Shutdown()
/* static */ void
PromiseDebugging::FlushUncaughtRejections()
{
// XXXbz figure out the plan
#ifndef SPIDERMONKEY_PROMISE
MOZ_ASSERT(!NS_IsMainThread());
FlushRejections::FlushSync();
#endif // SPIDERMONKEY_PROMISE
}
#ifndef SPIDERMONKEY_PROMISE
@ -342,7 +339,90 @@ PromiseDebugging::RemoveUncaughtRejectionObserver(GlobalObject&,
return false;
}
#ifndef SPIDERMONKEY_PROMISE
#ifdef SPIDERMONKEY_PROMISE
/* static */ void
PromiseDebugging::AddUncaughtRejection(JS::HandleObject aPromise)
{
// This might OOM, but won't set a pending exception, so we'll just ignore it.
if (CycleCollectedJSRuntime::Get()->mUncaughtRejections.append(aPromise)) {
FlushRejections::DispatchNeeded();
}
}
/* void */ void
PromiseDebugging::AddConsumedRejection(JS::HandleObject aPromise)
{
// If the promise is in our list of uncaught rejections, we haven't yet
// reported it as unhandled. In that case, just remove it from the list
// and don't add it to the list of consumed rejections.
auto& uncaughtRejections = CycleCollectedJSRuntime::Get()->mUncaughtRejections;
for (size_t i = 0; i < uncaughtRejections.length(); i++) {
if (uncaughtRejections[i] == aPromise) {
// To avoid large amounts of memmoves, we don't shrink the vector here.
// Instead, we filter out nullptrs when iterating over the vector later.
uncaughtRejections[i].set(nullptr);
return;
}
}
// This might OOM, but won't set a pending exception, so we'll just ignore it.
if (CycleCollectedJSRuntime::Get()->mConsumedRejections.append(aPromise)) {
FlushRejections::DispatchNeeded();
}
}
/* static */ void
PromiseDebugging::FlushUncaughtRejectionsInternal()
{
CycleCollectedJSRuntime* storage = CycleCollectedJSRuntime::Get();
auto& uncaught = storage->mUncaughtRejections;
auto& consumed = storage->mConsumedRejections;
AutoJSAPI jsapi;
jsapi.Init();
JSContext* cx = jsapi.cx();
// Notify observers of uncaught Promise.
auto& observers = storage->mUncaughtRejectionObservers;
for (size_t i = 0; i < uncaught.length(); i++) {
JS::RootedObject promise(cx, uncaught[i]);
// Filter out nullptrs which might've been added by
// PromiseDebugging::AddConsumedRejection.
if (!promise) {
continue;
}
for (size_t j = 0; j < observers.Length(); ++j) {
RefPtr<UncaughtRejectionObserver> obs =
static_cast<UncaughtRejectionObserver*>(observers[j].get());
IgnoredErrorResult err;
obs->OnLeftUncaught(promise, err);
}
JSAutoCompartment ac(cx, promise);
Promise::ReportRejectedPromise(cx, promise);
}
storage->mUncaughtRejections.clear();
// Notify observers of consumed Promise.
for (size_t i = 0; i < consumed.length(); i++) {
JS::RootedObject promise(cx, consumed[i]);
for (size_t j = 0; j < observers.Length(); ++j) {
RefPtr<UncaughtRejectionObserver> obs =
static_cast<UncaughtRejectionObserver*>(observers[j].get());
IgnoredErrorResult err;
obs->OnConsumed(promise, err);
}
}
storage->mConsumedRejections.clear();
}
#else
/* static */ void
PromiseDebugging::AddUncaughtRejection(Promise& aPromise)
@ -372,13 +452,10 @@ PromiseDebugging::GetPromiseID(GlobalObject&,
aID = sIDPrefix;
aID.AppendInt(promiseID);
}
#endif // SPIDERMONKEY_PROMISE
/* static */ void
PromiseDebugging::FlushUncaughtRejectionsInternal()
{
// XXXbz talk to till about replacement for this stuff.
#ifndef SPIDERMONKEY_PROMISE
CycleCollectedJSRuntime* storage = CycleCollectedJSRuntime::Get();
// The Promise that have been left uncaught (rejected and last in
@ -439,8 +516,8 @@ PromiseDebugging::FlushUncaughtRejectionsInternal()
obs->OnConsumed(*promise, err); // Ignore errors
}
}
#endif // SPIDERMONKEY_PROMISE
}
#endif // SPIDERMONKEY_PROMISE
} // namespace dom
} // namespace mozilla

View File

@ -26,6 +26,8 @@ class GlobalObject;
class UncaughtRejectionObserver;
class FlushRejections;
void TriggerFlushRejections();
class PromiseDebugging
{
public:
@ -63,13 +65,18 @@ public:
#endif // SPIDERMONKEY_PROMISE
// Mechanism for watching uncaught instances of Promise.
// XXXbz figure out the plan
static void AddUncaughtRejectionObserver(GlobalObject&,
UncaughtRejectionObserver& aObserver);
static bool RemoveUncaughtRejectionObserver(GlobalObject&,
UncaughtRejectionObserver& aObserver);
#ifndef SPIDERMONKEY_PROMISE
#ifdef SPIDERMONKEY_PROMISE
// Mark a Promise as having been left uncaught at script completion.
static void AddUncaughtRejection(JS::HandleObject);
// Mark a Promise previously added with `AddUncaughtRejection` as
// eventually consumed.
static void AddConsumedRejection(JS::HandleObject);
#else
// Mark a Promise as having been left uncaught at script completion.
static void AddUncaughtRejection(Promise&);
// Mark a Promise previously added with `AddUncaughtRejection` as
@ -78,7 +85,6 @@ public:
#endif // SPIDERMONKEY_PROMISE
// Propagate the informations from AddUncaughtRejection
// and AddConsumedRejection to observers.
// XXXbz figure out the plan.
static void FlushUncaughtRejections();
protected:

View File

@ -38,7 +38,11 @@ callback interface UncaughtRejectionObserver {
* caught, i.e. if its `then` callback is called, `onConsumed` will
* be called.
*/
#ifdef SPIDERMONKEY_PROMISE
void onLeftUncaught(object p);
#else
void onLeftUncaught(Promise<any> p);
#endif SPIDERMONKEY_PROMISE
/**
* A Promise previously left uncaught is not the last in its
@ -47,7 +51,11 @@ callback interface UncaughtRejectionObserver {
* @param p A Promise that was previously left in uncaught state is
* now caught, i.e. it is not the last in its chain anymore.
*/
#ifdef SPIDERMONKEY_PROMISE
void onConsumed(object p);
#else
void onConsumed(Promise<any> p);
#endif SPIDERMONKEY_PROMISE
};
[ChromeOnly, Exposed=(Window,System)]

View File

@ -857,6 +857,9 @@ add_test({
self.writeManifest(["test_simple_uncaught_rejection.js"])
self.assertTestResult(False)
self.assertInLog(TEST_FAIL_STRING)
self.assertInLog("test_simple_uncaught_rejection.js:3:3")
self.assertInLog("Test rejection.")
self.assertEquals(1, self.x.testCount)
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)
@ -869,6 +872,9 @@ add_test({
self.writeManifest(["test_simple_uncaught_rejection_jsm.js"])
self.assertTestResult(False)
self.assertInLog(TEST_FAIL_STRING)
self.assertInLog("test_simple_uncaught_rejection_jsm.js:4:16")
self.assertInLog("Test rejection.")
self.assertEquals(1, self.x.testCount)
self.assertEquals(0, self.x.passCount)
self.assertEquals(1, self.x.failCount)

View File

@ -69,6 +69,7 @@
#include "mozilla/dom/ProfileTimelineMarkerBinding.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseBinding.h"
#include "mozilla/dom/PromiseDebugging.h"
#include "mozilla/dom/ScriptSettings.h"
#include "jsprf.h"
#include "js/Debug.h"
@ -471,6 +472,11 @@ CycleCollectedJSRuntime::~CycleCollectedJSRuntime()
MOZ_ASSERT(mDebuggerPromiseMicroTaskQueue.empty());
MOZ_ASSERT(mPromiseMicroTaskQueue.empty());
#ifdef SPIDERMONKEY_PROMISE
mUncaughtRejections.reset();
mConsumedRejections.reset();
#endif // SPIDERMONKEY_PROMISE
JS_DestroyRuntime(mJSRuntime);
mJSRuntime = nullptr;
nsCycleCollector_forgetJSRuntime();
@ -540,6 +546,9 @@ CycleCollectedJSRuntime::Initialize(JSRuntime* aParentRuntime,
#ifdef SPIDERMONKEY_PROMISE
JS::SetEnqueuePromiseJobCallback(mJSRuntime, EnqueuePromiseJobCallback, this);
JS::SetPromiseRejectionTrackerCallback(mJSRuntime, PromiseRejectionTrackerCallback, this);
mUncaughtRejections.init(mJSRuntime, JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>(js::SystemAllocPolicy()));
mConsumedRejections.init(mJSRuntime, JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>(js::SystemAllocPolicy()));
#endif // SPIDERMONKEY_PROMISE
JS::dbg::SetDebuggerMallocSizeOf(mJSRuntime, moz_malloc_size_of);
@ -948,6 +957,28 @@ CycleCollectedJSRuntime::EnqueuePromiseJobCallback(JSContext* aCx,
return true;
}
#ifdef SPIDERMONKEY_PROMISE
/* static */
void
CycleCollectedJSRuntime::PromiseRejectionTrackerCallback(JSContext* aCx,
JS::HandleObject aPromise,
PromiseRejectionHandlingState state,
void* aData)
{
#ifdef DEBUG
CycleCollectedJSRuntime* self = static_cast<CycleCollectedJSRuntime*>(aData);
#endif // DEBUG
MOZ_ASSERT(JS_GetRuntime(aCx) == self->Runtime());
MOZ_ASSERT(Get() == self);
if (state == PromiseRejectionHandlingState::Unhandled) {
PromiseDebugging::AddUncaughtRejection(aPromise);
} else {
PromiseDebugging::AddConsumedRejection(aPromise);
}
}
#endif // SPIDERMONKEY_PROMISE
struct JsGcTracer : public TraceCallbacks
{
virtual void Trace(JS::Heap<JS::Value>* aPtr, const char* aName,

View File

@ -221,6 +221,12 @@ private:
JS::HandleObject aJob,
JS::HandleObject aAllocationSite,
void* aData);
#ifdef SPIDERMONKEY_PROMISE
static void PromiseRejectionTrackerCallback(JSContext* aCx,
JS::HandleObject aPromise,
PromiseRejectionHandlingState state,
void* aData);
#endif // SPIDERMONKEY_PROMISE
virtual void TraceNativeBlackRoots(JSTracer* aTracer) { };
void TraceNativeGrayRoots(JSTracer* aTracer);
@ -362,11 +368,24 @@ public:
// Storage for watching rejected promises waiting for some client to
// consume their rejection.
#ifdef SPIDERMONKEY_PROMISE
// Promises in this list have been rejected in the last turn of the
// event loop without the rejection being handled.
// Note that this can contain nullptrs in place of promises removed because
// they're consumed before it'd be reported.
JS::PersistentRooted<JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>> mUncaughtRejections;
// Promises in this list have previously been reported as rejected
// (because they were in the above list), but the rejection was handled
// in the last turn of the event loop.
JS::PersistentRooted<JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>> mConsumedRejections;
#else
// We store values as `nsISupports` to avoid adding compile-time dependencies
// from xpcom to dom/promise, but they can really only have a single concrete
// type.
nsTArray<nsCOMPtr<nsISupports /* Promise */>> mUncaughtRejections;
nsTArray<nsCOMPtr<nsISupports /* Promise */ >> mConsumedRejections;
#endif // SPIDERMONKEY_PROMISE
nsTArray<nsCOMPtr<nsISupports /* UncaughtRejectionObserver */ >> mUncaughtRejectionObservers;
private: