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
--- 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.