Bug 1830820 - Introduce some delays to user input handling r=smaug

In some edge cases, we may start handling user input events that
are not supposed to be handled by the current document.

This patch introduces some delay to handle user input events.

Differential Revision: https://phabricator.services.mozilla.com/D180976
This commit is contained in:
Sean Feng
2023-09-05 18:28:52 +00:00
parent 3b1f719e3e
commit a682aea259
11 changed files with 166 additions and 7 deletions

View File

@@ -1314,6 +1314,7 @@ void nsDocShell::FirePageHideShowNonRecursive(bool aShow) {
mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : nullptr;
if (mBrowsingContext->IsTop()) {
doc->NotifyPossibleTitleChange(false);
doc->SetLoadingOrRestoredFromBFCacheTimeStampToNow();
if (inner) {
// Now that we have found the inner window of the page restored
// from the history, we have to make sure that

View File

@@ -12220,7 +12220,7 @@ void Document::SetReadyStateInternal(ReadyState aReadyState,
}
if (aUpdateTimingInformation && READYSTATE_LOADING == aReadyState) {
mLoadingTimeStamp = TimeStamp::Now();
SetLoadingOrRestoredFromBFCacheTimeStampToNow();
}
NotifyLoading(mAncestorIsLoading, mReadyState, aReadyState);
mReadyState = aReadyState;
@@ -13564,8 +13564,9 @@ nsresult Document::GetStateObject(JS::MutableHandle<JS::Value> aState) {
void Document::SetNavigationTiming(nsDOMNavigationTiming* aTiming) {
mTiming = aTiming;
if (!mLoadingTimeStamp.IsNull() && mTiming) {
mTiming->SetDOMLoadingTimeStamp(GetDocumentURI(), mLoadingTimeStamp);
if (!mLoadingOrRestoredFromBFCacheTimeStamp.IsNull() && mTiming) {
mTiming->SetDOMLoadingTimeStamp(GetDocumentURI(),
mLoadingOrRestoredFromBFCacheTimeStamp);
}
// If there's already the DocumentTimeline instance, tell it since the

View File

@@ -1045,6 +1045,13 @@ class Document : public nsINode,
void SetLoadedAsData(bool aLoadedAsData, bool aConsiderForMemoryReporting);
TimeStamp GetLoadingOrRestoredFromBFCacheTimeStamp() const {
return mLoadingOrRestoredFromBFCacheTimeStamp;
}
void SetLoadingOrRestoredFromBFCacheTimeStampToNow() {
mLoadingOrRestoredFromBFCacheTimeStamp = TimeStamp::Now();
}
/**
* Normally we assert if a runnable labeled with one DocGroup touches data
* from another DocGroup. Calling IgnoreDocGroupMismatches() on a document
@@ -5124,8 +5131,9 @@ class Document : public nsINode,
RefPtr<nsDOMNavigationTiming> mTiming;
// Recorded time of change to 'loading' state.
TimeStamp mLoadingTimeStamp;
// Recorded time of change to 'loading' state
// or time of the page gets restored from BFCache.
TimeStamp mLoadingOrRestoredFromBFCacheTimeStamp;
// Decided to use nsTObserverArray because it allows us to
// remove candidates while iterating them and this is what

View File

@@ -1983,6 +1983,26 @@ bool PresShell::SimpleResizeReflow(nscoord aWidth, nscoord aHeight) {
return true;
}
bool PresShell::CanHandleUserInputEvents(WidgetGUIEvent* aGUIEvent) {
if (XRE_IsParentProcess()) {
return true;
}
if (aGUIEvent->mFlags.mIsSynthesizedForTests) {
return true;
}
if (!aGUIEvent->IsUserAction()) {
return true;
}
if (nsPresContext* rootPresContext = mPresContext->GetRootPresContext()) {
return rootPresContext->UserInputEventsAllowed();
}
return true;
}
void PresShell::AddResizeEventFlushObserverIfNeeded() {
if (!mIsDestroying && !mResizeEventPending &&
MOZ_LIKELY(!mDocument->GetBFCacheEntry())) {
@@ -6882,6 +6902,17 @@ nsresult PresShell::HandleEvent(nsIFrame* aFrameForPresShell,
aGUIEvent->AsMouseEvent()->mReason == WidgetMouseEvent::eSynthesized) {
return NS_OK;
}
// Here we are granting some delays to ensure that user input events are
// created while the page content may not be visible to the user are not
// processed.
// The main purpose of this is to avoid user inputs are handled in the
// new document where as the user inputs were originally targeting some
// content in the old document.
if (!CanHandleUserInputEvents(aGUIEvent)) {
return NS_OK;
}
EventHandler eventHandler(*this);
return eventHandler.HandleEvent(aFrameForPresShell, aGUIEvent,
aDontRetargetEvents, aEventStatus);
@@ -9304,6 +9335,10 @@ void PresShell::Freeze(bool aIncludeSubDocuments) {
if (presContext->RefreshDriver()->GetPresContext() == presContext) {
presContext->RefreshDriver()->Freeze();
}
if (nsPresContext* rootPresContext = presContext->GetRootPresContext()) {
rootPresContext->ResetUserInputEventsAllowed();
}
}
mFrozen = true;
@@ -9362,6 +9397,16 @@ void PresShell::Thaw(bool aIncludeSubDocuments) {
UpdateImageLockingState();
UnsuppressPainting();
// In case the above UnsuppressPainting call didn't start the
// refresh driver, we manually start the refresh driver to
// ensure nsPresContext::MaybeIncreaseMeasuredTicksSinceLoading
// can be called for user input events handling.
if (presContext && presContext->IsRoot()) {
if (!presContext->RefreshDriver()->HasPendingTick()) {
presContext->RefreshDriver()->InitializeTimer();
}
}
}
//--------------------------------------------------------

View File

@@ -385,6 +385,8 @@ class PresShell final : public nsStubDocumentObserver,
*/
bool SimpleResizeReflow(nscoord aWidth, nscoord aHeight);
bool CanHandleUserInputEvents(WidgetGUIEvent* aGUIEvent);
public:
/**
* Updates pending layout, assuming reasonable (up-to-date, or mid-update for

View File

@@ -249,6 +249,7 @@ nsPresContext::nsPresContext(dom::Document* aDocument, nsPresContextType aType)
mAnimationTriggeredRestyles(0),
mInterruptChecksToSkip(0),
mNextFrameRateMultiplier(0),
mMeasuredTicksSinceLoading(0),
mViewportScrollStyles(StyleOverflow::Auto, StyleOverflow::Auto),
// mImageAnimationMode is initialised below, in constructor body
mImageAnimationModePref(imgIContainer::kNormalAnimMode),
@@ -285,6 +286,7 @@ nsPresContext::nsPresContext(dom::Document* aDocument, nsPresContextType aType)
mHadFirstContentfulPaint(false),
mHadNonTickContentfulPaint(false),
mHadContentfulPaintComposite(false),
mUserInputEventsAllowed(false),
#ifdef DEBUG
mInitialized(false),
#endif
@@ -1236,6 +1238,69 @@ nsRootPresContext* nsPresContext::GetRootPresContext() const {
return pc->IsRoot() ? static_cast<nsRootPresContext*>(pc) : nullptr;
}
bool nsPresContext::UserInputEventsAllowed() {
MOZ_ASSERT(IsRoot());
if (mUserInputEventsAllowed) {
return true;
}
// Special document
if (Document()->IsInitialDocument()) {
return true;
}
if (mMeasuredTicksSinceLoading <
StaticPrefs::dom_input_events_security_minNumTicks()) {
return false;
}
if (!StaticPrefs::dom_input_events_security_minTimeElapsedInMS()) {
return true;
}
dom::Document* doc = Document();
MOZ_ASSERT_IF(StaticPrefs::dom_input_events_security_minNumTicks(),
doc->GetReadyStateEnum() >= Document::READYSTATE_LOADING);
TimeStamp loadingOrRestoredFromBFCacheTime =
doc->GetLoadingOrRestoredFromBFCacheTimeStamp();
MOZ_ASSERT(!loadingOrRestoredFromBFCacheTime.IsNull());
TimeDuration elapsed = TimeStamp::Now() - loadingOrRestoredFromBFCacheTime;
if (elapsed.ToMilliseconds() >=
StaticPrefs::dom_input_events_security_minTimeElapsedInMS()) {
mUserInputEventsAllowed = true;
return true;
}
return false;
}
void nsPresContext::MaybeIncreaseMeasuredTicksSinceLoading() {
MOZ_ASSERT(IsRoot());
if (mMeasuredTicksSinceLoading >=
StaticPrefs::dom_input_events_security_minNumTicks()) {
return;
}
// We consider READYSTATE_LOADING is the point when the page
// becomes interactive
if (Document()->GetReadyStateEnum() >= Document::READYSTATE_LOADING ||
Document()->IsInitialDocument()) {
++mMeasuredTicksSinceLoading;
}
if (mMeasuredTicksSinceLoading <
StaticPrefs::dom_input_events_security_minNumTicks()) {
// Here we are forcing refresh driver to run because we can't always
// guarantee refresh driver will run enough times to meet the minNumTicks
// requirement. i.e. about:blank.
if (!RefreshDriver()->HasPendingTick()) {
RefreshDriver()->InitializeTimer();
}
}
}
// Helper function for setting Anim Mode on image
static void SetImgAnimModeOnImgReq(imgIRequest* aImgReq, uint16_t aMode) {
if (aImgReq) {

View File

@@ -257,7 +257,7 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
*/
nsRootPresContext* GetRootPresContext() const;
virtual bool IsRoot() { return false; }
virtual bool IsRoot() const { return false; }
mozilla::dom::Document* Document() const {
#ifdef DEBUG
@@ -512,6 +512,16 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
nsDeviceContext* DeviceContext() const { return mDeviceContext; }
mozilla::EventStateManager* EventStateManager() { return mEventManager; }
bool UserInputEventsAllowed();
void MaybeIncreaseMeasuredTicksSinceLoading();
void ResetUserInputEventsAllowed() {
MOZ_ASSERT(IsRoot());
mMeasuredTicksSinceLoading = 0;
mUserInputEventsAllowed = false;
}
// Get the text zoom factor in use.
float TextZoom() const { return mTextZoom; }
@@ -1254,6 +1264,8 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
// During page load we use slower frame rate.
uint32_t mNextFrameRateMultiplier;
uint32_t mMeasuredTicksSinceLoading;
nsTArray<RefPtr<mozilla::ManagedPostRefreshObserver>>
mManagedPostRefreshObservers;
@@ -1347,6 +1359,7 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
// Has NotifyDidPaintForSubtree been called for a contentful paint?
unsigned mHadContentfulPaintComposite : 1;
unsigned mUserInputEventsAllowed : 1;
#ifdef DEBUG
unsigned mInitialized : 1;
#endif
@@ -1379,7 +1392,7 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
class nsRootPresContext final : public nsPresContext {
public:
nsRootPresContext(mozilla::dom::Document* aDocument, nsPresContextType aType);
virtual bool IsRoot() override { return true; }
virtual bool IsRoot() const override { return true; }
/**
* Add a runnable that will get called before the next paint. They will get

View File

@@ -2248,6 +2248,12 @@ void nsRefreshDriver::FlushAutoFocusDocuments() {
}
}
void nsRefreshDriver::MaybeIncreaseMeasuredTicksSinceLoading() {
if (mPresContext && mPresContext->IsRoot()) {
mPresContext->MaybeIncreaseMeasuredTicksSinceLoading();
}
}
void nsRefreshDriver::CancelFlushAutoFocus(Document* aDocument) {
mAutoFocusFlushDocuments.RemoveElement(aDocument);
}
@@ -2693,6 +2699,7 @@ void nsRefreshDriver::Tick(VsyncId aId, TimeStamp aNowTime,
DispatchAnimationEvents();
RunFullscreenSteps();
RunFrameRequestCallbacks(aNowTime);
MaybeIncreaseMeasuredTicksSinceLoading();
if (mPresContext && mPresContext->GetPresShell()) {
AutoTArray<PresShell*, 16> observers;

View File

@@ -492,6 +492,7 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
void RunFrameRequestCallbacks(mozilla::TimeStamp aNowTime);
void UpdateIntersectionObservations(mozilla::TimeStamp aNowTime);
void UpdateRelevancyOfContentVisibilityAutoFrames();
void MaybeIncreaseMeasuredTicksSinceLoading();
void EvaluateMediaQueriesAndReportChanges();
enum class IsExtraTick {

View File

@@ -2799,6 +2799,20 @@
value: false
mirror: always
# The minimum number of ticks after page navigation
# that need to occur before user input events are allowed to be handled.
- name: dom.input_events.security.minNumTicks
type: uint32_t
value: 3
mirror: always
# The minimum elapsed time (in milliseconds) after page navigation
# for user input events are allowed to be handled.
- name: dom.input_events.security.minTimeElapsedInMS
type: uint32_t
value: 100
mirror: always
# The maximum time (milliseconds) we reserve for handling input events in each
# frame.
- name: dom.input_event_queue.duration.max

View File

@@ -6,3 +6,5 @@
/* globals user_pref */
// ensure webrender is set (and we don't need MOZ_WEBRENDER env variable)
user_pref("gfx.webrender.all", true);
user_pref("dom.input_events.security.minNumTicks", 0);
user_pref("dom.input_events.security.minTimeElapsedInMS", 0);