Bug 1083361 - Exposing a PromiseDebugging API to monitor uncaught DOM Promise. r=bz

This commit is contained in:
David Rajchenbach-Teller 2014-11-19 14:31:06 +01:00
parent 33c237873d
commit b71d04be6e
11 changed files with 658 additions and 13 deletions

View File

@ -14,9 +14,11 @@
#include "mozilla/dom/PromiseBinding.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/MediaStreamError.h"
#include "mozilla/Atomics.h"
#include "mozilla/CycleCollectedJSRuntime.h"
#include "mozilla/Preferences.h"
#include "PromiseCallback.h"
#include "PromiseDebugging.h"
#include "PromiseNativeHandler.h"
#include "PromiseWorkerProxy.h"
#include "nsContentUtils.h"
@ -33,6 +35,12 @@
namespace mozilla {
namespace dom {
namespace {
// Generator used by Promise::GetID.
Atomic<uintptr_t> gIDGenerator(0);
}
using namespace workers;
NS_IMPL_ISUPPORTS0(PromiseNativeHandler)
@ -245,7 +253,11 @@ private:
NS_IMPL_CYCLE_COLLECTION_CLASS(Promise)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise)
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
tmp->MaybeReportRejectedOnce();
#else
tmp->mResult = JS::UndefinedValue();
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mResolveCallbacks)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRejectCallbacks)
@ -282,8 +294,14 @@ Promise::Promise(nsIGlobalObject* aGlobal)
, mRejectionStack(nullptr)
, mFullfillmentStack(nullptr)
, mState(Pending)
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
, mHadRejectCallback(false)
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
, mTaskPending(false)
, mResolvePending(false)
, mIsLastInChain(true)
, mWasNotifiedAsUncaught(false)
, mID(0)
{
MOZ_ASSERT(mGlobal);
@ -294,7 +312,9 @@ Promise::Promise(nsIGlobalObject* aGlobal)
Promise::~Promise()
{
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
MaybeReportRejectedOnce();
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
mozilla::DropJSObjects(this);
}
@ -935,17 +955,26 @@ void
Promise::AppendCallbacks(PromiseCallback* aResolveCallback,
PromiseCallback* aRejectCallback)
{
if (aResolveCallback) {
mResolveCallbacks.AppendElement(aResolveCallback);
}
MOZ_ASSERT(aResolveCallback);
MOZ_ASSERT(aRejectCallback);
if (aRejectCallback) {
mHadRejectCallback = true;
mRejectCallbacks.AppendElement(aRejectCallback);
// Now that there is a callback, we don't need to report anymore.
RemoveFeature();
if (mIsLastInChain && mState == PromiseState::Rejected) {
// This rejection is now consumed.
PromiseDebugging::AddConsumedRejection(*this);
// Note that we may not have had the opportunity to call
// RunResolveTask() yet, so we may never have called
// `PromiseDebugging:AddUncaughtRejection`.
}
mIsLastInChain = false;
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
// Now that there is a callback, we don't need to report anymore.
mHadRejectCallback = true;
RemoveFeature();
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
mResolveCallbacks.AppendElement(aResolveCallback);
mRejectCallbacks.AppendElement(aRejectCallback);
// If promise's state is fulfilled, queue a task to process our fulfill
// callbacks with promise's result. If promise's state is rejected, queue a
@ -998,6 +1027,7 @@ Promise::DispatchToMicroTask(nsIRunnable* aRunnable)
microtaskQueue.AppendElement(aRunnable);
}
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
void
Promise::MaybeReportRejected()
{
@ -1042,6 +1072,7 @@ Promise::MaybeReportRejected()
new AsyncErrorReporter(CycleCollectedJSRuntime::Get()->Runtime(), xpcReport);
NS_DispatchToMainThread(r);
}
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
void
Promise::MaybeResolveInternal(JSContext* aCx,
@ -1131,6 +1162,16 @@ Promise::Settle(JS::Handle<JS::Value> aValue, PromiseState aState)
JSAutoCompartment ac(cx, wrapper);
JS::dbg::onPromiseSettled(cx, wrapper);
if (aState == PromiseState::Rejected &&
mIsLastInChain) {
// The Promise has just been rejected, and it is last in chain.
// We need to inform PromiseDebugging.
// If the Promise is eventually not the last in chain anymore,
// we will need to inform PromiseDebugging again.
PromiseDebugging::AddUncaughtRejection(*this);
}
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
// If the Promise was rejected, and there is no reject handler already setup,
// watch for thread shutdown.
if (aState == PromiseState::Rejected &&
@ -1149,6 +1190,7 @@ Promise::Settle(JS::Handle<JS::Value> aValue, PromiseState aState)
MaybeReportRejectedOnce();
}
}
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
EnqueueCallbackTasks();
}
@ -1183,6 +1225,7 @@ Promise::EnqueueCallbackTasks()
}
}
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
void
Promise::RemoveFeature()
{
@ -1202,6 +1245,7 @@ PromiseReportRejectFeature::Notify(JSContext* aCx, workers::Status aStatus)
// After this point, `this` has been deleted by RemoveFeature!
return true;
}
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
bool
Promise::CaptureStack(JSContext* aCx, JS::Heap<JSObject*>& aTarget)
@ -1444,5 +1488,13 @@ void Promise::MaybeRejectBrokenly(const nsAString& aArg) {
MaybeSomething(aArg, &Promise::MaybeReject);
}
uint64_t
Promise::GetID() {
if (mID != 0) {
return mID;
}
return mID = ++gIDGenerator;
}
} // namespace dom
} // namespace mozilla

View File

@ -22,6 +22,13 @@
#include "mozilla/dom/workers/bindings/WorkerFeature.h"
// Bug 1083361 introduces a new mechanism for tracking uncaught
// rejections. This #define serves to track down the parts of code
// 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
class nsIGlobalObject;
namespace mozilla {
@ -35,6 +42,7 @@ class PromiseInit;
class PromiseNativeHandler;
class PromiseDebugging;
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
class Promise;
class PromiseReportRejectFeature : public workers::WorkerFeature
{
@ -51,6 +59,7 @@ public:
virtual bool
Notify(JSContext* aCx, workers::Status aStatus) MOZ_OVERRIDE;
};
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
class Promise : public nsISupports,
public nsWrapperCache,
@ -59,7 +68,9 @@ class Promise : public nsISupports,
friend class NativePromiseCallback;
friend class PromiseResolverTask;
friend class PromiseTask;
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
friend class PromiseReportRejectFeature;
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
friend class PromiseWorkerProxy;
friend class PromiseWorkerProxyRunnable;
friend class RejectPromiseCallback;
@ -171,6 +182,9 @@ public:
void AppendNativeHandler(PromiseNativeHandler* aRunnable);
// Return a unique-to-the-process identifier for this Promise.
uint64_t GetID();
protected:
// Do NOT call this unless you're Promise::Create. I wish we could enforce
// that from inside this class too, somehow.
@ -198,6 +212,21 @@ protected:
void GetDependentPromises(nsTArray<nsRefPtr<Promise>>& aPromises);
bool IsLastInChain() const
{
return mIsLastInChain;
}
void SetNotifiedAsUncaught()
{
mWasNotifiedAsUncaught = true;
}
bool WasNotifiedAsUncaught() const
{
return mWasNotifiedAsUncaught;
}
private:
friend class PromiseDebugging;
@ -231,6 +260,7 @@ private:
void AppendCallbacks(PromiseCallback* aResolveCallback,
PromiseCallback* aRejectCallback);
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
// If we have been rejected and our mResult is a JS exception,
// report it to the error console.
// Use MaybeReportRejectedOnce() for actual calls.
@ -241,6 +271,7 @@ private:
RemoveFeature();
mResult = JS::UndefinedValue();
}
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
void MaybeResolveInternal(JSContext* aCx,
JS::Handle<JS::Value> aValue);
@ -289,7 +320,9 @@ private:
void HandleException(JSContext* aCx);
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
void RemoveFeature();
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
// Capture the current stack and store it in aTarget. If false is
// returned, an exception is presumably pending on aCx.
@ -315,21 +348,38 @@ private:
// have a fulfillment stack.
JS::Heap<JSObject*> mFullfillmentStack;
PromiseState mState;
bool mHadRejectCallback;
bool mResolvePending;
#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
bool mHadRejectCallback;
// If a rejected promise on a worker has no reject callbacks attached, it
// needs to know when the worker is shutting down, to report the error on the
// console before the worker's context is deleted. This feature is used for
// that purpose.
nsAutoPtr<PromiseReportRejectFeature> mFeature;
#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
bool mTaskPending;
bool mResolvePending;
// `true` if this Promise is the last in the chain, or `false` if
// another Promise has been created from this one by a call to
// `then`, `all`, `race`, etc.
bool mIsLastInChain;
// `true` if PromiseDebugging has already notified at least one observer that
// this promise was left uncaught, `false` otherwise.
bool mWasNotifiedAsUncaught;
// The time when this promise was created.
TimeStamp mCreationTimestamp;
// The time when this promise transitioned out of the pending state.
TimeStamp mSettlementTimestamp;
// Once `GetID()` has been called, a unique-to-the-process identifier for this
// promise. Until then, `0`.
uint64_t mID;
};
} // namespace dom

View File

@ -4,18 +4,65 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/PromiseDebugging.h"
#include "js/Value.h"
#include "nsThreadUtils.h"
#include "mozilla/CycleCollectedJSRuntime.h"
#include "mozilla/ThreadLocal.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseDebugging.h"
#include "mozilla/dom/PromiseDebuggingBinding.h"
namespace mozilla {
namespace dom {
namespace {
class FlushRejections: public nsCancelableRunnable
{
public:
static void Init() {
if (!sDispatched.init()) {
MOZ_CRASH("Could not initialize FlushRejections::sDispatched");
}
sDispatched.set(false);
}
static void DispatchNeeded() {
if (sDispatched.get()) {
// An instance of `FlushRejections` has already been dispatched
// and not run yet. No need to dispatch another one.
return;
}
sDispatched.set(true);
NS_DispatchToCurrentThread(new FlushRejections());
}
nsresult Run()
{
sDispatched.set(false);
// Call the callbacks if necessary.
// Note that these callbacks may in turn cause Promise to turn
// uncaught or consumed. Since `sDispatched` is `false`,
// `FlushRejections` will be called once again, on an ulterior
// tick.
PromiseDebugging::FlushUncaughtRejections();
return NS_OK;
}
private:
// `true` if an instance of `FlushRejections` is currently dispatched
// and has not been executed yet.
static ThreadLocal<bool> sDispatched;
};
/* static */ ThreadLocal<bool>
FlushRejections::sDispatched;
} // namespace
/* static */ void
PromiseDebugging::GetState(GlobalObject&, Promise& aPromise,
PromiseDebuggingStateHolder& aState)
@ -37,6 +84,30 @@ PromiseDebugging::GetState(GlobalObject&, Promise& aPromise,
}
}
/*static */ nsString
PromiseDebugging::sIDPrefix;
/* static */ void
PromiseDebugging::Init()
{
FlushRejections::Init();
// Generate a prefix for identifiers: "PromiseDebugging.$processid."
sIDPrefix = NS_LITERAL_STRING("PromiseDebugging.");
if (XRE_GetProcessType() == GeckoProcessType_Content) {
sIDPrefix.AppendInt(ContentChild::GetSingleton()->GetID());
sIDPrefix.Append('.');
} else {
sIDPrefix.AppendLiteral("0.");
}
}
/* static */ void
PromiseDebugging::Shutdown()
{
sIDPrefix.SetIsVoid(true);
}
/* static */ void
PromiseDebugging::GetAllocationStack(GlobalObject&, Promise& aPromise,
JS::MutableHandle<JSObject*> aStack)
@ -83,5 +154,107 @@ PromiseDebugging::GetTimeToSettle(GlobalObject&, Promise& aPromise,
aPromise.mCreationTimestamp).ToMilliseconds();
}
/* static */ void
PromiseDebugging::AddUncaughtRejectionObserver(GlobalObject&,
UncaughtRejectionObserver& aObserver)
{
CycleCollectedJSRuntime* storage = CycleCollectedJSRuntime::Get();
nsTArray<nsRefPtr<UncaughtRejectionObserver>>& observers = storage->mUncaughtRejectionObservers;
observers.AppendElement(&aObserver);
}
/* static */ void
PromiseDebugging::RemoveUncaughtRejectionObserver(GlobalObject&,
UncaughtRejectionObserver& aObserver)
{
CycleCollectedJSRuntime* storage = CycleCollectedJSRuntime::Get();
nsTArray<nsRefPtr<UncaughtRejectionObserver>>& observers = storage->mUncaughtRejectionObservers;
for (size_t i = 0; i < observers.Length(); ++i) {
if (*observers[i] == aObserver) {
observers.RemoveElementAt(i);
return;
}
}
}
/* static */ void
PromiseDebugging::AddUncaughtRejection(Promise& aPromise)
{
CycleCollectedJSRuntime::Get()->mUncaughtRejections.AppendElement(&aPromise);
FlushRejections::DispatchNeeded();
}
/* void */ void
PromiseDebugging::AddConsumedRejection(Promise& aPromise)
{
CycleCollectedJSRuntime::Get()->mConsumedRejections.AppendElement(&aPromise);
FlushRejections::DispatchNeeded();
}
/* static */ void
PromiseDebugging::GetPromiseID(GlobalObject&,
Promise& aPromise,
nsString& aID)
{
uint64_t promiseID = aPromise.GetID();
aID = sIDPrefix;
aID.AppendInt(promiseID);
}
/* static */ void
PromiseDebugging::FlushUncaughtRejections()
{
CycleCollectedJSRuntime* storage = CycleCollectedJSRuntime::Get();
// The Promise that have been left uncaught (rejected and last in
// their chain) since the last call to this function.
nsTArray<nsRefPtr<Promise>> uncaught;
storage->mUncaughtRejections.SwapElements(uncaught);
// The Promise that have been left uncaught at some point, but that
// have eventually had their `then` method called.
nsTArray<nsRefPtr<Promise>> consumed;
storage->mConsumedRejections.SwapElements(consumed);
nsTArray<nsRefPtr<UncaughtRejectionObserver>>& observers = storage->mUncaughtRejectionObservers;
// Notify observers of uncaught Promise.
for (size_t i = 0; i < uncaught.Length(); ++i) {
nsRefPtr<Promise> promise = uncaught[i];
if (!promise->IsLastInChain()) {
// This promise is not the last in the chain anymore,
// so the error has been caught at some point.
continue;
}
// For the moment, the Promise is still at the end of the
// chain. Let's inform observers, so that they may decide whether
// to report it.
for (size_t j = 0; j < observers.Length(); ++j) {
ErrorResult rv;
observers[j]->OnLeftUncaught(*promise, rv);
// Ignore errors
}
promise->SetNotifiedAsUncaught();
}
// Notify observers of consumed Promise.
for (size_t i = 0; i < consumed.Length(); ++i) {
nsRefPtr<Promise> promise = consumed[i];
if (!promise->WasNotifiedAsUncaught()) {
continue;
}
MOZ_ASSERT(!promise->IsLastInChain());
for (size_t j = 0; j < observers.Length(); ++j) {
ErrorResult rv;
observers[j]->OnConsumed(*promise, rv); // Ignore errors
}
}
}
} // namespace dom
} // namespace mozilla

View File

@ -20,10 +20,14 @@ namespace dom {
class Promise;
struct PromiseDebuggingStateHolder;
class GlobalObject;
class UncaughtRejectionObserver;
class PromiseDebugging
{
public:
static void Init();
static void Shutdown();
static void GetState(GlobalObject&, Promise& aPromise,
PromiseDebuggingStateHolder& aState);
@ -38,6 +42,32 @@ public:
static double GetPromiseLifetime(GlobalObject&, Promise& aPromise);
static double GetTimeToSettle(GlobalObject&, Promise& aPromise,
ErrorResult& aRv);
static void GetPromiseID(GlobalObject&, Promise&, nsString&);
// Mechanism for watching uncaught instances of Promise.
static void AddUncaughtRejectionObserver(GlobalObject&,
UncaughtRejectionObserver& aObserver);
static void RemoveUncaughtRejectionObserver(GlobalObject&,
UncaughtRejectionObserver& aObserver);
// Mark a Promise as having been left uncaught at script completion.
static void AddUncaughtRejection(Promise&);
// Mark a Promise previously added with `AddUncaughtRejection` as
// eventually consumed.
static void AddConsumedRejection(Promise&);
// Propagate the informations from AddUncaughtRejection
// and AddConsumedRejection to observers.
static void FlushUncaughtRejections();
private:
// Identity of the process.
// This property is:
// - set during initialization of the layout module,
// prior to any Worker using it;
// - read by both the main thread and the Workers;
// - unset during shutdown of the layout module,
// after any Worker has been shutdown.
static nsString sIDPrefix;
};
} // namespace dom

View File

@ -24,11 +24,15 @@ FAIL_ON_WARNINGS = True
LOCAL_INCLUDES += [
'../base',
'../ipc',
'../workers',
]
include('/ipc/chromium/chromium-config.mozbuild')
FINAL_LIBRARY = 'xul'
MOCHITEST_MANIFESTS += ['tests/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
BROWSER_CHROME_MANIFESTS += ['tests/browser.ini']

View File

@ -0,0 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
[DEFAULT]
[browser_monitorUncaught.js]

View File

@ -0,0 +1,262 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
Cu.import("resource://gre/modules/Timer.jsm", this);
add_task(function* test_globals() {
Assert.equal(Promise.defer || undefined, undefined, "We are testing DOM Promise.");
Assert.notEqual(PromiseDebugging, undefined, "PromiseDebugging is available.");
});
add_task(function* test_promiseID() {
let p1 = new Promise(resolve => {});
let p2 = new Promise(resolve => {});
let p3 = p2.then(null, null);
let promise = [p1, p2, p3];
let identifiers = promise.map(PromiseDebugging.getPromiseID);
info("Identifiers: " + JSON.stringify(identifiers));
let idSet = new Set(identifiers);
Assert.equal(idSet.size, identifiers.length,
"PromiseDebugging.getPromiseID returns a distinct id per promise");
let identifiers2 = promise.map(PromiseDebugging.getPromiseID);
Assert.equal(JSON.stringify(identifiers),
JSON.stringify(identifiers2),
"Successive calls to PromiseDebugging.getPromiseID return the same id for the same promise");
});
add_task(function* test_observe_uncaught() {
// The names of Promise instances
let names = new Map();
// The results for UncaughtPromiseObserver callbacks.
let CallbackResults = function(name) {
this.name = name;
this.expected = new Set();
this.observed = new Set();
this.blocker = new Promise(resolve => this.resolve = resolve);
};
CallbackResults.prototype = {
observe: function(promise) {
info(this.name + " observing Promise " + names.get(promise));
Assert.equal(PromiseDebugging.getState(promise).state, "rejected",
this.name + " observed a rejected Promise");
if (!this.expected.has(promise)) {
Assert.ok(false,
this.name + " observed a Promise that it expected to observe, " +
names.get(promise) +
" (" + PromiseDebugging.getPromiseID(promise) +
", " + PromiseDebugging.getAllocationStack(promise) + ")");
}
Assert.ok(this.expected.delete(promise),
this.name + " observed a Promise that it expected to observe, " +
names.get(promise) + " (" + PromiseDebugging.getPromiseID(promise) + ")");
Assert.ok(!this.observed.has(promise),
this.name + " observed a Promise that it has not observed yet");
this.observed.add(promise);
if (this.expected.size == 0) {
this.resolve();
} else {
info(this.name + " is still waiting for " + this.expected.size + " observations:");
info(JSON.stringify([names.get(x) for (x of this.expected.values())]));
}
},
};
let onLeftUncaught = new CallbackResults("onLeftUncaught");
let onConsumed = new CallbackResults("onConsumed");
let observer = {
onLeftUncaught: function(promise, data) {
onLeftUncaught.observe(promise);
},
onConsumed: function(promise) {
onConsumed.observe(promise);
},
};
let resolveLater = function(delay = 20) {
return new Promise((resolve, reject) => setTimeout(resolve, delay));
};
let rejectLater = function(delay = 20) {
return new Promise((resolve, reject) => setTimeout(reject, delay));
};
let makeSamples = function*() {
yield {
promise: Promise.resolve(0),
name: "Promise.resolve",
};
yield {
promise: Promise.resolve(resolve => resolve(0)),
name: "Resolution callback",
};
yield {
promise: Promise.resolve(0).then(null, null),
name: "`then(null, null)`"
};
yield {
promise: Promise.reject(0).then(null, () => {}),
name: "Reject and catch immediately",
};
yield {
promise: resolveLater(),
name: "Resolve later",
};
yield {
promise: Promise.reject("Simple rejection"),
leftUncaught: true,
consumed: false,
name: "Promise.reject",
};
// Reject a promise now, consume it later.
let p = Promise.reject("Reject now, consume later");
setTimeout(() => p.then(null, () => {
info("Consumed promise");
}), 200);
yield {
promise: p,
leftUncaught: true,
consumed: true,
name: "Reject now, consume later",
};
yield {
promise: Promise.all([
Promise.resolve("Promise.all"),
rejectLater()
]),
leftUncaught: true,
name: "Rejecting through Promise.all"
};
yield {
promise: Promise.race([
resolveLater(500),
Promise.reject(),
]),
leftUncaught: true, // The rejection wins the race.
name: "Rejecting through Promise.race",
};
yield {
promise: Promise.race([
Promise.resolve(),
rejectLater(500)
]),
leftUncaught: false, // The resolution wins the race.
name: "Resolving through Promise.race",
};
let boom = new Error("`throw` in the constructor");
yield {
promise: new Promise(() => { throw boom; }),
leftUncaught: true,
name: "Throwing in the constructor",
};
let rejection = Promise.reject("`reject` during resolution");
yield {
promise: rejection,
leftUncaught: false,
consumed: false, // `rejection` is consumed immediately (see below)
name: "Promise.reject, again",
};
yield {
promise: new Promise(resolve => resolve(rejection)),
leftUncaught: true,
consumed: false,
name: "Resolving with a rejected promise",
};
yield {
promise: Promise.resolve(0).then(() => rejection),
leftUncaught: true,
consumed: false,
name: "Returning a rejected promise from success handler",
};
yield {
promise: Promise.resolve(0).then(() => { throw new Error(); }),
leftUncaught: true,
consumed: false,
name: "Throwing during the call to the success callback",
};
};
let samples = [];
for (let s of makeSamples()) {
samples.push(s);
info("Promise '" + s.name + "' has id " + PromiseDebugging.getPromiseID(s.promise));
}
PromiseDebugging.addUncaughtRejectionObserver(observer);
for (let s of samples) {
names.set(s.promise, s.name);
if (s.leftUncaught || false) {
onLeftUncaught.expected.add(s.promise);
}
if (s.consumed || false) {
onConsumed.expected.add(s.promise);
}
}
info("Test setup, waiting for callbacks.");
yield onLeftUncaught.blocker;
info("All calls to onLeftUncaught are complete.");
if (onConsumed.expected.size != 0) {
info("onConsumed is still waiting for the following Promise:");
info(JSON.stringify([names.get(x) for (x of onConsumed.expected.values())]));
yield onConsumed.blocker;
}
info("All calls to onConsumed are complete.");
PromiseDebugging.removeUncaughtRejectionObserver(observer);
});
add_task(function* test_uninstall_observer() {
let Observer = function() {
this.blocker = new Promise(resolve => this.resolve = resolve);
this.active = true;
};
Observer.prototype = {
set active(x) {
this._active = x;
if (x) {
PromiseDebugging.addUncaughtRejectionObserver(this);
} else {
PromiseDebugging.removeUncaughtRejectionObserver(this);
}
},
onLeftUncaught: function() {
Assert.ok(this._active, "This observer is active.");
this.resolve();
},
onConsumed: function() {
Assert.ok(false, "We should not consume any Promise.");
},
};
info("Adding an observer.");
let deactivate = new Observer();
Promise.reject("I am an uncaught rejection.");
yield deactivate.blocker;
Assert.ok(true, "The observer has observed an uncaught Promise.");
deactivate.active = false;
info("Removing the observer, it should not observe any further uncaught Promise.");
info("Rejecting a Promise and waiting a little to give a chance to observers.");
let wait = new Observer();
Promise.reject("I am another uncaught rejection.");
yield wait.blocker;
yield new Promise(resolve => setTimeout(resolve, 100));
// Normally, `deactivate` should not be notified of the uncaught rejection.
wait.active = false;
});

View File

@ -14,6 +14,42 @@ dictionary PromiseDebuggingStateHolder {
};
enum PromiseDebuggingState { "pending", "fulfilled", "rejected" };
/**
* An observer for Promise that _may_ be leaking uncaught rejections.
*
* It is generally a programming error to leave a Promise rejected and
* not consume its rejection. The information exposed by this
* interface is designed to allow clients to track down such Promise,
* i.e. Promise that are currently
* - in `rejected` state;
* - last of their chain.
*
* Note, however, that a promise in such a state at the end of a tick
* may eventually be consumed in some ulterior tick. Implementers of
* this interface are responsible for presenting the information
* in a meaningful manner.
*/
callback interface UncaughtRejectionObserver {
/**
* A Promise has been left in `rejected` state and is the
* last in its chain.
*
* @param p A currently uncaught Promise. If `p` is is eventually
* caught, i.e. if its `then` callback is called, `onConsumed` will
* be called.
*/
void onLeftUncaught(Promise<any> p);
/**
* A Promise previously left uncaught is not the last in its
* chain anymore.
*
* @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.
*/
void onConsumed(Promise<any> p);
};
[ChromeOnly, Exposed=(Window,System)]
interface PromiseDebugging {
static PromiseDebuggingStateHolder getState(Promise<any> p);
@ -38,6 +74,12 @@ interface PromiseDebugging {
*/
static object? getFullfillmentStack(Promise<any> p);
/**
* Return an identifier for a promise. This identifier is guaranteed
* to be unique to this instance of Firefox.
*/
static DOMString getPromiseID(Promise<any> p);
/**
* Get the promises directly depending on a given promise. These are:
*
@ -68,4 +110,13 @@ interface PromiseDebugging {
*/
[Throws]
static DOMHighResTimeStamp getTimeToSettle(Promise<any> p);
/**
* Watching uncaught rejections on the current thread.
*
* Adding an observer twice will cause it to be notified twice
* of events.
*/
static void addUncaughtRejectionObserver(UncaughtRejectionObserver o);
static void removeUncaughtRejectionObserver(UncaughtRejectionObserver o);
};

View File

@ -67,6 +67,7 @@
#include "AudioChannelService.h"
#include "mozilla/dom/DataStoreService.h"
#include "mozilla/dom/PromiseDebugging.h"
#ifdef MOZ_XUL
#include "nsXULPopupManager.h"
@ -300,6 +301,8 @@ nsLayoutStatics::Initialize()
IMEStateManager::Init();
PromiseDebugging::Init();
return NS_OK;
}
@ -431,4 +434,6 @@ nsLayoutStatics::Shutdown()
CacheObserver::Shutdown();
CameraPreferences::Shutdown();
PromiseDebugging::Shutdown();
}

View File

@ -1299,3 +1299,4 @@ CycleCollectedJSRuntime::OnLargeAllocationFailure()
CustomLargeAllocationFailureCallback();
AnnotateAndSetOutOfMemory(&mLargeAllocationFailureState, OOMState::Reported);
}

View File

@ -16,6 +16,10 @@
#include "nsHashKeys.h"
#include "nsTArray.h"
#include "mozilla/dom/PromiseDebugging.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseDebuggingBinding.h"
class nsCycleCollectionNoteRootCallback;
class nsIException;
class nsIRunnable;
@ -291,6 +295,12 @@ public:
// isn't one.
static CycleCollectedJSRuntime* Get();
// Storage for watching rejected promises waiting for some client to
// consume their rejection.
nsTArray<nsRefPtr<dom::Promise>> mUncaughtRejections;
nsTArray<nsRefPtr<dom::Promise>> mConsumedRejections;
nsTArray<nsRefPtr<dom::UncaughtRejectionObserver>> mUncaughtRejectionObservers;
private:
JSGCThingParticipant mGCThingCycleCollectorGlobal;