Bug 1434376 - Introduce ChromeOnly window.promiseDocumentFlushed to detect when refresh driver ticks have completed. r?bz draft
authorMike Conley <mconley@mozilla.com>
Sun, 11 Feb 2018 20:14:49 -0500
changeset 759486 d9c0a8de52cf4c3439cf93e8148274f1062a8bb2
parent 759477 bfe62272d2a21f9d10d45e5aaa680f5c735604ae
child 759487 483a0cd846ded2ccf85aa3a433319309484185c2
push id100366
push usermconley@mozilla.com
push dateSun, 25 Feb 2018 01:20:16 +0000
reviewersbz
bugs1434376
milestone60.0a1
Bug 1434376 - Introduce ChromeOnly window.promiseDocumentFlushed to detect when refresh driver ticks have completed. r?bz This is particularly useful for knowing when it's safe to query for style and layout information for a window without causing a synchronous style or layout flush. Note that promiseDocumentFlushed was chosen over promiseDidRefresh or promiseRefreshed to avoid potential confusion with the actual network-level refresh of browsers or documents. MozReview-Commit-ID: Am3G9yvSgdN
dom/base/nsGlobalWindowInner.cpp
dom/base/nsGlobalWindowInner.h
dom/webidl/Window.webidl
layout/style/nsStyleStruct.h
widget/SystemTimeConverter.h
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -851,16 +851,51 @@ nsGlobalWindowInner::DisableIdleCallback
 }
 
 bool
 nsGlobalWindowInner::IsBackgroundInternal() const
 {
   return !mOuterWindow || mOuterWindow->IsBackground();
 }
 
+class PromiseDocumentFlushedResolver final {
+public:
+  PromiseDocumentFlushedResolver(Promise* aPromise,
+                                 PromiseDocumentFlushedCallback& aCallback)
+  : mPromise(aPromise)
+  , mCallback(&aCallback)
+  {
+  }
+
+  virtual ~PromiseDocumentFlushedResolver() = default;
+
+  void Call()
+  {
+    MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
+
+    ErrorResult error;
+    JS::Rooted<JS::Value> returnVal(RootingCx());
+    mCallback->Call(&returnVal, error);
+
+    if (error.Failed()) {
+      mPromise->MaybeReject(error);
+    } else {
+      mPromise->MaybeResolve(returnVal);
+    }
+  }
+
+  void Cancel()
+  {
+    mPromise->MaybeReject(NS_ERROR_ABORT);
+  }
+
+  RefPtr<Promise> mPromise;
+  RefPtr<PromiseDocumentFlushedCallback> mCallback;
+};
+
 //*****************************************************************************
 //***    nsGlobalWindowInner: Object Management
 //*****************************************************************************
 
 nsGlobalWindowInner::nsGlobalWindowInner(nsGlobalWindowOuter *aOuterWindow)
   : nsPIDOMWindowInner(aOuterWindow->AsOuter()),
     mIdleFuzzFactor(0),
     mIdleCallbackIndex(-1),
@@ -884,16 +919,18 @@ nsGlobalWindowInner::nsGlobalWindowInner
     mFreezeDepth(0),
     mFocusMethod(0),
     mSerial(0),
     mIdleRequestCallbackCounter(1),
     mIdleRequestExecutor(nullptr),
     mCleanedUp(false),
     mDialogAbuseCount(0),
     mAreDialogsEnabled(true),
+    mObservingDidRefresh(false),
+    mIteratingDocumentFlushedResolvers(false),
     mCanSkipCCGeneration(0),
     mBeforeUnloadListenerCount(0)
 {
   AssertIsOnMainThread();
 
   nsLayoutStatics::AddRef();
 
   // Initialize the PRCList (this).
@@ -1288,16 +1325,23 @@ nsGlobalWindowInner::FreeInnerObjects()
     // Remember the document's principal and URI.
     mDocumentPrincipal = mDoc->NodePrincipal();
     mDocumentURI = mDoc->GetDocumentURI();
     mDocBaseURI = mDoc->GetDocBaseURI();
 
     while (mDoc->EventHandlingSuppressed()) {
       mDoc->UnsuppressEventHandlingAndFireEvents(false);
     }
+
+    if (mObservingDidRefresh) {
+      nsIPresShell* shell = mDoc->GetShell();
+      if (shell) {
+        Unused << shell->RemovePostRefreshObserver(this);
+      }
+    }
   }
 
   // Remove our reference to the document and the document principal.
   mFocusedNode = nullptr;
 
   if (mApplicationCache) {
     static_cast<nsDOMOfflineResourceList*>(mApplicationCache.get())->Disconnect();
     mApplicationCache = nullptr;
@@ -1329,16 +1373,21 @@ nsGlobalWindowInner::FreeInnerObjects()
 
   if (mTabChild) {
     // Remove any remaining listeners, and reset mBeforeUnloadListenerCount.
     for (int i = 0; i < mBeforeUnloadListenerCount; ++i) {
       mTabChild->BeforeUnloadRemoved();
     }
     mBeforeUnloadListenerCount = 0;
   }
+
+  // If we have any promiseDocumentFlushed callbacks, fire them now so
+  // that the Promises can resolve.
+  CallDocumentFlushedResolvers();
+  mObservingDidRefresh = false;
 }
 
 //*****************************************************************************
 // nsGlobalWindowInner::nsISupports
 //*****************************************************************************
 
 // QueryInterface implementation for nsGlobalWindowInner
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsGlobalWindowInner)
@@ -1484,16 +1533,22 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIntlUtils)
 
   tmp->TraverseHostObjectURIs(cb);
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChromeFields.mMessageManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChromeFields.mGroupMessageManagers)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingPromises)
+
+  for (size_t i = 0; i < tmp->mDocumentFlushedResolvers.Length(); i++) {
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentFlushedResolvers[i]->mPromise);
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentFlushedResolvers[i]->mCallback);
+  }
+
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsGlobalWindowInner)
   tmp->CleanupCachedXBLHandlers();
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mNavigator)
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPerformance)
@@ -1578,16 +1633,21 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
         tmp->mChromeFields.mMessageManager.get())->Disconnect();
       NS_IMPL_CYCLE_COLLECTION_UNLINK(mChromeFields.mMessageManager)
     }
     tmp->DisconnectAndClearGroupMessageManagers();
     NS_IMPL_CYCLE_COLLECTION_UNLINK(mChromeFields.mGroupMessageManagers)
   }
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPromises)
+  for (size_t i = 0; i < tmp->mDocumentFlushedResolvers.Length(); i++) {
+    NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentFlushedResolvers[i]->mPromise);
+    NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentFlushedResolvers[i]->mCallback);
+  }
+  tmp->mDocumentFlushedResolvers.Clear();
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 #ifdef DEBUG
 void
 nsGlobalWindowInner::RiskyUnlink()
 {
@@ -7318,16 +7378,135 @@ nsGlobalWindowInner::BeginWindowMove(Eve
   if (!mouseEvent || mouseEvent->mClass != eMouseEventClass) {
     aError.Throw(NS_ERROR_FAILURE);
     return;
   }
 
   aError = widget->BeginMoveDrag(mouseEvent);
 }
 
+already_AddRefed<Promise>
+nsGlobalWindowInner::PromiseDocumentFlushed(PromiseDocumentFlushedCallback& aCallback,
+                                            ErrorResult& aError)
+{
+  MOZ_RELEASE_ASSERT(IsChromeWindow());
+
+  if (!IsCurrentInnerWindow()) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  if (mIteratingDocumentFlushedResolvers) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  if (!mDoc) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  nsIPresShell* shell = mDoc->GetShell();
+  if (!shell) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  // We need to associate the lifetime of the Promise to the lifetime
+  // of the caller's global. That way, if the window we're observing
+  // refresh driver ticks on goes away before our observer is fired,
+  // we can still resolve the Promise.
+  nsIGlobalObject* global = GetIncumbentGlobal();
+  if (!global) {
+    aError.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  RefPtr<Promise> resultPromise = Promise::Create(global, aError);
+  if (aError.Failed()) {
+    return nullptr;
+  }
+
+  UniquePtr<PromiseDocumentFlushedResolver> flushResolver(
+    new PromiseDocumentFlushedResolver(resultPromise, aCallback));
+
+  if (!shell->NeedFlush(FlushType::Style)) {
+    flushResolver->Call();
+    return resultPromise.forget();
+  }
+
+  if (!mObservingDidRefresh) {
+    bool success = shell->AddPostRefreshObserver(this);
+    if (!success) {
+      aError.Throw(NS_ERROR_FAILURE);
+      return nullptr;
+    }
+    mObservingDidRefresh = true;
+  }
+
+  mDocumentFlushedResolvers.AppendElement(Move(flushResolver));
+  return resultPromise.forget();
+}
+
+void
+nsGlobalWindowInner::CallDocumentFlushedResolvers()
+{
+  MOZ_ASSERT(!mIteratingDocumentFlushedResolvers);
+  mIteratingDocumentFlushedResolvers = true;
+  for (const auto& documentFlushedResolver : mDocumentFlushedResolvers) {
+    documentFlushedResolver->Call();
+  }
+  mDocumentFlushedResolvers.Clear();
+  mIteratingDocumentFlushedResolvers = false;
+}
+
+void
+nsGlobalWindowInner::CancelDocumentFlushedResolvers()
+{
+  MOZ_ASSERT(!mIteratingDocumentFlushedResolvers);
+  mIteratingDocumentFlushedResolvers = true;
+  for (const auto& documentFlushedResolver : mDocumentFlushedResolvers) {
+    documentFlushedResolver->Cancel();
+  }
+  mDocumentFlushedResolvers.Clear();
+  mIteratingDocumentFlushedResolvers = false;
+}
+
+void
+nsGlobalWindowInner::DidRefresh()
+{
+  auto rejectionGuard = MakeScopeExit([&] {
+    CancelDocumentFlushedResolvers();
+    mObservingDidRefresh = false;
+  });
+
+  MOZ_ASSERT(mDoc);
+
+  nsIPresShell* shell = mDoc->GetShell();
+  MOZ_ASSERT(shell);
+
+  if (shell->NeedStyleFlush() || shell->HasPendingReflow()) {
+    // By the time our observer fired, something has already invalidated
+    // style and maybe layout. We'll wait until the next refresh driver
+    // tick instead.
+    rejectionGuard.release();
+    return;
+  }
+
+  bool success = shell->RemovePostRefreshObserver(this);
+  if (!success) {
+    return;
+  }
+
+  rejectionGuard.release();
+
+  CallDocumentFlushedResolvers();
+  mObservingDidRefresh = false;
+}
+
 already_AddRefed<nsWindowRoot>
 nsGlobalWindowInner::GetWindowRoot(mozilla::ErrorResult& aError)
 {
   FORWARD_TO_OUTER_OR_THROW(GetWindowRootOuter, (), aError, nullptr);
 }
 
 void
 nsGlobalWindowInner::SetCursor(const nsAString& aCursor, ErrorResult& aError)
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -52,16 +52,17 @@
 #include "mozilla/dom/EventTarget.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "Units.h"
 #include "nsComponentManagerUtils.h"
 #include "nsSize.h"
 #include "nsCheapSets.h"
 #include "mozilla/dom/ImageBitmapSource.h"
 #include "mozilla/UniquePtr.h"
+#include "nsRefreshDriver.h"
 
 class nsIArray;
 class nsIBaseWindow;
 class nsIContent;
 class nsICSSDeclaration;
 class nsIDocShellTreeOwner;
 class nsIDOMOfflineResourceList;
 class nsIScrollableFrame;
@@ -84,16 +85,18 @@ class nsIIdleService;
 struct nsRect;
 
 class nsWindowSizes;
 
 class IdleRequestExecutor;
 
 class DialogValueHolder;
 
+class PromiseDocumentFlushedResolver;
+
 namespace mozilla {
 class AbstractThread;
 class ThrottledEventQueue;
 namespace dom {
 class BarProp;
 struct ChannelPixelLayout;
 class ClientSource;
 class Console;
@@ -206,17 +209,18 @@ class nsGlobalWindowInner : public mozil
                             private nsIDOMWindow,
                             // NOTE: This interface is private, as it's only
                             // implemented on chrome windows.
                             private nsIDOMChromeWindow,
                             public nsIScriptGlobalObject,
                             public nsIScriptObjectPrincipal,
                             public nsSupportsWeakReference,
                             public nsIInterfaceRequestor,
-                            public PRCListStr
+                            public PRCListStr,
+                            public nsAPostRefreshObserver
 {
 public:
   typedef mozilla::TimeStamp TimeStamp;
   typedef mozilla::TimeDuration TimeDuration;
 
   typedef nsDataHashtable<nsUint64HashKey, nsGlobalWindowInner*> InnerWindowByIdTable;
 
   static void
@@ -939,16 +943,22 @@ public:
                                  mozilla::ErrorResult& aError);
   nsIMessageBroadcaster* GetMessageManager(mozilla::ErrorResult& aError);
   nsIMessageBroadcaster* GetGroupMessageManager(const nsAString& aGroup,
                                                 mozilla::ErrorResult& aError);
   void BeginWindowMove(mozilla::dom::Event& aMouseDownEvent,
                        mozilla::dom::Element* aPanel,
                        mozilla::ErrorResult& aError);
 
+  already_AddRefed<mozilla::dom::Promise>
+  PromiseDocumentFlushed(mozilla::dom::PromiseDocumentFlushedCallback& aCallback,
+                         mozilla::ErrorResult& aError);
+
+  void DidRefresh() override;
+
   void GetDialogArgumentsOuter(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval,
                                nsIPrincipal& aSubjectPrincipal,
                                mozilla::ErrorResult& aError);
   void GetDialogArguments(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval,
                           nsIPrincipal& aSubjectPrincipal,
                           mozilla::ErrorResult& aError);
   void GetReturnValueOuter(JSContext* aCx, JS::MutableHandle<JS::Value> aReturnValue,
                            nsIPrincipal& aSubjectPrincipal,
@@ -1264,16 +1274,19 @@ private:
       nsIMessageBroadcaster* mm = iter.UserData();
       if (mm) {
         static_cast<nsFrameMessageManager*>(mm)->Disconnect();
       }
     }
     mChromeFields.mGroupMessageManagers.Clear();
   }
 
+  void CallDocumentFlushedResolvers();
+  void CancelDocumentFlushedResolvers();
+
 public:
   // Dispatch a runnable related to the global.
   virtual nsresult Dispatch(mozilla::TaskCategory aCategory,
                             already_AddRefed<nsIRunnable>&& aRunnable) override;
 
   virtual nsISerialEventTarget*
   EventTargetFor(mozilla::TaskCategory aCategory) const override;
 
@@ -1410,16 +1423,24 @@ protected:
   // dom.successive_dialog_time_limit, we show a checkbox or confirmation prompt
   // to allow disabling of further dialogs from this window.
   TimeStamp                     mLastDialogQuitTime;
 
   // This flag keeps track of whether dialogs are
   // currently enabled on this window.
   bool                          mAreDialogsEnabled;
 
+  // This flag keeps track of whether this window is currently
+  // observing DidRefresh notifications from the refresh driver.
+  bool                          mObservingDidRefresh;
+  // This flag keeps track of whether or not we're going through
+  // promiseDocumentFlushed resolvers. When true, promiseDocumentFlushed
+  // cannot be called.
+  bool                          mIteratingDocumentFlushedResolvers;
+
   nsTArray<uint32_t> mEnabledSensors;
 
 #if defined(MOZ_WIDGET_ANDROID)
   mozilla::UniquePtr<mozilla::dom::WindowOrientationObserver> mOrientationChangeObserver;
 #endif
 
 #ifdef MOZ_WEBSPEECH
   RefPtr<mozilla::dom::SpeechSynthesis> mSpeechSynthesis;
@@ -1436,16 +1457,18 @@ protected:
   int64_t mBeforeUnloadListenerCount;
 
   RefPtr<mozilla::dom::IntlUtils> mIntlUtils;
 
   mozilla::UniquePtr<mozilla::dom::ClientSource> mClientSource;
 
   nsTArray<RefPtr<mozilla::dom::Promise>> mPendingPromises;
 
+  nsTArray<mozilla::UniquePtr<PromiseDocumentFlushedResolver>> mDocumentFlushedResolvers;
+
   static InnerWindowByIdTable* sInnerWindowsById;
 
   // Members in the mChromeFields member should only be used in chrome windows.
   // All accesses to this field should be guarded by a check of mIsChrome.
   struct ChromeFields {
     ChromeFields()
       : mGroupMessageManagers(1)
     {}
--- a/dom/webidl/Window.webidl
+++ b/dom/webidl/Window.webidl
@@ -367,16 +367,18 @@ partial interface Window {
 #ifdef HAVE_SIDEBAR
 // Mozilla extension
 partial interface Window {
   [Replaceable, Throws, UseCounter]
   readonly attribute (External or WindowProxy) sidebar;
 };
 #endif
 
+callback PromiseDocumentFlushedCallback = any ();
+
 // Mozilla extensions for Chrome windows.
 partial interface Window {
   // The STATE_* constants need to match the corresponding enum in nsGlobalWindow.cpp.
   [Func="nsGlobalWindowInner::IsPrivilegedChromeWindow"]
   const unsigned short STATE_MAXIMIZED = 1;
   [Func="nsGlobalWindowInner::IsPrivilegedChromeWindow"]
   const unsigned short STATE_MINIMIZED = 2;
   [Func="nsGlobalWindowInner::IsPrivilegedChromeWindow"]
@@ -439,16 +441,57 @@ partial interface Window {
    *
    * The optional panel argument should be set when moving a panel.
    *
    * Throws NS_ERROR_NOT_IMPLEMENTED if the OS doesn't support this.
    */
   [Throws, Func="nsGlobalWindowInner::IsPrivilegedChromeWindow"]
   void beginWindowMove(Event mouseDownEvent, optional Element? panel = null);
 
+  /**
+   * Calls the given function as soon as a style or layout flush for the
+   * top-level document is not necessary, and returns a Promise which
+   * resolves to the callback's return value after it executes.
+   *
+   * In the event that the window goes away before a flush can occur, the
+   * callback will still be called and the Promise resolved as the window
+   * tears itself down.
+   *
+   * Note that the callback can be called either synchronously or asynchronously
+   * depending on whether or not flushes are pending:
+   *
+   *   The callback will be called synchronously when calling
+   *   promiseDocumentFlushed when NO flushes are already pending. This is
+   *   to ensure that no script has a chance to dirty the DOM before the callback
+   *   is called.
+   *
+   *   The callback will be called asynchronously if a flush is pending.
+   *
+   * The expected execution order is that all pending callbacks will
+   * be fired first (and in the order that they were queued) and then the
+   * Promise resolution handlers will all be invoked later on during the
+   * next microtask checkpoint.
+   *
+   * promiseDocumentFlushed does not support re-entrancy - so calling it from
+   * within a promiseDocumentFlushed callback will result in the inner call
+   * throwing an NS_ERROR_FAILURE exception, and the outer Promise rejecting
+   * with that exception.
+   *
+   * The callback function *must not make any changes which would require
+   * a style or layout flush*.
+   *
+   * Also throws NS_ERROR_FAILURE if the window is not in a state where flushes
+   * can be waited for (for example, the PresShell has not yet been created).
+   *
+   * @param {function} callback
+   * @returns {Promise}
+   */
+  [Throws, Func="nsGlobalWindowInner::IsPrivilegedChromeWindow"]
+  Promise<any> promiseDocumentFlushed(PromiseDocumentFlushedCallback callback);
+
   [Func="IsChromeOrXBL"]
   readonly attribute boolean isChromeWindow;
 };
 
 partial interface Window {
   [Pref="dom.vr.enabled"]
   attribute EventHandler onvrdisplayconnect;
   [Pref="dom.vr.enabled"]
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -2325,23 +2325,23 @@ public:
 
   StyleFillRule GetFillRule() const { return mFillRule; }
   void SetFillRule(StyleFillRule aFillRule)
   {
     MOZ_ASSERT(mType == StyleBasicShapeType::Polygon, "expected polygon");
     mFillRule = aFillRule;
   }
 
-  Position& GetPosition() {
+  mozilla::Position& GetPosition() {
     MOZ_ASSERT(mType == StyleBasicShapeType::Circle ||
                mType == StyleBasicShapeType::Ellipse,
                "expected circle or ellipse");
     return mPosition;
   }
-  const Position& GetPosition() const {
+  const mozilla::Position& GetPosition() const {
     MOZ_ASSERT(mType == StyleBasicShapeType::Circle ||
                mType == StyleBasicShapeType::Ellipse,
                "expected circle or ellipse");
     return mPosition;
   }
 
   bool HasRadius() const {
     MOZ_ASSERT(mType == StyleBasicShapeType::Inset, "expected inset");
@@ -2391,17 +2391,17 @@ private:
   StyleBasicShapeType mType;
   StyleFillRule mFillRule;
 
   // mCoordinates has coordinates for polygon or radii for
   // ellipse and circle.
   // (top, right, bottom, left) for inset
   nsTArray<nsStyleCoord> mCoordinates;
   // position of center for ellipse or circle
-  Position mPosition;
+  mozilla::Position mPosition;
   // corner radii for inset (0 if not set)
   nsStyleCorners mRadius;
 };
 
 struct StyleShapeSource final
 {
   StyleShapeSource() = default;
 
--- a/widget/SystemTimeConverter.h
+++ b/widget/SystemTimeConverter.h
@@ -5,16 +5,22 @@
 
 #ifndef SystemTimeConverter_h
 #define SystemTimeConverter_h
 
 #include <limits>
 #include "mozilla/TimeStamp.h"
 #include "mozilla/TypeTraits.h"
 
+// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
+// GetTickCount().
+#ifdef GetCurrentTime
+#undef GetCurrentTime
+#endif
+
 namespace mozilla {
 
 // Utility class that converts time values represented as an unsigned integral
 // number of milliseconds from one time source (e.g. a native event time) to
 // corresponding mozilla::TimeStamp objects.
 //
 // This class handles wrapping of integer values and skew between the time
 // source and mozilla::TimeStamp values.