Bug 1058695 - Add member to nsIGlobalObject to detect it is going away. Make promises use it. r=bholley
authorNikhil Marathe <nsm.nikhil@gmail.com>
Mon, 27 Apr 2015 12:00:41 -0700
changeset 241240 531631f340210c8e885b0f9a34ec00e1d474a15b
parent 241239 b2dd209a43df1379f9f7e7272032ec4b94585b32
child 241241 e44777d69e7a949a3fbaaae37e3c7986c9847e49
push id59063
push usernsm.nikhil@gmail.com
push dateMon, 27 Apr 2015 19:08:01 +0000
treeherdermozilla-inbound@531631f34021 [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.cpp
js/src/jsapi.h
js/xpconnect/src/XPCJSRuntime.cpp
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -1434,16 +1434,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.StealNSResult();
   }
 
 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.cpp
+++ b/js/src/jsapi.cpp
@@ -4457,16 +4457,22 @@ JS_New(JSContext* cx, HandleObject ctor,
     RootedObject obj(cx);
     {
         AutoLastFrameCheck lfc(cx);
         obj = JS_NewHelper(cx, ctor, inputArgs);
     }
     return obj;
 }
 
+JS_PUBLIC_API(bool)
+JS_CheckForInterrupt(JSContext* cx)
+{
+    return js::CheckForInterrupt(cx);
+}
+
 JS_PUBLIC_API(JSInterruptCallback)
 JS_SetInterruptCallback(JSRuntime* rt, JSInterruptCallback callback)
 {
     JSInterruptCallback old = rt->interruptCallback;
     rt->interruptCallback = callback;
     return old;
 }
 
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -3943,19 +3943,21 @@ 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 */
 
+extern JS_PUBLIC_API(bool)
+JS_CheckForInterrupt(JSContext* cx);
+
 /*
  * 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
  * interrupt callbacks. The embedding should thus not rely on callbacks being
  * triggered through the external API only.
--- 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;
     }