Bug 1491403 - Part 3: Propagate the user input event handling state to the promise resolve handlers in case the promise creator requests it r=smaug,arai,baku
authorEhsan Akhgari <ehsan@mozilla.com>
Tue, 09 Oct 2018 21:42:22 +0000
changeset 496072 18d88d24495acae45a381f8bf95c3ab86fe800ec
parent 496071 b62dd5af680e03d48caaa31f6fa337b12dc0eeb7
child 496073 ae1629a704aa5cd43caffce144d40623416773e8
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, arai, baku
bugs1491403
milestone64.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 1491403 - Part 3: Propagate the user input event handling state to the promise resolve handlers in case the promise creator requests it r=smaug,arai,baku Depends on D7004 Differential Revision: https://phabricator.services.mozilla.com/D7005
dom/promise/Promise.cpp
dom/promise/Promise.h
xpcom/base/CycleCollectedJSContext.cpp
--- a/dom/promise/Promise.cpp
+++ b/dom/promise/Promise.cpp
@@ -6,19 +6,21 @@
 
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/Promise-inl.h"
 
 #include "js/Debug.h"
 
 #include "mozilla/Atomics.h"
 #include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/EventStateManager.h"
 #include "mozilla/OwningNonNull.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/ResultExtensions.h"
+#include "mozilla/Unused.h"
 
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/DOMExceptionBinding.h"
 #include "mozilla/dom/MediaStreamError.h"
 #include "mozilla/dom/PromiseBinding.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/WorkerPrivate.h"
@@ -84,66 +86,86 @@ Promise::Promise(nsIGlobalObject* aGloba
 
 Promise::~Promise()
 {
   mozilla::DropJSObjects(this);
 }
 
 // static
 already_AddRefed<Promise>
-Promise::Create(nsIGlobalObject* aGlobal, ErrorResult& aRv)
+Promise::Create(nsIGlobalObject* aGlobal,
+                ErrorResult& aRv,
+                PropagateUserInteraction aPropagateUserInteraction)
 {
   if (!aGlobal) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
   RefPtr<Promise> p = new Promise(aGlobal);
-  p->CreateWrapper(nullptr, aRv);
+  p->CreateWrapper(nullptr, aRv, aPropagateUserInteraction);
   if (aRv.Failed()) {
     return nullptr;
   }
   return p.forget();
 }
 
+bool
+Promise::MaybePropagateUserInputEventHandling()
+{
+  JS::PromiseUserInputEventHandlingState state =
+    EventStateManager::IsHandlingUserInput() ?
+      JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation :
+      JS::PromiseUserInputEventHandlingState::DidntHaveUserInteractionAtCreation;
+  JS::Rooted<JSObject*> p(RootingCx(), mPromiseObj);
+  return JS::SetPromiseUserInputEventHandlingState(p, state);
+}
+
 // static
 already_AddRefed<Promise>
 Promise::Resolve(nsIGlobalObject* aGlobal, JSContext* aCx,
-                 JS::Handle<JS::Value> aValue, ErrorResult& aRv)
+                 JS::Handle<JS::Value> aValue,
+                 ErrorResult& aRv,
+                 PropagateUserInteraction aPropagateUserInteraction)
 {
   JSAutoRealm ar(aCx, aGlobal->GetGlobalJSObject());
   JS::Rooted<JSObject*> p(aCx,
                           JS::CallOriginalPromiseResolve(aCx, aValue));
   if (!p) {
     aRv.NoteJSContextException(aCx);
     return nullptr;
   }
 
-  return CreateFromExisting(aGlobal, p);
+  return CreateFromExisting(aGlobal, p, aPropagateUserInteraction);
 }
 
 // static
 already_AddRefed<Promise>
 Promise::Reject(nsIGlobalObject* aGlobal, JSContext* aCx,
                 JS::Handle<JS::Value> aValue, ErrorResult& aRv)
 {
   JSAutoRealm ar(aCx, aGlobal->GetGlobalJSObject());
   JS::Rooted<JSObject*> p(aCx,
                           JS::CallOriginalPromiseReject(aCx, aValue));
   if (!p) {
     aRv.NoteJSContextException(aCx);
     return nullptr;
   }
 
-  return CreateFromExisting(aGlobal, p);
+  // This promise will never be resolved, so we pass
+  // eDontPropagateUserInteraction for aPropagateUserInteraction
+  // unconditionally.
+  return CreateFromExisting(aGlobal, p, eDontPropagateUserInteraction);
 }
 
 // static
 already_AddRefed<Promise>
 Promise::All(JSContext* aCx,
-             const nsTArray<RefPtr<Promise>>& aPromiseList, ErrorResult& aRv)
+             const nsTArray<RefPtr<Promise>>& aPromiseList,
+             ErrorResult& aRv,
+             PropagateUserInteraction aPropagateUserInteraction)
 {
   JS::Rooted<JSObject*> globalObj(aCx, JS::CurrentGlobalOrNull(aCx));
   if (!globalObj) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
 
   nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(globalObj);
@@ -169,17 +191,17 @@ Promise::All(JSContext* aCx,
   }
 
   JS::Rooted<JSObject*> result(aCx, JS::GetWaitForAllPromise(aCx, promises));
   if (!result) {
     aRv.NoteJSContextException(aCx);
     return nullptr;
   }
 
-  return CreateFromExisting(global, result);
+  return CreateFromExisting(global, result, aPropagateUserInteraction);
 }
 
 void
 Promise::Then(JSContext* aCx,
               // aCalleeGlobal may not be in the compartment of aCx, when called over
               // Xrays.
               JS::Handle<JSObject*> aCalleeGlobal,
               AnyCallback* aResolveCallback, AnyCallback* aRejectCallback,
@@ -266,30 +288,35 @@ Result<RefPtr<Promise>, nsresult>
 Promise::ThenWithoutCycleCollection(
   const std::function<already_AddRefed<Promise>(JSContext* aCx,
                                                 JS::HandleValue aValue)>& aCallback)
 {
   return ThenWithCycleCollectedArgs(aCallback);
 }
 
 void
-Promise::CreateWrapper(JS::Handle<JSObject*> aDesiredProto, ErrorResult& aRv)
+Promise::CreateWrapper(JS::Handle<JSObject*> aDesiredProto,
+                       ErrorResult& aRv,
+                       PropagateUserInteraction aPropagateUserInteraction)
 {
   AutoJSAPI jsapi;
   if (!jsapi.Init(mGlobal)) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
   JSContext* cx = jsapi.cx();
   mPromiseObj = JS::NewPromiseObject(cx, nullptr, aDesiredProto);
   if (!mPromiseObj) {
     JS_ClearPendingException(cx);
     aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
     return;
   }
+  if (aPropagateUserInteraction == ePropagateUserInteraction) {
+    Unused << MaybePropagateUserInputEventHandling();
+  }
 }
 
 void
 Promise::MaybeResolve(JSContext* aCx,
                       JS::Handle<JS::Value> aValue)
 {
   NS_ASSERT_OWNINGTHREAD(Promise);
 
@@ -486,22 +513,27 @@ Promise::HandleException(JSContext* aCx)
     // This is only called from MaybeSomething, so it's OK to MaybeReject here.
     MaybeReject(aCx, exn);
   }
 }
 
 // static
 already_AddRefed<Promise>
 Promise::CreateFromExisting(nsIGlobalObject* aGlobal,
-                            JS::Handle<JSObject*> aPromiseObj)
+                            JS::Handle<JSObject*> aPromiseObj,
+                            PropagateUserInteraction aPropagateUserInteraction)
 {
   MOZ_ASSERT(js::GetObjectCompartment(aGlobal->GetGlobalJSObject()) ==
              js::GetObjectCompartment(aPromiseObj));
   RefPtr<Promise> p = new Promise(aGlobal);
   p->mPromiseObj = aPromiseObj;
+  if (aPropagateUserInteraction == ePropagateUserInteraction &&
+      !p->MaybePropagateUserInputEventHandling()) {
+    return nullptr;
+  }
   return p.forget();
 }
 
 
 void
 Promise::MaybeResolveWithUndefined()
 {
   NS_ASSERT_OWNINGTHREAD(Promise);
--- a/dom/promise/Promise.h
+++ b/dom/promise/Promise.h
@@ -46,22 +46,34 @@ class Promise : public nsISupports,
   friend class PromiseWorkerProxyRunnable;
 
 public:
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_PROMISE_IID)
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Promise)
   MOZ_DECLARE_WEAKREFERENCE_TYPENAME(Promise)
 
+  enum PropagateUserInteraction
+  {
+    eDontPropagateUserInteraction,
+    ePropagateUserInteraction
+  };
+
   // Promise creation tries to create a JS reflector for the Promise, so is
   // fallible.  Furthermore, we don't want to do JS-wrapping on a 0-refcount
   // object, so we addref before doing that and return the addrefed pointer
   // here.
+  // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+  // the promise resolve handler to be called as if we were handling user
+  // input events in case we are currently handling user input events.
   static already_AddRefed<Promise>
-  Create(nsIGlobalObject* aGlobal, ErrorResult& aRv);
+  Create(nsIGlobalObject* aGlobal,
+         ErrorResult& aRv,
+         PropagateUserInteraction aPropagateUserInteraction =
+           eDontPropagateUserInteraction);
 
   // Reports a rejected Promise by sending an error report.
   static void ReportRejectedPromise(JSContext* aCx, JS::HandleObject aPromise);
 
   typedef void (Promise::*MaybeFunc)(JSContext* aCx,
                                      JS::Handle<JS::Value> aValue);
 
   void MaybeResolve(JSContext* aCx,
@@ -111,33 +123,44 @@ public:
   nsIGlobalObject* GetParentObject() const
   {
     return mGlobal;
   }
 
   // Do the equivalent of Promise.resolve in the compartment of aGlobal.  The
   // compartment of aCx is ignored.  Errors are reported on the ErrorResult; if
   // aRv comes back !Failed(), this function MUST return a non-null value.
+  // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+  // the promise resolve handler to be called as if we were handling user
+  // input events in case we are currently handling user input events.
   static already_AddRefed<Promise>
   Resolve(nsIGlobalObject* aGlobal, JSContext* aCx,
-          JS::Handle<JS::Value> aValue, ErrorResult& aRv);
+          JS::Handle<JS::Value> aValue,
+          ErrorResult& aRv,
+          PropagateUserInteraction aPropagateUserInteraction =
+            eDontPropagateUserInteraction);
 
   // Do the equivalent of Promise.reject in the compartment of aGlobal.  The
   // compartment of aCx is ignored.  Errors are reported on the ErrorResult; if
   // aRv comes back !Failed(), this function MUST return a non-null value.
   static already_AddRefed<Promise>
   Reject(nsIGlobalObject* aGlobal, JSContext* aCx,
          JS::Handle<JS::Value> aValue, ErrorResult& aRv);
 
   // Do the equivalent of Promise.all in the current compartment of aCx.  Errors
   // are reported on the ErrorResult; if aRv comes back !Failed(), this function
   // MUST return a non-null value.
+  // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+  // the promise resolve handler to be called as if we were handling user
+  // input events in case we are currently handling user input events.
   static already_AddRefed<Promise>
   All(JSContext* aCx, const nsTArray<RefPtr<Promise>>& aPromiseList,
-      ErrorResult& aRv);
+      ErrorResult& aRv,
+      PropagateUserInteraction aPropagateUserInteraction =
+        eDontPropagateUserInteraction);
 
   void
   Then(JSContext* aCx,
        // aCalleeGlobal may not be in the compartment of aCx, when called over
        // Xrays.
        JS::Handle<JSObject*> aCalleeGlobal,
        AnyCallback* aResolveCallback, AnyCallback* aRejectCallback,
        JS::MutableHandle<JS::Value> aRetval,
@@ -188,19 +211,24 @@ public:
   void AppendNativeHandler(PromiseNativeHandler* aRunnable);
 
   JSObject* GlobalJSObject() const;
 
   JS::Compartment* Compartment() const;
 
   // Create a dom::Promise from a given SpiderMonkey Promise object.
   // aPromiseObj MUST be in the compartment of aGlobal's global JS object.
+  // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+  // the promise resolve handler to be called as if we were handling user
+  // input events in case we are currently handling user input events.
   static already_AddRefed<Promise>
   CreateFromExisting(nsIGlobalObject* aGlobal,
-                     JS::Handle<JSObject*> aPromiseObj);
+                     JS::Handle<JSObject*> aPromiseObj,
+                     PropagateUserInteraction aPropagateUserInteraction =
+                       eDontPropagateUserInteraction);
 
   enum class PromiseState {
     Pending,
     Resolved,
     Rejected
   };
 
   PromiseState State() const;
@@ -212,17 +240,23 @@ protected:
   // Promise::CreateFromExisting.  I wish we could enforce that from inside this
   // class too, somehow.
   explicit Promise(nsIGlobalObject* aGlobal);
 
   virtual ~Promise();
 
   // Do JS-wrapping after Promise creation.  Passing null for aDesiredProto will
   // use the default prototype for the sort of Promise we have.
-  void CreateWrapper(JS::Handle<JSObject*> aDesiredProto, ErrorResult& aRv);
+  // Pass ePropagateUserInteraction for aPropagateUserInteraction if you want
+  // the promise resolve handler to be called as if we were handling user
+  // input events in case we are currently handling user input events.
+  void CreateWrapper(JS::Handle<JSObject*> aDesiredProto,
+                     ErrorResult& aRv,
+                     PropagateUserInteraction aPropagateUserInteraction =
+                       eDontPropagateUserInteraction);
 
 private:
   template <typename T>
   void MaybeSomething(T&& aArgument, MaybeFunc aFunc) {
     MOZ_ASSERT(PromiseObj()); // It was preserved!
 
     AutoEntryScript aes(mGlobal, "Promise resolution or rejection");
     JSContext* cx = aes.cx();
@@ -233,16 +267,18 @@ private:
       return;
     }
 
     (this->*aFunc)(cx, val);
   }
 
   void HandleException(JSContext* aCx);
 
+  bool MaybePropagateUserInputEventHandling();
+
   RefPtr<nsIGlobalObject> mGlobal;
 
   JS::Heap<JSObject*> mPromiseObj;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(Promise, NS_PROMISE_IID)
 
 } // namespace dom
--- a/xpcom/base/CycleCollectedJSContext.cpp
+++ b/xpcom/base/CycleCollectedJSContext.cpp
@@ -4,42 +4,43 @@
  * 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/CycleCollectedJSContext.h"
 #include <algorithm>
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/CycleCollectedJSRuntime.h"
+#include "mozilla/EventStateManager.h"
 #include "mozilla/Move.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/TimelineConsumers.h"
 #include "mozilla/TimelineMarker.h"
 #include "mozilla/Unused.h"
 #include "mozilla/DebuggerOnGCRunnable.h"
 #include "mozilla/dom/DOMJSClass.h"
 #include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/ProfileTimelineMarkerBinding.h"
-#include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseBinding.h"
 #include "mozilla/dom/PromiseDebugging.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "jsapi.h"
 #include "js/Debug.h"
 #include "js/GCAPI.h"
 #include "js/Utility.h"
 #include "nsContentUtils.h"
 #include "nsCycleCollectionNoteRootCallback.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsCycleCollector.h"
 #include "nsDOMJSUtils.h"
 #include "nsDOMMutationObserver.h"
 #include "nsJSUtils.h"
+#include "nsPIDOMWindow.h"
 #include "nsWrapperCache.h"
 #include "nsStringBuffer.h"
 
 #include "nsThread.h"
 #include "nsThreadUtils.h"
 #include "xpcpublic.h"
 
 using namespace mozilla;
@@ -198,37 +199,56 @@ size_t
 CycleCollectedJSContext::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
 {
   return 0;
 }
 
 class PromiseJobRunnable final : public MicroTaskRunnable
 {
 public:
-  PromiseJobRunnable(JS::HandleObject aCallback,
+  PromiseJobRunnable(JS::HandleObject aPromise,
+                     JS::HandleObject aCallback,
                      JS::HandleObject aCallbackGlobal,
                      JS::HandleObject aAllocationSite,
                      nsIGlobalObject* aIncumbentGlobal)
-    :mCallback(
+    : mCallback(
        new PromiseJobCallback(aCallback, aCallbackGlobal, aAllocationSite,
                               aIncumbentGlobal))
+    , mPropagateUserInputEventHandling(false)
   {
     MOZ_ASSERT(js::IsFunctionObject(aCallback));
+
+    if (aPromise) {
+      JS::PromiseUserInputEventHandlingState state =
+        JS::GetPromiseUserInputEventHandlingState(aPromise);
+      mPropagateUserInputEventHandling =
+        state == JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation;
+    }
   }
 
   virtual ~PromiseJobRunnable()
   {
   }
 
 protected:
   virtual void Run(AutoSlowOperation& aAso) override
   {
     JSObject* callback = mCallback->CallbackPreserveColor();
     nsIGlobalObject* global = callback ? xpc::NativeGlobal(callback) : nullptr;
     if (global && !global->IsDying()) {
+      // Propagate the user input event handling bit if needed.
+      nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global);
+      nsCOMPtr<nsIDocument> doc;
+      if (win) {
+        doc = win->GetExtantDoc();
+      }
+      AutoHandlingUserInputStatePusher userInpStatePusher(mPropagateUserInputEventHandling,
+                                                          nullptr,
+                                                          doc);
+
       mCallback->Call("promise callback");
       aAso.CheckForInterrupt();
     }
     // Now that mCallback is no longer needed, clear any pointers it contains to
     // JS GC things. This removes any storebuffer entries associated with those
     // pointers, which can cause problems by taking up memory and by triggering
     // minor GCs. This otherwise would not happen until the next minor GC or
     // cycle collection.
@@ -239,16 +259,17 @@ protected:
   {
     nsIGlobalObject* global =
       xpc::NativeGlobal(mCallback->CallbackPreserveColor());
     return global && global->IsInSyncOperation();
   }
 
 private:
   RefPtr<PromiseJobCallback> mCallback;
+  bool mPropagateUserInputEventHandling;
 };
 
 /* static */
 JSObject*
 CycleCollectedJSContext::GetIncumbentGlobalCallback(JSContext* aCx)
 {
   nsIGlobalObject* global = mozilla::dom::GetIncumbentGlobal();
   if (global) {
@@ -270,19 +291,20 @@ CycleCollectedJSContext::EnqueuePromiseJ
   MOZ_ASSERT(aCx == self->Context());
   MOZ_ASSERT(Get() == self);
 
   nsIGlobalObject* global = nullptr;
   if (aIncumbentGlobal) {
     global = xpc::NativeGlobal(aIncumbentGlobal);
   }
   JS::RootedObject jobGlobal(aCx, JS::CurrentGlobalOrNull(aCx));
-  RefPtr<MicroTaskRunnable> runnable = new PromiseJobRunnable(aJob, jobGlobal,
-                                                              aAllocationSite,
-                                                              global);
+  RefPtr<PromiseJobRunnable> runnable = new PromiseJobRunnable(aPromise, aJob,
+                                                               jobGlobal,
+                                                               aAllocationSite,
+                                                               global);
   self->DispatchToMicroTask(runnable.forget());
   return true;
 }
 
 /* static */
 void
 CycleCollectedJSContext::PromiseRejectionTrackerCallback(JSContext* aCx,
                                                          JS::HandleObject aPromise,