Programmatic scrolling and content changes are not always synchronized
https://bugs.webkit.org/show_bug.cgi?id=139245
rdar://problem/18833612
Reviewed by Anders Carlsson.
Manual test that tries to sync layout with programmatic scrolling.
* ManualTests/programmatic-scroll-flicker.html: Added.
Source/WebCore:
For programmatic scrolls, AsyncScrollingCoordinator::requestScrollPositionUpdate()
calls updateScrollPositionAfterAsyncScroll(), then dispatches the requested
scroll position to the scrolling thread.
Once the scrolling thread commits, it calls back to the main thread via
scheduleUpdateScrollPositionAfterAsyncScroll(), which schedules a second
call to updateScrollPositionAfterAsyncScroll() on a timer. That's a problem,
because some other scroll may have happened in the meantime; when the timer
fires, it can sometimes restore a stale scroll position.
Fix by bailing early from scheduleUpdateScrollPositionAfterAsyncScroll()
for programmatic scrolls, since we know that requestScrollPositionUpdate()
already did the updateScrollPositionAfterAsyncScroll().
Test:
ManualTests/programmatic-scroll-flicker.html
* page/FrameView.cpp:
(WebCore::FrameView::reset): nullptr.
(WebCore::FrameView::setScrollPosition): Ditto.
(WebCore::FrameView::setWasScrolledByUser): Ditto.
* page/scrolling/AsyncScrollingCoordinator.cpp:
(WebCore::AsyncScrollingCoordinator::requestScrollPositionUpdate): Use a local variable for
isProgrammaticScroll just to make sure we use the same value for the duration of this function.
(WebCore::AsyncScrollingCoordinator::scheduleUpdateScrollPositionAfterAsyncScroll): Do nothing
if this is a programmatic scroll.
git-svn-id: http://svn.webkit.org/repository/webkit/trunk@176899 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/ChangeLog b/ChangeLog
index 7d785f0..e5b5ca9 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,15 @@
+2014-12-05 Simon Fraser <simon.fraser@apple.com>
+
+ Programmatic scrolling and content changes are not always synchronized
+ https://bugs.webkit.org/show_bug.cgi?id=139245
+ rdar://problem/18833612
+
+ Reviewed by Anders Carlsson.
+
+ Manual test that tries to sync layout with programmatic scrolling.
+
+ * ManualTests/programmatic-scroll-flicker.html: Added.
+
2014-12-04 Alberto Garcia <berto@igalia.com>
can not find cairo-gl.h when build webkit with gtk on ubuntu 14.04
diff --git a/ManualTests/programmatic-scroll-flicker.html b/ManualTests/programmatic-scroll-flicker.html
new file mode 100644
index 0000000..f680f73
--- /dev/null
+++ b/ManualTests/programmatic-scroll-flicker.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<head>
+<style>
+ #testInner {
+ position: absolute;
+ width: 600px;
+ top: 400px;
+ left: 200px;;
+ padding: 1em;
+ background: yellow;
+ box-shadow: 0 0 0.5em gray;
+ }
+
+ .marker {
+ position: fixed;
+ background: black;
+ color: white;
+ top: 200px;
+ padding: 1em;
+ }
+
+ .spacer {
+ height: 200px;
+ }
+
+ button {
+ position: fixed;
+ top: 100px;
+ left: 200px;
+ padding: 2em;
+ font-size: 16px;
+ font-weight: bold;
+ }
+
+ .trigger #testInner {
+ -webkit-transform: translateY(-200px);
+ }
+</style>
+</head>
+<body>
+<button onclick="toggleRunning()">
+ CLICK ME to start / stop test
+</button>
+<div class="marker">Correct top position</div>
+<div id="parent" class="trigger">
+ <div id="testInner">test element</div>
+</div>
+<p class="spacer">.</p>
+<p class="spacer">.</p>
+<p class="spacer">.</p>
+<p class="spacer">.</p>
+<p class="spacer">.</p>
+<p class="spacer">.</p>
+<p class="spacer" id="last">.</p>
+
+<script>
+var INTERVAL_MS = 17;
+var testElement = document.getElementById('testInner');
+var testContainerElement = document.getElementById('parent');
+
+var state = {};
+state.triggerTranslationOnOrOff = false;
+state.running = false;
+state.intervalId = 0;
+
+function updateState()
+{
+ var translated = testContainerElement.classList.contains('trigger');
+ state.scrolled = !translated;
+}
+
+function toggleRunning()
+{
+ state.running = !state.running;
+ if (state.running) {
+ updateState();
+ state.intervalId = setInterval(runSequence, INTERVAL_MS);
+ } else {
+ clearInterval(state.intervalId);
+ }
+}
+
+function doExpensiveContentUpdate()
+{
+ var content = 'I should be stable at the correct position and not flicker above/below';
+ if (state.scrolled)
+ content += '.';
+
+ testElement.innerHTML = content;
+
+ var lastElement = document.getElementById('last');
+ var startTime = Date.now();
+ for (var i = 0; i < 1000; i++) {
+ lastElement.innerHTML = (lastElement.innerHTML + '.');
+ }
+ var domWriteTimeMs = Date.now() - startTime;
+ if (domWriteTimeMs > 2 * INTERVAL_MS)
+ lastElement.innerHTML = '';
+}
+
+function runSequence()
+{
+ doExpensiveContentUpdate();
+
+ var newScrollTop;
+ if (state.scrolled) {
+ testContainerElement.classList.add('trigger');
+ newScrollTop = 0;
+ } else {
+ testContainerElement.classList.remove('trigger');
+ newScrollTop = 200;
+ }
+
+ document.body.scrollTop = newScrollTop;
+ state.scrolled = !state.scrolled;
+}
+
+</script>
+</body>
+
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index c1c4b04..5e40cf52 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,38 @@
+2014-12-05 Simon Fraser <simon.fraser@apple.com>
+
+ Programmatic scrolling and content changes are not always synchronized
+ https://bugs.webkit.org/show_bug.cgi?id=139245
+ rdar://problem/18833612
+
+ Reviewed by Anders Carlsson.
+
+ For programmatic scrolls, AsyncScrollingCoordinator::requestScrollPositionUpdate()
+ calls updateScrollPositionAfterAsyncScroll(), then dispatches the requested
+ scroll position to the scrolling thread.
+
+ Once the scrolling thread commits, it calls back to the main thread via
+ scheduleUpdateScrollPositionAfterAsyncScroll(), which schedules a second
+ call to updateScrollPositionAfterAsyncScroll() on a timer. That's a problem,
+ because some other scroll may have happened in the meantime; when the timer
+ fires, it can sometimes restore a stale scroll position.
+
+ Fix by bailing early from scheduleUpdateScrollPositionAfterAsyncScroll()
+ for programmatic scrolls, since we know that requestScrollPositionUpdate()
+ already did the updateScrollPositionAfterAsyncScroll().
+
+ Test:
+ ManualTests/programmatic-scroll-flicker.html
+
+ * page/FrameView.cpp:
+ (WebCore::FrameView::reset): nullptr.
+ (WebCore::FrameView::setScrollPosition): Ditto.
+ (WebCore::FrameView::setWasScrolledByUser): Ditto.
+ * page/scrolling/AsyncScrollingCoordinator.cpp:
+ (WebCore::AsyncScrollingCoordinator::requestScrollPositionUpdate): Use a local variable for
+ isProgrammaticScroll just to make sure we use the same value for the duration of this function.
+ (WebCore::AsyncScrollingCoordinator::scheduleUpdateScrollPositionAfterAsyncScroll): Do nothing
+ if this is a programmatic scroll.
+
2014-12-05 Timothy Horton <timothy_horton@apple.com>
Build fix.
diff --git a/Source/WebCore/page/FrameView.cpp b/Source/WebCore/page/FrameView.cpp
index 1064014..e021501 100644
--- a/Source/WebCore/page/FrameView.cpp
+++ b/Source/WebCore/page/FrameView.cpp
@@ -289,7 +289,7 @@
m_visuallyNonEmptyPixelCount = 0;
m_isVisuallyNonEmpty = false;
m_firstVisuallyNonEmptyLayoutCallbackPending = true;
- m_maintainScrollPositionAnchor = 0;
+ m_maintainScrollPositionAnchor = nullptr;
m_throttledTimers.clear();
}
@@ -2023,7 +2023,7 @@
void FrameView::setScrollPosition(const IntPoint& scrollPoint)
{
TemporaryChange<bool> changeInProgrammaticScroll(m_inProgrammaticScroll, true);
- m_maintainScrollPositionAnchor = 0;
+ m_maintainScrollPositionAnchor = nullptr;
ScrollView::setScrollPosition(scrollPoint);
}
@@ -3713,7 +3713,7 @@
{
if (m_inProgrammaticScroll)
return;
- m_maintainScrollPositionAnchor = 0;
+ m_maintainScrollPositionAnchor = nullptr;
if (m_wasScrolledByUser == wasScrolledByUser)
return;
m_wasScrolledByUser = wasScrolledByUser;
diff --git a/Source/WebCore/page/scrolling/AsyncScrollingCoordinator.cpp b/Source/WebCore/page/scrolling/AsyncScrollingCoordinator.cpp
index 1f82303..e4cb06f 100644
--- a/Source/WebCore/page/scrolling/AsyncScrollingCoordinator.cpp
+++ b/Source/WebCore/page/scrolling/AsyncScrollingCoordinator.cpp
@@ -176,8 +176,9 @@
if (!coordinatesScrollingForFrameView(frameView))
return false;
- if (frameView->inProgrammaticScroll() || frameView->frame().document()->inPageCache())
- updateScrollPositionAfterAsyncScroll(frameView->scrollLayerID(), scrollPosition, frameView->inProgrammaticScroll(), SetScrollingLayerPosition);
+ bool isProgrammaticScroll = frameView->inProgrammaticScroll();
+ if (isProgrammaticScroll || frameView->frame().document()->inPageCache())
+ updateScrollPositionAfterAsyncScroll(frameView->scrollLayerID(), scrollPosition, isProgrammaticScroll, SetScrollingLayerPosition);
// If this frame view's document is being put into the page cache, we don't want to update our
// main frame scroll position. Just let the FrameView think that we did.
@@ -188,7 +189,7 @@
if (!stateNode)
return false;
- stateNode->setRequestedScrollPosition(scrollPosition, frameView->inProgrammaticScroll());
+ stateNode->setRequestedScrollPosition(scrollPosition, isProgrammaticScroll);
return true;
}
@@ -196,6 +197,10 @@
{
ScheduledScrollUpdate scrollUpdate(nodeID, scrollPosition, programmaticScroll, scrollingLayerPositionAction);
+ // For programmatic scrolls, requestScrollPositionUpdate() has already called updateScrollPositionAfterAsyncScroll().
+ if (programmaticScroll)
+ return;
+
if (m_updateNodeScrollPositionTimer.isActive()) {
if (m_scheduledScrollUpdate.matchesUpdateType(scrollUpdate)) {
m_scheduledScrollUpdate.scrollPosition = scrollPosition;