Bug 1058695 - Add member to nsIGlobalObject to detect it is going away. Make promises use it. r=bholley
☠☠ backed out by 66e5d77f947d ☠ ☠
authorNikhil Marathe <nsm.nikhil@gmail.com>
Wed, 22 Apr 2015 16:34:21 -0700
changeset 240773 cb59db723d15f69f390cbea514069607d4bb8379
parent 240772 7c9a39a083d1fa5f54e63deb6a3ea23fe8a2ba7b
child 240774 490428d618442718179d7a48eb9751d3cd306156
push id28644
push userryanvm@gmail.com
push dateThu, 23 Apr 2015 21:10:29 +0000
treeherdermozilla-central@22a157f7feb7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbholley
bugs1058695
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1058695 - Add member to nsIGlobalObject to detect it is going away. Make promises use it. r=bholley
dom/base/nsGlobalWindow.cpp
dom/base/nsIGlobalObject.h
dom/promise/Promise.cpp
dom/promise/PromiseCallback.cpp
dom/promise/PromiseCallback.h
js/src/jsapi.h
js/src/jsfriendapi.h
js/xpconnect/src/XPCJSRuntime.cpp
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -1433,16 +1433,18 @@ nsGlobalWindow::DropOuterWindowDocs()
 void
 nsGlobalWindow::CleanUp()
 {
   // Guarantee idempotence.
   if (mCleanedUp)
     return;
   mCleanedUp = true;
 
+  StartDying();
+
   mEventTargetObjects.EnumerateEntries(DisconnectEventTargetObjects, nullptr);
   mEventTargetObjects.Clear();
 
   if (mObserver) {
     nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
     if (os) {
       os->RemoveObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
       os->RemoveObserver(mObserver, "dom-storage2-changed");
--- a/dom/base/nsIGlobalObject.h
+++ b/dom/base/nsIGlobalObject.h
@@ -12,21 +12,55 @@
 #define NS_IGLOBALOBJECT_IID \
 { 0xe2538ded, 0x13ef, 0x4f4d, \
 { 0x94, 0x6b, 0x65, 0xd3, 0x33, 0xb4, 0xf0, 0x3c } }
 
 class nsIPrincipal;
 
 class nsIGlobalObject : public nsISupports
 {
+  bool mIsDying;
+
+protected:
+  nsIGlobalObject()
+   : mIsDying(false)
+  {}
+
 public:
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_IGLOBALOBJECT_IID)
 
+  /**
+   * This check is added to deal with Promise microtask queues. On the main
+   * thread, we do not impose restrictions about when script stops running or
+   * when runnables can no longer be dispatched to the main thread. This means
+   * it is possible for a Promise chain to keep resolving an infinite chain of
+   * promises, preventing the browser from shutting down. See Bug 1058695. To
+   * prevent this, the nsGlobalWindow subclass sets this flag when it is
+   * closed. The Promise implementation checks this and prohibits new runnables
+   * from being dispatched.
+   *
+   * We pair this with checks during processing the promise microtask queue
+   * that pops up the slow script dialog when the Promise queue is preventing
+   * a window from going away.
+   */
+  bool
+  IsDying() const
+  {
+    return mIsDying;
+  }
+
   virtual JSObject* GetGlobalJSObject() = 0;
 
   // This method is not meant to be overridden.
   nsIPrincipal* PrincipalOrNull();
+
+protected:
+  void
+  StartDying()
+  {
+    mIsDying = true;
+  }
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsIGlobalObject,
                               NS_IGLOBALOBJECT_IID)
 
 #endif // nsIGlobalObject_h__
--- a/dom/promise/Promise.cpp
+++ b/dom/promise/Promise.cpp
@@ -258,17 +258,17 @@ protected:
 
         mPromise->RejectInternal(cx, exn);
       }
       // At least one of resolveFunc or rejectFunc have been called, so ignore
       // the exception. FIXME(nsm): This should be reported to the error
       // console though, for debugging.
     }
 
-    return NS_OK;
+    return rv.ErrorCode();
   }
 
 private:
   nsRefPtr<Promise> mPromise;
   JS::PersistentRooted<JSObject*> mThenable;
   nsRefPtr<PromiseInit> mThen;
   NS_DECL_OWNINGTHREAD;
 };
@@ -484,23 +484,34 @@ Promise::PerformMicroTaskCheckpoint()
   CycleCollectedJSRuntime* runtime = CycleCollectedJSRuntime::Get();
   nsTArray<nsCOMPtr<nsIRunnable>>& microtaskQueue =
     runtime->GetPromiseMicroTaskQueue();
 
   if (microtaskQueue.IsEmpty()) {
     return false;
   }
 
+  Maybe<AutoSafeJSContext> cx;
+  if (NS_IsMainThread()) {
+    cx.emplace();
+  }
+
   do {
     nsCOMPtr<nsIRunnable> runnable = microtaskQueue.ElementAt(0);
     MOZ_ASSERT(runnable);
 
     // This function can re-enter, so we remove the element before calling.
     microtaskQueue.RemoveElementAt(0);
-    runnable->Run();
+    nsresult rv = runnable->Run();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return false;
+    }
+    if (cx.isSome()) {
+      js::CheckForInterrupt(cx.ref());
+    }
   } while (!microtaskQueue.IsEmpty());
 
   return true;
 }
 
 /* static */ bool
 Promise::JSCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp)
 {
@@ -1066,16 +1077,20 @@ Promise::Compartment() const
 {
   return js::GetObjectCompartment(GlobalJSObject());
 }
 
 void
 Promise::AppendCallbacks(PromiseCallback* aResolveCallback,
                          PromiseCallback* aRejectCallback)
 {
+  if (mGlobal->IsDying()) {
+    return;
+  }
+
   MOZ_ASSERT(aResolveCallback);
   MOZ_ASSERT(aRejectCallback);
 
   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
@@ -1292,17 +1307,22 @@ Promise::RejectInternal(JSContext* aCx,
   mResolvePending = true;
 
   MaybeSettle(aValue, Rejected);
 }
 
 void
 Promise::Settle(JS::Handle<JS::Value> aValue, PromiseState aState)
 {
+  if (mGlobal->IsDying()) {
+    return;
+  }
+
   mSettlementTimestamp = TimeStamp::Now();
+
   SetResult(aValue);
   SetState(aState);
 
   AutoJSAPI jsapi;
   jsapi.Init();
   JSContext* cx = jsapi.cx();
   JS::RootedObject wrapper(cx, GetWrapper());
   MOZ_ASSERT(wrapper); // We preserved it
--- a/dom/promise/PromiseCallback.cpp
+++ b/dom/promise/PromiseCallback.cpp
@@ -66,30 +66,31 @@ ResolvePromiseCallback::ResolvePromiseCa
   HoldJSObjects(this);
 }
 
 ResolvePromiseCallback::~ResolvePromiseCallback()
 {
   DropJSObjects(this);
 }
 
-void
+nsresult
 ResolvePromiseCallback::Call(JSContext* aCx,
                              JS::Handle<JS::Value> aValue)
 {
   // Run resolver's algorithm with value and the synchronous flag set.
 
   JSAutoCompartment ac(aCx, mGlobal);
   JS::Rooted<JS::Value> value(aCx, aValue);
   if (!JS_WrapValue(aCx, &value)) {
     NS_WARNING("Failed to wrap value into the right compartment.");
-    return;
+    return NS_ERROR_FAILURE;
   }
 
   mPromise->ResolveInternal(aCx, value);
+  return NS_OK;
 }
 
 // RejectPromiseCallback
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(RejectPromiseCallback)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(RejectPromiseCallback,
                                                 PromiseCallback)
@@ -124,31 +125,32 @@ RejectPromiseCallback::RejectPromiseCall
   HoldJSObjects(this);
 }
 
 RejectPromiseCallback::~RejectPromiseCallback()
 {
   DropJSObjects(this);
 }
 
-void
+nsresult
 RejectPromiseCallback::Call(JSContext* aCx,
                             JS::Handle<JS::Value> aValue)
 {
   // Run resolver's algorithm with value and the synchronous flag set.
 
   JSAutoCompartment ac(aCx, mGlobal);
   JS::Rooted<JS::Value> value(aCx, aValue);
   if (!JS_WrapValue(aCx, &value)) {
     NS_WARNING("Failed to wrap value into the right compartment.");
-    return;
+    return NS_ERROR_FAILURE;
   }
 
 
   mPromise->RejectInternal(aCx, value);
+  return NS_OK;
 }
 
 // WrapperPromiseCallback
 NS_IMPL_CYCLE_COLLECTION_CLASS(WrapperPromiseCallback)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WrapperPromiseCallback,
                                                 PromiseCallback)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mNextPromise)
@@ -185,25 +187,25 @@ WrapperPromiseCallback::WrapperPromiseCa
   HoldJSObjects(this);
 }
 
 WrapperPromiseCallback::~WrapperPromiseCallback()
 {
   DropJSObjects(this);
 }
 
-void
+nsresult
 WrapperPromiseCallback::Call(JSContext* aCx,
                              JS::Handle<JS::Value> aValue)
 {
   JSAutoCompartment ac(aCx, mGlobal);
   JS::Rooted<JS::Value> value(aCx, aValue);
   if (!JS_WrapValue(aCx, &value)) {
     NS_WARNING("Failed to wrap value into the right compartment.");
-    return;
+    return NS_ERROR_FAILURE;
   }
 
   ErrorResult rv;
 
   // PromiseReactionTask step 6
   JS::Rooted<JS::Value> retValue(aCx);
   mCallback->Call(value, &retValue, rv, "promise callback",
                   CallbackObject::eRethrowExceptions,
@@ -214,30 +216,30 @@ WrapperPromiseCallback::Call(JSContext* 
   // PromiseReactionTask step 7
   if (rv.Failed()) {
     JS::Rooted<JS::Value> value(aCx);
     if (rv.IsJSException()) {
       rv.StealJSException(aCx, &value);
 
       if (!JS_WrapValue(aCx, &value)) {
         NS_WARNING("Failed to wrap value into the right compartment.");
-        return;
+        return NS_ERROR_FAILURE;
       }
     } else {
       // Convert the ErrorResult to a JS exception object that we can reject
       // ourselves with.  This will be exactly the exception that would get
       // thrown from a binding method whose ErrorResult ended up with whatever
       // is on "rv" right now.
       JSAutoCompartment ac(aCx, mNextPromise->GlobalJSObject());
       DebugOnly<bool> conversionResult = ToJSValue(aCx, rv, &value);
       MOZ_ASSERT(conversionResult);
     }
 
     mNextPromise->RejectInternal(aCx, value);
-    return;
+    return NS_OK;
   }
 
   // If the return value is the same as the promise itself, throw TypeError.
   if (retValue.isObject()) {
     JS::Rooted<JSObject*> valueObj(aCx, &retValue.toObject());
     Promise* returnedPromise;
     nsresult r = UNWRAP_OBJECT(Promise, valueObj, returnedPromise);
 
@@ -265,48 +267,49 @@ WrapperPromiseCallback::Call(JSContext* 
         }
       }
 
       // We're back in aValue's compartment here.
       JS::Rooted<JSString*> fn(aCx, JS_NewStringCopyZ(aCx, fileName));
       if (!fn) {
         // Out of memory. Promise will stay unresolved.
         JS_ClearPendingException(aCx);
-        return;
+        return NS_ERROR_OUT_OF_MEMORY;
       }
 
       JS::Rooted<JSString*> message(aCx,
         JS_NewStringCopyZ(aCx,
           "then() cannot return same Promise that it resolves."));
       if (!message) {
         // Out of memory. Promise will stay unresolved.
         JS_ClearPendingException(aCx);
-        return;
+        return NS_ERROR_OUT_OF_MEMORY;
       }
 
       JS::Rooted<JS::Value> typeError(aCx);
       if (!JS::CreateError(aCx, JSEXN_TYPEERR, JS::NullPtr(), fn, lineNumber, 0,
                            nullptr, message, &typeError)) {
         // Out of memory. Promise will stay unresolved.
         JS_ClearPendingException(aCx);
-        return;
+        return NS_ERROR_OUT_OF_MEMORY;
       }
 
       mNextPromise->RejectInternal(aCx, typeError);
-      return;
+      return NS_OK;
     }
   }
 
   // Otherwise, run resolver's resolve with value.
   if (!JS_WrapValue(aCx, &retValue)) {
     NS_WARNING("Failed to wrap value into the right compartment.");
-    return;
+    return NS_ERROR_FAILURE;
   }
 
   mNextPromise->ResolveInternal(aCx, retValue);
+  return NS_OK;
 }
 
 // NativePromiseCallback
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(NativePromiseCallback,
                                    PromiseCallback, mHandler)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(NativePromiseCallback)
@@ -322,31 +325,32 @@ NativePromiseCallback::NativePromiseCall
 {
   MOZ_ASSERT(aHandler);
 }
 
 NativePromiseCallback::~NativePromiseCallback()
 {
 }
 
-void
+nsresult
 NativePromiseCallback::Call(JSContext* aCx,
                             JS::Handle<JS::Value> aValue)
 {
   if (mState == Promise::Resolved) {
     mHandler->ResolvedCallback(aCx, aValue);
-    return;
+    return NS_OK;
   }
 
   if (mState == Promise::Rejected) {
     mHandler->RejectedCallback(aCx, aValue);
-    return;
+    return NS_OK;
   }
 
   NS_NOTREACHED("huh?");
+  return NS_ERROR_FAILURE;
 }
 
 /* static */ PromiseCallback*
 PromiseCallback::Factory(Promise* aNextPromise, JS::Handle<JSObject*> aGlobal,
                          AnyCallback* aCallback, Task aTask)
 {
   MOZ_ASSERT(aNextPromise);
 
--- a/dom/promise/PromiseCallback.h
+++ b/dom/promise/PromiseCallback.h
@@ -21,18 +21,18 @@ protected:
   virtual ~PromiseCallback();
 
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_CLASS(PromiseCallback)
 
   PromiseCallback();
 
-  virtual void Call(JSContext* aCx,
-                    JS::Handle<JS::Value> aValue) = 0;
+  virtual nsresult Call(JSContext* aCx,
+                        JS::Handle<JS::Value> aValue) = 0;
 
   // Return the Promise that this callback will end up resolving or
   // rejecting, if any.
   virtual Promise* GetDependentPromise() = 0;
 
   enum Task {
     Resolve,
     Reject
@@ -49,18 +49,18 @@ public:
 // aNextPromise->RejectFunction() if the JS Callback throws.
 class WrapperPromiseCallback final : public PromiseCallback
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(WrapperPromiseCallback,
                                                          PromiseCallback)
 
-  void Call(JSContext* aCx,
-            JS::Handle<JS::Value> aValue) override;
+  nsresult Call(JSContext* aCx,
+                JS::Handle<JS::Value> aValue) override;
 
   Promise* GetDependentPromise() override
   {
     return mNextPromise;
   }
 
   WrapperPromiseCallback(Promise* aNextPromise, JS::Handle<JSObject*> aGlobal,
                          AnyCallback* aCallback);
@@ -77,18 +77,18 @@ private:
 // received by Call().
 class ResolvePromiseCallback final : public PromiseCallback
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ResolvePromiseCallback,
                                                          PromiseCallback)
 
-  void Call(JSContext* aCx,
-            JS::Handle<JS::Value> aValue) override;
+  nsresult Call(JSContext* aCx,
+                JS::Handle<JS::Value> aValue) override;
 
   Promise* GetDependentPromise() override
   {
     return mPromise;
   }
 
   ResolvePromiseCallback(Promise* aPromise, JS::Handle<JSObject*> aGlobal);
 
@@ -103,18 +103,18 @@ private:
 // received by Call().
 class RejectPromiseCallback final : public PromiseCallback
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(RejectPromiseCallback,
                                                          PromiseCallback)
 
-  void Call(JSContext* aCx,
-            JS::Handle<JS::Value> aValue) override;
+  nsresult Call(JSContext* aCx,
+                JS::Handle<JS::Value> aValue) override;
 
   Promise* GetDependentPromise() override
   {
     return mPromise;
   }
 
   RejectPromiseCallback(Promise* aPromise, JS::Handle<JSObject*> aGlobal);
 
@@ -128,18 +128,18 @@ private:
 // NativePromiseCallback wraps a PromiseNativeHandler.
 class NativePromiseCallback final : public PromiseCallback
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(NativePromiseCallback,
                                            PromiseCallback)
 
-  void Call(JSContext* aCx,
-            JS::Handle<JS::Value> aValue) override;
+  nsresult Call(JSContext* aCx,
+                JS::Handle<JS::Value> aValue) override;
 
   Promise* GetDependentPromise() override
   {
     return nullptr;
   }
 
   NativePromiseCallback(PromiseNativeHandler* aHandler,
                         Promise::PromiseState aState);
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -3936,17 +3936,16 @@ Call(JSContext* cx, JS::HandleValue this
     JS::RootedValue fun(cx, JS::ObjectValue(*funObj));
     return Call(cx, thisv, fun, args, rval);
 }
 
 extern JS_PUBLIC_API(bool)
 Construct(JSContext* cx, JS::HandleValue fun,
           const JS::HandleValueArray& args,
           MutableHandleValue rval);
-
 } /* namespace JS */
 
 /*
  * These functions allow setting an interrupt callback that will be called
  * from the JS thread some time after any thread triggered the callback using
  * JS_RequestInterruptCallback(rt).
  *
  * To schedule the GC and for other activities the engine internally triggers
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -2737,16 +2737,18 @@ GetFirstSubsumedSavedFrame(JSContext* cx
 
 extern JS_FRIEND_API(bool)
 ReportIsNotFunction(JSContext* cx, JS::HandleValue v);
 
 extern JS_FRIEND_API(bool)
 DefineOwnProperty(JSContext* cx, JSObject* objArg, jsid idArg,
                   JS::Handle<JSPropertyDescriptor> descriptor, JS::ObjectOpResult& result);
 
+extern JS_FRIEND_API(bool)
+CheckForInterrupt(JSContext* cx);
 } /* namespace js */
 
 extern JS_FRIEND_API(void)
 JS_StoreObjectPostBarrierCallback(JSContext* cx,
                                   void (*callback)(JSTracer* trc, JSObject* key, void* data),
                                   JSObject* key, void* data);
 
 extern JS_FRIEND_API(void)
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -1456,18 +1456,23 @@ XPCJSRuntime::InterruptCallback(JSContex
         if (!JS_GetPrototype(cx, global, &proto))
             return false;
         if (proto && IsSandboxPrototypeProxy(proto) &&
             (proto = js::CheckedUnwrap(proto, /* stopAtOuter = */ false)))
         {
             win = WindowGlobalOrNull(proto);
         }
     }
-    if (!win)
+
+    if (!win) {
+        NS_WARNING("No active window");
         return true;
+    }
+
+    MOZ_ASSERT(!win->IsDying());
 
     if (win->GetIsPrerendered()) {
         // We cannot display a dialog if the page is being prerendered, so
         // just kill the page.
         mozilla::dom::HandlePrerenderingViolation(win);
         return false;
     }