mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1083361 - Exposing a PromiseDebugging API to monitor uncaught DOM Promise. r=bz
This commit is contained in:
parent
e4647d8db5
commit
78ace752f9
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
7
dom/promise/tests/browser.ini
Normal file
7
dom/promise/tests/browser.ini
Normal 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]
|
262
dom/promise/tests/browser_monitorUncaught.js
Normal file
262
dom/promise/tests/browser_monitorUncaught.js
Normal 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;
|
||||
|
||||
});
|
@ -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);
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -1299,3 +1299,4 @@ CycleCollectedJSRuntime::OnLargeAllocationFailure()
|
||||
CustomLargeAllocationFailureCallback();
|
||||
AnnotateAndSetOutOfMemory(&mLargeAllocationFailureState, OOMState::Reported);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user