421 lines
16 KiB
C++
421 lines
16 KiB
C++
/*
|
|
* Copyright (C) 2014-2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "ThreadedScrollingTree.h"
|
|
|
|
#if ENABLE(ASYNC_SCROLLING) && ENABLE(SCROLLING_THREAD)
|
|
|
|
#include "AsyncScrollingCoordinator.h"
|
|
#include "Logging.h"
|
|
#include "PlatformWheelEvent.h"
|
|
#include "ScrollingThread.h"
|
|
#include "ScrollingTreeFrameScrollingNode.h"
|
|
#include "ScrollingTreeNode.h"
|
|
#include "ScrollingTreeOverflowScrollProxyNode.h"
|
|
#include "ScrollingTreeScrollingNode.h"
|
|
#include <wtf/RunLoop.h>
|
|
#include <wtf/SetForScope.h>
|
|
#include <wtf/SystemTracing.h>
|
|
#include <wtf/text/TextStream.h>
|
|
#include <wtf/threads/BinarySemaphore.h>
|
|
|
|
namespace WebCore {
|
|
|
|
ThreadedScrollingTree::ThreadedScrollingTree(AsyncScrollingCoordinator& scrollingCoordinator)
|
|
: m_scrollingCoordinator(&scrollingCoordinator)
|
|
#if ENABLE(SMOOTH_SCROLLING)
|
|
, m_scrollAnimatorEnabled(scrollingCoordinator.scrollAnimatorEnabled())
|
|
#endif
|
|
{
|
|
}
|
|
|
|
ThreadedScrollingTree::~ThreadedScrollingTree()
|
|
{
|
|
// invalidate() should have cleared m_scrollingCoordinator.
|
|
ASSERT(!m_scrollingCoordinator);
|
|
}
|
|
|
|
WheelEventHandlingResult ThreadedScrollingTree::handleWheelEvent(const PlatformWheelEvent& wheelEvent, OptionSet<WheelEventProcessingSteps> processingSteps)
|
|
{
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
return ScrollingTree::handleWheelEvent(wheelEvent, processingSteps);
|
|
}
|
|
|
|
bool ThreadedScrollingTree::handleWheelEventAfterMainThread(const PlatformWheelEvent& wheelEvent, ScrollingNodeID targetNodeID, std::optional<WheelScrollGestureState> gestureState)
|
|
{
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
|
|
LOG_WITH_STREAM(Scrolling, stream << "ThreadedScrollingTree::handleWheelEventAfterMainThread " << wheelEvent << " node " << targetNodeID << " gestureState " << gestureState);
|
|
|
|
Locker locker { m_treeLock };
|
|
|
|
bool allowLatching = false;
|
|
OptionSet<WheelEventProcessingSteps> processingSteps;
|
|
if (gestureState.value_or(WheelScrollGestureState::Blocking) == WheelScrollGestureState::NonBlocking) {
|
|
allowLatching = true;
|
|
processingSteps = { WheelEventProcessingSteps::ScrollingThread, WheelEventProcessingSteps::MainThreadForNonBlockingDOMEventDispatch };
|
|
}
|
|
|
|
SetForScope<bool> disallowLatchingScope(m_allowLatching, allowLatching);
|
|
RefPtr<ScrollingTreeNode> targetNode = nodeForID(targetNodeID);
|
|
auto result = handleWheelEventWithNode(wheelEvent, processingSteps, targetNode.get(), EventTargeting::NodeOnly);
|
|
return result.wasHandled;
|
|
}
|
|
|
|
void ThreadedScrollingTree::wheelEventWasProcessedByMainThread(const PlatformWheelEvent& wheelEvent, std::optional<WheelScrollGestureState> gestureState)
|
|
{
|
|
LOG_WITH_STREAM(Scrolling, stream << "ThreadedScrollingTree::wheelEventWasProcessedByMainThread - gestureState " << gestureState);
|
|
|
|
ASSERT(isMainThread());
|
|
|
|
Locker locker { m_treeLock };
|
|
if (m_receivedBeganEventFromMainThread || !wheelEvent.isGestureStart())
|
|
return;
|
|
|
|
setGestureState(gestureState);
|
|
|
|
m_receivedBeganEventFromMainThread = true;
|
|
m_waitingForBeganEventCondition.notifyOne();
|
|
}
|
|
|
|
void ThreadedScrollingTree::willSendEventToMainThread(const PlatformWheelEvent&)
|
|
{
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
|
|
Locker locker { m_treeLock };
|
|
m_receivedBeganEventFromMainThread = false;
|
|
}
|
|
|
|
void ThreadedScrollingTree::waitForEventToBeProcessedByMainThread(const PlatformWheelEvent& wheelEvent)
|
|
{
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
|
|
if (!wheelEvent.isGestureStart())
|
|
return;
|
|
|
|
Locker locker { m_treeLock };
|
|
|
|
static constexpr auto maxAllowableMainThreadDelay = 50_ms;
|
|
auto startTime = MonotonicTime::now();
|
|
auto timeoutTime = startTime + maxAllowableMainThreadDelay;
|
|
|
|
bool receivedEvent = m_waitingForBeganEventCondition.waitUntil(m_treeLock, timeoutTime, [&] {
|
|
assertIsHeld(m_treeLock);
|
|
return m_receivedBeganEventFromMainThread;
|
|
});
|
|
|
|
if (!receivedEvent) {
|
|
// Timed out, go asynchronous.
|
|
setGestureState(WheelScrollGestureState::NonBlocking);
|
|
}
|
|
|
|
LOG_WITH_STREAM(Scrolling, stream << "ThreadedScrollingTree::waitForBeganEventFromMainThread done - timed out " << !receivedEvent << " gesture state is " << gestureState());
|
|
}
|
|
|
|
void ThreadedScrollingTree::invalidate()
|
|
{
|
|
// Invalidate is dispatched by the ScrollingCoordinator class on the ScrollingThread
|
|
// to break the reference cycle between ScrollingTree and ScrollingCoordinator when the
|
|
// ScrollingCoordinator's page is destroyed.
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
|
|
Locker locker { m_treeLock };
|
|
|
|
removeAllNodes();
|
|
m_delayedRenderingUpdateDetectionTimer = nullptr;
|
|
|
|
// Since this can potentially be the last reference to the scrolling coordinator,
|
|
// we need to release it on the main thread since it has member variables (such as timers)
|
|
// that expect to be destroyed from the main thread.
|
|
RunLoop::main().dispatch([scrollingCoordinator = WTFMove(m_scrollingCoordinator)] {
|
|
});
|
|
}
|
|
|
|
void ThreadedScrollingTree::propagateSynchronousScrollingReasons(const HashSet<ScrollingNodeID>& synchronousScrollingNodes)
|
|
{
|
|
auto propagateStateToAncestors = [&](ScrollingTreeNode& node) {
|
|
ASSERT(is<ScrollingTreeScrollingNode>(node) && !downcast<ScrollingTreeScrollingNode>(node).synchronousScrollingReasons().isEmpty());
|
|
|
|
if (is<ScrollingTreeFrameScrollingNode>(node))
|
|
return;
|
|
|
|
auto currNode = node.parent();
|
|
|
|
while (currNode) {
|
|
if (is<ScrollingTreeScrollingNode>(currNode))
|
|
downcast<ScrollingTreeScrollingNode>(*currNode).addSynchronousScrollingReason(SynchronousScrollingReason::DescendantScrollersHaveSynchronousScrolling);
|
|
|
|
if (is<ScrollingTreeOverflowScrollProxyNode>(currNode)) {
|
|
currNode = nodeForID(downcast<ScrollingTreeOverflowScrollProxyNode>(*currNode).overflowScrollingNodeID());
|
|
continue;
|
|
}
|
|
|
|
if (is<ScrollingTreeFrameScrollingNode>(currNode))
|
|
break;
|
|
|
|
currNode = currNode->parent();
|
|
}
|
|
};
|
|
|
|
m_hasNodesWithSynchronousScrollingReasons = !synchronousScrollingNodes.isEmpty();
|
|
|
|
for (auto nodeID : synchronousScrollingNodes) {
|
|
if (auto node = nodeForID(nodeID))
|
|
propagateStateToAncestors(*node);
|
|
}
|
|
}
|
|
|
|
bool ThreadedScrollingTree::canUpdateLayersOnScrollingThread() const
|
|
{
|
|
return !m_hasNodesWithSynchronousScrollingReasons;
|
|
}
|
|
|
|
void ThreadedScrollingTree::scrollingTreeNodeDidScroll(ScrollingTreeScrollingNode& node, ScrollingLayerPositionAction scrollingLayerPositionAction)
|
|
{
|
|
if (!m_scrollingCoordinator)
|
|
return;
|
|
|
|
auto scrollPosition = node.currentScrollPosition();
|
|
if (node.isRootNode())
|
|
setMainFrameScrollPosition(scrollPosition);
|
|
|
|
if (isHandlingProgrammaticScroll())
|
|
return;
|
|
|
|
std::optional<FloatPoint> layoutViewportOrigin;
|
|
if (is<ScrollingTreeFrameScrollingNode>(node))
|
|
layoutViewportOrigin = downcast<ScrollingTreeFrameScrollingNode>(node).layoutViewport().location();
|
|
|
|
if (RunLoop::isMain()) {
|
|
m_scrollingCoordinator->applyScrollUpdate(node.scrollingNodeID(), scrollPosition, layoutViewportOrigin, ScrollType::User, scrollingLayerPositionAction);
|
|
return;
|
|
}
|
|
|
|
LOG_WITH_STREAM(Scrolling, stream << "ThreadedScrollingTree::scrollingTreeNodeDidScroll " << node.scrollingNodeID() << " to " << scrollPosition << " triggering main thread rendering update");
|
|
|
|
auto scrollUpdate = ScrollUpdate { node.scrollingNodeID(), scrollPosition, layoutViewportOrigin, scrollingLayerPositionAction };
|
|
addPendingScrollUpdate(WTFMove(scrollUpdate));
|
|
|
|
auto deferrer = WheelEventTestMonitorCompletionDeferrer { wheelEventTestMonitor(), reinterpret_cast<WheelEventTestMonitor::ScrollableAreaIdentifier>(node.scrollingNodeID()), WheelEventTestMonitor::ScrollingThreadSyncNeeded };
|
|
RunLoop::main().dispatch([strongThis = makeRef(*this), deferrer = WTFMove(deferrer)] {
|
|
if (auto* scrollingCoordinator = strongThis->m_scrollingCoordinator.get())
|
|
scrollingCoordinator->scrollingThreadAddedPendingUpdate();
|
|
});
|
|
}
|
|
|
|
void ThreadedScrollingTree::reportSynchronousScrollingReasonsChanged(MonotonicTime timestamp, OptionSet<SynchronousScrollingReason> reasons)
|
|
{
|
|
RunLoop::main().dispatch([scrollingCoordinator = m_scrollingCoordinator, timestamp, reasons] {
|
|
scrollingCoordinator->reportSynchronousScrollingReasonsChanged(timestamp, reasons);
|
|
});
|
|
}
|
|
|
|
void ThreadedScrollingTree::reportExposedUnfilledArea(MonotonicTime timestamp, unsigned unfilledArea)
|
|
{
|
|
RunLoop::main().dispatch([scrollingCoordinator = m_scrollingCoordinator, timestamp, unfilledArea] {
|
|
scrollingCoordinator->reportExposedUnfilledArea(timestamp, unfilledArea);
|
|
});
|
|
}
|
|
|
|
#if PLATFORM(COCOA)
|
|
void ThreadedScrollingTree::currentSnapPointIndicesDidChange(ScrollingNodeID nodeID, std::optional<unsigned> horizontal, std::optional<unsigned> vertical)
|
|
{
|
|
if (!m_scrollingCoordinator)
|
|
return;
|
|
|
|
RunLoop::main().dispatch([scrollingCoordinator = m_scrollingCoordinator, nodeID, horizontal, vertical] {
|
|
scrollingCoordinator->setActiveScrollSnapIndices(nodeID, horizontal, vertical);
|
|
});
|
|
}
|
|
#endif
|
|
|
|
#if PLATFORM(MAC)
|
|
void ThreadedScrollingTree::handleWheelEventPhase(ScrollingNodeID nodeID, PlatformWheelEventPhase phase)
|
|
{
|
|
if (!m_scrollingCoordinator)
|
|
return;
|
|
|
|
RunLoop::main().dispatch([scrollingCoordinator = m_scrollingCoordinator, nodeID, phase] {
|
|
scrollingCoordinator->handleWheelEventPhase(nodeID, phase);
|
|
});
|
|
}
|
|
|
|
void ThreadedScrollingTree::setActiveScrollSnapIndices(ScrollingNodeID nodeID, std::optional<unsigned> horizontalIndex, std::optional<unsigned> verticalIndex)
|
|
{
|
|
if (!m_scrollingCoordinator)
|
|
return;
|
|
|
|
RunLoop::main().dispatch([scrollingCoordinator = m_scrollingCoordinator, nodeID, horizontalIndex, verticalIndex] {
|
|
scrollingCoordinator->setActiveScrollSnapIndices(nodeID, horizontalIndex, verticalIndex);
|
|
});
|
|
}
|
|
#endif
|
|
|
|
void ThreadedScrollingTree::willStartRenderingUpdate()
|
|
{
|
|
ASSERT(isMainThread());
|
|
|
|
if (!hasProcessedWheelEventsRecently())
|
|
return;
|
|
|
|
tracePoint(ScrollingThreadRenderUpdateSyncStart);
|
|
|
|
// Wait for the scrolling thread to acquire m_treeLock. This ensures that any pending wheel events are processed.
|
|
BinarySemaphore semaphore;
|
|
ScrollingThread::dispatch([protectedThis = makeRef(*this), &semaphore]() {
|
|
Locker treeLocker { protectedThis->m_treeLock };
|
|
semaphore.signal();
|
|
protectedThis->waitForRenderingUpdateCompletionOrTimeout();
|
|
});
|
|
semaphore.wait();
|
|
|
|
Locker locker { m_treeLock };
|
|
m_state = SynchronizationState::InRenderingUpdate;
|
|
}
|
|
|
|
Seconds ThreadedScrollingTree::maxAllowableRenderingUpdateDurationForSynchronization()
|
|
{
|
|
constexpr double allowableFrameFraction = 0.5;
|
|
auto displayFPS = nominalFramesPerSecond().value_or(60);
|
|
Seconds frameDuration = 1_s / (double)displayFPS;
|
|
return allowableFrameFraction * frameDuration;
|
|
}
|
|
|
|
// This code allows the main thread about half a frame to complete its rendering udpate. If the main thread
|
|
// is responsive (i.e. managing to render every frame), then we expect to get a didCompleteRenderingUpdate()
|
|
// within 8ms of willStartRenderingUpdate(). We time this via m_stateCondition, which blocks the scrolling
|
|
// thread (with m_treeLock locked at the start and end) so that we don't handle wheel events while waiting.
|
|
// If the condition times out, we know the main thread is being slow, and allow the scrolling thread to
|
|
// commit layer positions.
|
|
void ThreadedScrollingTree::waitForRenderingUpdateCompletionOrTimeout()
|
|
{
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
ASSERT(m_treeLock.isLocked());
|
|
|
|
if (m_delayedRenderingUpdateDetectionTimer)
|
|
m_delayedRenderingUpdateDetectionTimer->stop();
|
|
|
|
auto startTime = MonotonicTime::now();
|
|
auto timeoutTime = startTime + maxAllowableRenderingUpdateDurationForSynchronization();
|
|
|
|
bool becameIdle = m_stateCondition.waitUntil(m_treeLock, timeoutTime, [&] {
|
|
assertIsHeld(m_treeLock);
|
|
return m_state == SynchronizationState::Idle;
|
|
});
|
|
|
|
ASSERT(m_treeLock.isLocked());
|
|
|
|
if (!becameIdle) {
|
|
m_state = SynchronizationState::Desynchronized;
|
|
// At this point we know the main thread is taking too long in the rendering update,
|
|
// so we give up trying to sync with the main thread and update layers here on the scrolling thread.
|
|
if (canUpdateLayersOnScrollingThread())
|
|
applyLayerPositionsInternal();
|
|
tracePoint(ScrollingThreadRenderUpdateSyncEnd, 1);
|
|
} else
|
|
tracePoint(ScrollingThreadRenderUpdateSyncEnd);
|
|
}
|
|
|
|
void ThreadedScrollingTree::didCompleteRenderingUpdate()
|
|
{
|
|
ASSERT(isMainThread());
|
|
Locker locker { m_treeLock };
|
|
|
|
if (m_state == SynchronizationState::InRenderingUpdate)
|
|
m_stateCondition.notifyOne();
|
|
|
|
m_state = SynchronizationState::Idle;
|
|
}
|
|
|
|
void ThreadedScrollingTree::scheduleDelayedRenderingUpdateDetectionTimer(Seconds delay)
|
|
{
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
ASSERT(m_treeLock.isLocked());
|
|
|
|
if (!m_delayedRenderingUpdateDetectionTimer)
|
|
m_delayedRenderingUpdateDetectionTimer = makeUnique<RunLoop::Timer<ThreadedScrollingTree>>(RunLoop::current(), this, &ThreadedScrollingTree::delayedRenderingUpdateDetectionTimerFired);
|
|
|
|
m_delayedRenderingUpdateDetectionTimer->startOneShot(delay);
|
|
}
|
|
|
|
void ThreadedScrollingTree::delayedRenderingUpdateDetectionTimerFired()
|
|
{
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
|
|
Locker locker { m_treeLock };
|
|
if (canUpdateLayersOnScrollingThread())
|
|
applyLayerPositionsInternal();
|
|
m_state = SynchronizationState::Desynchronized;
|
|
}
|
|
|
|
void ThreadedScrollingTree::displayDidRefreshOnScrollingThread()
|
|
{
|
|
TraceScope tracingScope(ScrollingThreadDisplayDidRefreshStart, ScrollingThreadDisplayDidRefreshEnd, displayID());
|
|
ASSERT(ScrollingThread::isCurrentThread());
|
|
|
|
Locker locker { m_treeLock };
|
|
|
|
if (m_state != SynchronizationState::Idle && canUpdateLayersOnScrollingThread())
|
|
applyLayerPositionsInternal();
|
|
|
|
switch (m_state) {
|
|
case SynchronizationState::Idle: {
|
|
m_state = SynchronizationState::WaitingForRenderingUpdate;
|
|
constexpr auto maxStartRenderingUpdateDelay = 1_ms;
|
|
scheduleDelayedRenderingUpdateDetectionTimer(maxStartRenderingUpdateDelay);
|
|
break;
|
|
}
|
|
case SynchronizationState::WaitingForRenderingUpdate:
|
|
case SynchronizationState::InRenderingUpdate:
|
|
case SynchronizationState::Desynchronized:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ThreadedScrollingTree::displayDidRefresh(PlatformDisplayID displayID)
|
|
{
|
|
bool hasProcessedWheelEventsRecently = this->hasProcessedWheelEventsRecently();
|
|
|
|
// We're on the EventDispatcher thread or in the ThreadedCompositor thread here.
|
|
tracePoint(ScrollingTreeDisplayDidRefresh, displayID, hasProcessedWheelEventsRecently);
|
|
|
|
if (displayID != this->displayID())
|
|
return;
|
|
|
|
#if !PLATFORM(WPE) && !PLATFORM(GTK)
|
|
if (!hasProcessedWheelEventsRecently)
|
|
return;
|
|
#endif
|
|
|
|
ScrollingThread::dispatch([protectedThis = makeRef(*this)]() {
|
|
protectedThis->displayDidRefreshOnScrollingThread();
|
|
});
|
|
}
|
|
|
|
} // namespace WebCore
|
|
|
|
#endif // ENABLE(ASYNC_SCROLLING) && ENABLE(SCROLLING_THREAD)
|