Bug 1592474 - Add some heuristics to disable scroll anchoring in pathological cases. r=dholbert

The idea of these are not to penalize legit uses of scroll anchoring, and
catching pathological cases fast.

The current algorithm I thought of is just whether the average of all the
consecutive scroll anchoring adjustments is less than a given threshold.

If the average adjustment is close to zero and the user is not scrolling, it
means that we're not making much progress.

It is important that zero adjustments don't get counted, since those are common
during window resizes and don't have side-effects anyway.

Exact number may need tuning, let me know if you want it
nightly-and-early-beta-only for now or something.

Depends on D51038

Differential Revision: https://phabricator.services.mozilla.com/D51024

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Emilio Cobos Álvarez 2019-10-31 09:25:08 +00:00
Родитель 4510f3e34a
Коммит f70cc8c005
3 изменённых файлов: 113 добавлений и 15 удалений

Просмотреть файл

@ -23,15 +23,15 @@ using namespace mozilla::dom;
#define ANCHOR_LOG(...)
/*
#define ANCHOR_LOG(fmt, ...) \
printf_stderr("ANCHOR(%p, %s): " fmt, this, \
Frame() \
->PresContext() \
->Document() \
->GetDocumentURI() \
->GetSpecOrDefault() \
.get(), \
##__VA_ARGS__)
#define ANCHOR_LOG(fmt, ...) \
printf_stderr("ANCHOR(%p, %s, root: %d): " fmt, this, \
Frame() \
->PresContext() \
->Document() \
->GetDocumentURI() \
->GetSpecOrDefault() \
.get(), \
mScrollFrame->mIsRoot, ##__VA_ARGS__)
*/
namespace mozilla {
@ -41,6 +41,7 @@ ScrollAnchorContainer::ScrollAnchorContainer(ScrollFrameHelper* aScrollFrame)
: mScrollFrame(aScrollFrame),
mAnchorNode(nullptr),
mLastAnchorOffset(0),
mDisabled(false),
mAnchorNodeIsDirty(true),
mApplyingAnchorAdjustment(false),
mSuppressAnchorAdjustment(false) {}
@ -199,7 +200,7 @@ void ScrollAnchorContainer::SelectAnchor() {
MOZ_ASSERT(mScrollFrame->mScrolledFrame);
MOZ_ASSERT(mAnchorNodeIsDirty);
if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
if (mDisabled || !StaticPrefs::layout_css_scroll_anchoring_enabled()) {
return;
}
@ -285,6 +286,56 @@ void ScrollAnchorContainer::UserScrolled() {
return;
}
InvalidateAnchor();
mConsecutiveScrollAnchoringAdjustments = SaturateUint32(0);
mConsecutiveScrollAnchoringAdjustmentLength = 0;
}
void ScrollAnchorContainer::AdjustmentMade(nscoord aAdjustment) {
// A reasonably large number of times that we want to check for this. If we
// haven't hit this limit after these many attempts we assume we'll never hit
// it.
//
// This is to prevent the number getting too large and making the limit round
// to zero by mere precision error.
//
// 100k should be enough for anyone :)
static const uint32_t kAnchorCheckCountLimit = 100000;
// Zero-length adjustments are common & don't have side effects, so we don't
// want them to consider them here; they'd bias our average towards 0.
MOZ_ASSERT(aAdjustment, "Don't call this API for zero-length adjustments");
mConsecutiveScrollAnchoringAdjustments++;
mConsecutiveScrollAnchoringAdjustmentLength = NSCoordSaturatingAdd(
mConsecutiveScrollAnchoringAdjustmentLength, aAdjustment);
uint32_t maxConsecutiveAdjustments =
StaticPrefs::layout_css_scroll_anchoring_max_consecutive_adjustments();
if (!maxConsecutiveAdjustments) {
return;
}
uint32_t consecutiveAdjustments =
mConsecutiveScrollAnchoringAdjustments.value();
if (consecutiveAdjustments < maxConsecutiveAdjustments ||
consecutiveAdjustments > kAnchorCheckCountLimit) {
return;
}
auto cssPixels =
CSSPixel::FromAppUnits(mConsecutiveScrollAnchoringAdjustmentLength);
double average = double(cssPixels) / consecutiveAdjustments;
uint32_t minAverage = StaticPrefs::
layout_css_scroll_anchoring_min_average_adjustment_threshold();
if (MOZ_UNLIKELY(std::abs(average) < double(minAverage))) {
ANCHOR_LOG(
"Disabled scroll anchoring for container: "
"%f average, %f total out of %u consecutive adjustments\n",
average, float(cssPixels), mConsecutiveScrollAnchoringAdjustments);
mDisabled = true;
}
}
void ScrollAnchorContainer::SuppressAdjustments() {
@ -302,7 +353,7 @@ void ScrollAnchorContainer::InvalidateAnchor(ScheduleSelection aSchedule) {
mAnchorNodeIsDirty = true;
mLastAnchorOffset = 0;
if (aSchedule == ScheduleSelection::No ||
if (mDisabled || aSchedule == ScheduleSelection::No ||
!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
return;
}
@ -315,14 +366,15 @@ void ScrollAnchorContainer::Destroy() {
}
void ScrollAnchorContainer::ApplyAdjustments() {
if (!mAnchorNode || mAnchorNodeIsDirty ||
if (!mAnchorNode || mAnchorNodeIsDirty || mDisabled ||
mScrollFrame->HasPendingScrollRestoration() ||
mScrollFrame->IsProcessingScrollEvent() ||
mScrollFrame->IsProcessingAsyncScroll()) {
ANCHOR_LOG(
"Ignoring post-reflow (anchor=%p, dirty=%d, pendingRestoration=%d, "
"scrollevent=%d asyncScroll=%d pendingSuppression=%d container=%p).\n",
mAnchorNode, mAnchorNodeIsDirty,
"Ignoring post-reflow (anchor=%p, dirty=%d, disabled=%d, "
"pendingRestoration=%d, scrollevent=%d, asyncScroll=%d, "
"pendingSuppression=%d, container=%p).\n",
mAnchorNode, mAnchorNodeIsDirty, mDisabled,
mScrollFrame->HasPendingScrollRestoration(),
mScrollFrame->IsProcessingScrollEvent(),
mScrollFrame->IsProcessingAsyncScroll(), mSuppressAnchorAdjustment,
@ -357,6 +409,8 @@ void ScrollAnchorContainer::ApplyAdjustments() {
ANCHOR_LOG("Applying anchor adjustment of %d in %s with anchor %p.\n",
logicalAdjustment, writingMode.DebugString(), mAnchorNode);
AdjustmentMade(logicalAdjustment);
nsPoint physicalAdjustment;
switch (writingMode.GetBlockDir()) {
case WritingMode::eBlockTB: {

Просмотреть файл

@ -8,6 +8,7 @@
#define mozilla_layout_ScrollAnchorContainer_h_
#include "nsPoint.h"
#include "mozilla/Saturate.h"
class nsFrameList;
class nsIFrame;
@ -129,6 +130,10 @@ class ScrollAnchorContainer final {
// anchor node, if one was found in this child list, or null otherwise.
nsIFrame* FindAnchorInList(const nsFrameList& aFrameList) const;
// Notes that a given adjustment has happened, and maybe disables scroll
// anchoring on this scroller altogether based on various prefs.
void AdjustmentMade(nscoord aAdjustment);
// The owner of this scroll anchor container
ScrollFrameHelper* mScrollFrame;
@ -143,6 +148,20 @@ class ScrollAnchorContainer final {
// the anchor node in the same relative position
nscoord mLastAnchorOffset;
// The number of consecutive scroll anchoring adjustments that have happened
// without a user scroll.
SaturateUint32 mConsecutiveScrollAnchoringAdjustments{0};
// The total length that has been adjusted by all the consecutive adjustments
// referenced above. Note that this is a sum, so that oscillating adjustments
// average towards zero.
nscoord mConsecutiveScrollAnchoringAdjustmentLength{0};
// True if we've been disabled by the heuristic controlled by
// layout.css.scroll-anchoring.max-consecutive-adjustments and
// layout.css.scroll-anchoring.min-adjustment-threshold.
bool mDisabled : 1;
// True if we should recalculate our anchor node at the next chance
bool mAnchorNodeIsDirty : 1;
// True if we are applying a scroll anchor adjustment

Просмотреть файл

@ -5266,6 +5266,31 @@
value: true
mirror: always
# Pref to control how many consecutive scroll-anchoring adjustments (since the
# most recent user scroll) we'll average, before we consider whether to
# automatically turn off scroll anchoring. When we hit this threshold, the
# actual decision to disable also depends on the
# min-average-adjustment-threshold pref, see below for more details.
#
# Zero disables the heuristic.
- name: layout.css.scroll-anchoring.max-consecutive-adjustments
type: uint32_t
value: 10
mirror: always
# Pref to control whether we should disable scroll anchoring on a scroller
# where at least max-consecutive-adjustments have happened, and which the
# average adjustment ends up being less than this number, in CSS pixels.
#
# So, for example, given max-consecutive-adjustments=10 and
# min-average-adjustment-treshold=3, we'll block scroll anchoring if there have
# been 10 consecutive adjustments without a user scroll or more, and the
# average offset difference between them amount to less than 3 CSS pixels.
- name: layout.css.scroll-anchoring.min-average-adjustment-threshold
type: uint32_t
value: 3
mirror: always
# Pref to control disabling scroll anchoring suppression triggers, see
#
# https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers