Skip to content

Commit

Permalink
Teach the opportunistic task scheduler about zero-delay timers and re…
Browse files Browse the repository at this point in the history
…questAnimationFrame

https://bugs.webkit.org/show_bug.cgi?id=261205
rdar://113237238

Reviewed by Yusuke Suzuki.

Refine heuristics used to schedule incremental sweeping in `OpportunisticTaskScheduler`, such that
it's aware of both one-shot zero-delay timers and pending rAF callbacks. Earlier versions of this
task scheduler (prior to `266680@main`) attempted to avoid scheduling work in both DOM timers and
rAF by exiting early if any `OpportunisticTaskDeferralScope` was held, but this has the additional
effect of preventing opportunistic tasks from being scheduled in many cases where it's necessary to
maintain performance on some critical benchmarks. As such, our current approach ignores pending
timers and rAF altogether, and simply schedules an opportunistic task at the first turn of the
runloop after finalizing a rendering update.

We take a slightly different approach in this patch; rather than limit opportunistically scheduled
tasks to non-deferral scopes, we instead use information about whether there is imminently scheduled
work as one of several pieces of information used to heurisitcally choose when to schedule
opportunistic tasks.

See below for more details.

* Source/WebCore/dom/ScriptedAnimationController.cpp:
(WebCore::ScriptedAnimationController::registerCallback):
(WebCore::ScriptedAnimationController::serviceRequestAnimationFrameCallbacks):
* Source/WebCore/dom/ScriptedAnimationController.h:

Grab a `ImminentlyScheduledWorkScope` while a rAF callback is pending.

* Source/WebCore/page/DOMTimer.cpp:
(WebCore::DOMTimer::install):
(WebCore::DOMTimer::removeById):
(WebCore::DOMTimer::fired):
(WebCore::DOMTimer::stop):
(WebCore::DOMTimer::makeImminentlyScheduledWorkScopeIfPossible):
(WebCore::DOMTimer::clearImminentlyScheduledWorkScope):
* Source/WebCore/page/DOMTimer.h:

Similarly, grab a `ImminentlyScheduledWorkScope` when a one-shot, low-delay DOM timer is pending.

This essentially restores the codepath that was removed in `266680@main`, but renames the "task
deferral" scopes to "imminently scheduled work" instead, to reflect the fact that it's only used to
provide a hint to the scheduler that there's imminently scheduled work.

* Source/WebCore/page/OpportunisticTaskScheduler.cpp:
(WebCore::OpportunisticTaskScheduler::reschedule):

We also turn `OpportunisticTaskScheduler` into a repeating runloop observer, so that we can choose
which runloop after a rendering update we should schedule the opportunistic task (or none at all).
We currently always schedule an opportunistic task after the first turn of the runloop after
finalizing the rendering update, but with this patch, we may now wait for a few turns of the runloop
to pass before scheduling the task (or we may not schedule a task during the rendering update at
all).

(WebCore::OpportunisticTaskScheduler::makeScheduledWorkScope):
(WebCore::OpportunisticTaskScheduler::runLoopObserverFired):

Implement the main heuristic here — if we have no imminent work, we schedule the opportunistic task
right away (matching behavior on trunk). Otherwise, we'll schedule work only if we have (relatively)
a lot of time until the next rendering update, or if the runloop has turned at least a few times
since the end of the rendering update. We start with this simple heuristic for now; in subsequent
patches, we'll continue to refine this heuristic to account for additional signals such as heap
size and object count, current GC phase, and other state from JavaScriptCore.

Note that we preserve the existing behavior of scheduling at most one opportunistic task per
rendering update. In future patches (especially if we can further quantize GC phases), we should
also consider allowing multiple opportunistic tasks per rendering update, at much smaller time
slices.

(WebCore::ImminentlyScheduledWorkScope::ImminentlyScheduledWorkScope):
(WebCore::ImminentlyScheduledWorkScope::~ImminentlyScheduledWorkScope):

Rename `OpportunisticTaskDeferralScope` to `ImminentlyScheduledWorkScope`, to better reflect the
fact that it's only used to provide a hint to the scheduler that there's imminently scheduled work,
rather than always deferring opportunistic tasks during the scope.

(WebCore::OpportunisticTaskDeferralScope::OpportunisticTaskDeferralScope): Deleted.
(WebCore::OpportunisticTaskDeferralScope::~OpportunisticTaskDeferralScope): Deleted.
(WebCore::OpportunisticTaskScheduler::makeDeferralScope): Deleted.
(WebCore::OpportunisticTaskScheduler::incrementDeferralCount): Deleted.
(WebCore::OpportunisticTaskScheduler::decrementDeferralCount): Deleted.
* Source/WebCore/page/OpportunisticTaskScheduler.h:
(WebCore::ImminentlyScheduledWorkScope::create):
(WebCore::OpportunisticTaskScheduler::hasImminentlyScheduledWork const):
* Source/WebCore/page/Page.cpp:
(WebCore::Page::didCommitLoad):
(WebCore::Page::didFirstMeaningfulPaint):

Replace the task deferral scope here with just a simple boolean flag indicating whether we're
waiting for the first meaningful paint. If so, we'll bail from opportunistic tasks altogether.

(WebCore::Page::opportunisticallyRunIdleCallbacks):

Split out logic for scheduling idle callbacks from the rest of the opportunistically scheduled
tasks, so that we can safely bail if the page itself is destroyed during idle callbacks. We also
move the `TraceScope` to `OpportunisticTaskScheduler`, where we already have the (approximate)
remaining time — this allows us to avoid an extra syscall to `mach_absolute_time` when creating the
trace scope.

(WebCore::Page::performOpportunisticallyScheduledTasks):
* Source/WebCore/page/Page.h:
(WebCore::Page::isWaitingForFirstMeaningfulPaint const):

Remove the former task deferral scope used to avoid opportunistic tasks prior to first meaningful
paint; instead, just make this a boolean flag and return early from `OpportunisticTaskScheduler` in
the case when it's set.

Canonical link: https://commits.webkit.org/267818@main
  • Loading branch information
whsieh committed Sep 9, 2023
1 parent 9435b85 commit 7d1d62f
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 63 deletions.
8 changes: 6 additions & 2 deletions Source/WebCore/dom/ScriptedAnimationController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

#include "InspectorInstrumentation.h"
#include "Logging.h"
#include "OpportunisticTaskScheduler.h"
#include "Page.h"
#include "RequestAnimationFrameCallback.h"
#include "Settings.h"
Expand Down Expand Up @@ -108,7 +109,10 @@ ScriptedAnimationController::CallbackId ScriptedAnimationController::registerCal
CallbackId callbackId = ++m_nextCallbackId;
callback->m_firedOrCancelled = false;
callback->m_id = callbackId;
m_callbackDataList.append({ WTFMove(callback), UserGestureIndicator::currentUserGesture() });
RefPtr<ImminentlyScheduledWorkScope> workScope;
if (page())
workScope = page()->opportunisticTaskScheduler().makeScheduledWorkScope();
m_callbackDataList.append({ WTFMove(callback), UserGestureIndicator::currentUserGesture(), WTFMove(workScope) });

if (m_document)
InspectorInstrumentation::didRequestAnimationFrame(*m_document, callbackId);
Expand Down Expand Up @@ -157,7 +161,7 @@ void ScriptedAnimationController::serviceRequestAnimationFrameCallbacks(ReducedR
Ref<ScriptedAnimationController> protectedThis(*this);
Ref<Document> protectedDocument(*m_document);

for (auto& [callback, userGestureTokenToForward] : callbackDataList) {
for (auto& [callback, userGestureTokenToForward, scheduledWorkScope] : callbackDataList) {
if (callback->m_firedOrCancelled)
continue;
callback->m_firedOrCancelled = true;
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/dom/ScriptedAnimationController.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
namespace WebCore {

class Document;
class ImminentlyScheduledWorkScope;
class Page;
class RequestAnimationFrameCallback;
class UserGestureToken;
Expand Down Expand Up @@ -78,6 +79,7 @@ class ScriptedAnimationController : public RefCounted<ScriptedAnimationControlle
struct CallbackData {
Ref<RequestAnimationFrameCallback> callback;
RefPtr<UserGestureToken> userGestureTokenToForward;
RefPtr<ImminentlyScheduledWorkScope> scheduledWorkScope;
};
Vector<CallbackData> m_callbackDataList;

Expand Down
31 changes: 30 additions & 1 deletion Source/WebCore/page/DOMTimer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ int DOMTimer::install(ScriptExecutionContext& context, Function<void(ScriptExecu
{
Ref<DOMTimer> timer = adoptRef(*new DOMTimer(context, WTFMove(action), timeout, type));
timer->suspendIfNeeded();
timer->makeImminentlyScheduledWorkScopeIfPossible(context);

// Keep asking for the next id until we're given one that we don't already have.
do {
Expand Down Expand Up @@ -246,8 +247,10 @@ void DOMTimer::removeById(ScriptExecutionContext& context, int timeoutId)

InspectorInstrumentation::didRemoveTimer(context, timeoutId);

if (auto timer = context.takeTimeout(timeoutId))
if (auto timer = context.takeTimeout(timeoutId)) {
timer->clearImminentlyScheduledWorkScope();
timer->m_timer = nullptr;
}
}

inline bool DOMTimer::isDOMTimersThrottlingEnabled(const Document& document) const
Expand Down Expand Up @@ -345,6 +348,7 @@ void DOMTimer::fired()

updateThrottlingStateIfNecessary(fireState);

clearImminentlyScheduledWorkScope();
return;
}

Expand All @@ -371,6 +375,8 @@ void DOMTimer::fired()
}
nestedTimers->stopTracking();
}

clearImminentlyScheduledWorkScope();
}

void DOMTimer::stop()
Expand All @@ -380,6 +386,8 @@ void DOMTimer::stop()
// which will cause a memory leak.
m_timer = nullptr;
m_action = nullptr;

clearImminentlyScheduledWorkScope();
}

void DOMTimer::updateTimerIntervalIfNecessary()
Expand Down Expand Up @@ -442,4 +450,25 @@ const char* DOMTimer::activeDOMObjectName() const
return "DOMTimer";
}

void DOMTimer::makeImminentlyScheduledWorkScopeIfPossible(ScriptExecutionContext& context)
{
if (!m_oneShot || m_currentTimerInterval > 1_ms)
return;

RefPtr document = dynamicDowncast<Document>(context);
if (!document)
return;

auto* page = document->page();
if (!page)
return;

m_imminentlyScheduledWorkScope = page->opportunisticTaskScheduler().makeScheduledWorkScope();
}

void DOMTimer::clearImminentlyScheduledWorkScope()
{
m_imminentlyScheduledWorkScope = nullptr;
}

} // namespace WebCore
5 changes: 5 additions & 0 deletions Source/WebCore/page/DOMTimer.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ namespace WebCore {

class DOMTimerFireState;
class Document;
class ImminentlyScheduledWorkScope;
class ScheduledAction;

class DOMTimer final : public RefCounted<DOMTimer>, public ActiveDOMObject, public CanMakeWeakPtr<DOMTimer> {
Expand Down Expand Up @@ -82,6 +83,9 @@ class DOMTimer final : public RefCounted<DOMTimer>, public ActiveDOMObject, publ
const char* activeDOMObjectName() const final;
void stop() final;

void makeImminentlyScheduledWorkScopeIfPossible(ScriptExecutionContext&);
void clearImminentlyScheduledWorkScope();

enum TimerThrottleState {
Undetermined,
ShouldThrottle,
Expand All @@ -98,6 +102,7 @@ class DOMTimer final : public RefCounted<DOMTimer>, public ActiveDOMObject, publ
bool m_hasReachedMaxNestingLevel;
Seconds m_currentTimerInterval;
RefPtr<UserGestureToken> m_userGestureTokenToForward;
RefPtr<ImminentlyScheduledWorkScope> m_imminentlyScheduledWorkScope;
};

} // namespace WebCore
92 changes: 51 additions & 41 deletions Source/WebCore/page/OpportunisticTaskScheduler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,80 +30,90 @@

namespace WebCore {

OpportunisticTaskDeferralScope::OpportunisticTaskDeferralScope(OpportunisticTaskScheduler& scheduler)
: m_scheduler(&scheduler)
{
scheduler.incrementDeferralCount();
}

OpportunisticTaskDeferralScope::OpportunisticTaskDeferralScope(OpportunisticTaskDeferralScope&& scope)
: m_scheduler(std::exchange(scope.m_scheduler, { }))
{
}

OpportunisticTaskDeferralScope::~OpportunisticTaskDeferralScope()
{
if (m_scheduler)
m_scheduler->decrementDeferralCount();
}

OpportunisticTaskScheduler::OpportunisticTaskScheduler(Page& page)
: m_page(&page)
, m_runLoopObserver(makeUnique<RunLoopObserver>(RunLoopObserver::WellKnownOrder::PostRenderingUpdate, [weakThis = WeakPtr { this }] {
if (auto strongThis = weakThis.get())
strongThis->runLoopObserverFired();
}, RunLoopObserver::Type::OneShot))
}, RunLoopObserver::Type::Repeating))
{
m_runLoopObserver->schedule();
}

OpportunisticTaskScheduler::~OpportunisticTaskScheduler() = default;

void OpportunisticTaskScheduler::reschedule(MonotonicTime deadline)
{
m_runloopCountAfterBeingScheduled = 0;
m_currentDeadline = deadline;

if (m_runLoopObserver->isScheduled())
return;

if (m_taskDeferralCount)
m_runLoopObserver->invalidate();
else
m_runLoopObserver->schedule();
}

std::unique_ptr<OpportunisticTaskDeferralScope> OpportunisticTaskScheduler::makeDeferralScope()
Ref<ImminentlyScheduledWorkScope> OpportunisticTaskScheduler::makeScheduledWorkScope()
{
return makeUnique<OpportunisticTaskDeferralScope>(*this);
return ImminentlyScheduledWorkScope::create(*this);
}

void OpportunisticTaskScheduler::runLoopObserverFired()
{
m_runLoopObserver->invalidate();
if (!m_currentDeadline)
return;

if (m_taskDeferralCount)
auto page = m_page;
if (UNLIKELY(!page))
return;

auto deadline = std::exchange(m_currentDeadline, MonotonicTime { });
if (!deadline)
if (page->isWaitingForFirstMeaningfulPaint())
return;

auto currentTime = ApproximateTime::now();
auto remainingTime = m_currentDeadline.secondsSinceEpoch() - currentTime.secondsSinceEpoch();
if (remainingTime < 0_ms)
return;

m_runloopCountAfterBeingScheduled++;

bool shouldRunTask = [&] {
if (!hasImminentlyScheduledWork())
return true;

static constexpr auto fractionOfRenderingIntervalWhenScheduledWorkIsImminent = 0.72;
if (remainingTime > fractionOfRenderingIntervalWhenScheduledWorkIsImminent * page->preferredRenderingUpdateInterval())
return true;

static constexpr auto minimumRunloopCountWhenScheduledWorkIsImminent = 4;
if (m_runloopCountAfterBeingScheduled > minimumRunloopCountWhenScheduledWorkIsImminent)
return true;

return false;
}();

if (!shouldRunTask)
return;

if (ApproximateTime::now().secondsSinceEpoch() > deadline.secondsSinceEpoch())
TraceScope tracingScope {
PerformOpportunisticallyScheduledTasksStart,
PerformOpportunisticallyScheduledTasksEnd,
static_cast<uint64_t>(remainingTime.microseconds())
};

auto deadline = std::exchange(m_currentDeadline, MonotonicTime { });
page->opportunisticallyRunIdleCallbacks();
if (UNLIKELY(!page))
return;

if (auto page = m_page.get())
page->performOpportunisticallyScheduledTasks(deadline);
page->performOpportunisticallyScheduledTasks(deadline);
}

void OpportunisticTaskScheduler::incrementDeferralCount()
ImminentlyScheduledWorkScope::ImminentlyScheduledWorkScope(OpportunisticTaskScheduler& scheduler)
: m_scheduler(&scheduler)
{
if (++m_taskDeferralCount == 1)
m_runLoopObserver->invalidate();
scheduler.m_imminentlyScheduledWorkCount++;
}

void OpportunisticTaskScheduler::decrementDeferralCount()
ImminentlyScheduledWorkScope::~ImminentlyScheduledWorkScope()
{
if (!--m_taskDeferralCount && m_currentDeadline)
m_runLoopObserver->schedule();
if (m_scheduler)
m_scheduler->m_imminentlyScheduledWorkCount--;
}

} // namespace WebCore
27 changes: 15 additions & 12 deletions Source/WebCore/page/OpportunisticTaskScheduler.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,21 @@

namespace WebCore {

class Page;
class OpportunisticTaskScheduler;
class Page;

class OpportunisticTaskDeferralScope {
WTF_MAKE_NONCOPYABLE(OpportunisticTaskDeferralScope); WTF_MAKE_FAST_ALLOCATED;
class ImminentlyScheduledWorkScope : public RefCounted<ImminentlyScheduledWorkScope> {
public:
OpportunisticTaskDeferralScope(OpportunisticTaskScheduler&);
OpportunisticTaskDeferralScope(OpportunisticTaskDeferralScope&&);
~OpportunisticTaskDeferralScope();
static Ref<ImminentlyScheduledWorkScope> create(OpportunisticTaskScheduler& scheduler)
{
return adoptRef(*new ImminentlyScheduledWorkScope(scheduler));
}

~ImminentlyScheduledWorkScope();

private:
ImminentlyScheduledWorkScope(OpportunisticTaskScheduler&);

WeakPtr<OpportunisticTaskScheduler> m_scheduler;
};

Expand All @@ -56,20 +60,19 @@ class OpportunisticTaskScheduler : public RefCounted<OpportunisticTaskScheduler>
~OpportunisticTaskScheduler();

void reschedule(MonotonicTime deadline);
bool hasImminentlyScheduledWork() const { return m_imminentlyScheduledWorkCount; }

WARN_UNUSED_RETURN std::unique_ptr<OpportunisticTaskDeferralScope> makeDeferralScope();
WARN_UNUSED_RETURN Ref<ImminentlyScheduledWorkScope> makeScheduledWorkScope();

private:
friend class OpportunisticTaskDeferralScope;
friend class ImminentlyScheduledWorkScope;

OpportunisticTaskScheduler(Page&);
void runLoopObserverFired();

void incrementDeferralCount();
void decrementDeferralCount();

WeakPtr<Page> m_page;
uint64_t m_taskDeferralCount { 0 };
uint64_t m_imminentlyScheduledWorkCount { 0 };
uint64_t m_runloopCountAfterBeingScheduled { 0 };
MonotonicTime m_currentDeadline;
std::unique_ptr<RunLoopObserver> m_runLoopObserver;
};
Expand Down
13 changes: 8 additions & 5 deletions Source/WebCore/page/Page.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1490,12 +1490,12 @@ void Page::didCommitLoad()
geolocationController->didNavigatePage();
#endif

m_opportunisticTaskDeferralScopeForFirstPaint = m_opportunisticTaskScheduler->makeDeferralScope();
m_isWaitingForFirstMeaningfulPaint = true;
}

void Page::didFirstMeaningfulPaint()
{
m_opportunisticTaskDeferralScopeForFirstPaint = nullptr;
m_isWaitingForFirstMeaningfulPaint = false;
}

void Page::didFinishLoad()
Expand Down Expand Up @@ -4535,12 +4535,15 @@ void Page::reloadExecutionContextsForOrigin(const ClientOrigin& origin, std::opt
}
}

void Page::performOpportunisticallyScheduledTasks(MonotonicTime deadline)
void Page::opportunisticallyRunIdleCallbacks()
{
TraceScope tracingScope(PerformOpportunisticallyScheduledTasksStart, PerformOpportunisticallyScheduledTasksEnd, (deadline - MonotonicTime::now()).microseconds());
forEachWindowEventLoop([&](WindowEventLoop& eventLoop) {
forEachWindowEventLoop([&](auto& eventLoop) {
eventLoop.opportunisticallyRunIdleCallbacks();
});
}

void Page::performOpportunisticallyScheduledTasks(MonotonicTime deadline)
{
commonVM().performOpportunisticallyScheduledTasks(deadline);
}

Expand Down
6 changes: 4 additions & 2 deletions Source/WebCore/page/Page.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ class MediaPlaybackTarget;
class MediaRecorderProvider;
class MediaSessionCoordinatorPrivate;
class ModelPlayerProvider;
class OpportunisticTaskDeferralScope;
class PageConfiguration;
class PageConsoleClient;
class PageDebuggable;
Expand Down Expand Up @@ -1053,8 +1052,11 @@ class Page : public Supplementable<Page>, public CanMakeWeakPtr<Page> {
WEBCORE_EXPORT void addRootFrame(LocalFrame&);
WEBCORE_EXPORT void removeRootFrame(LocalFrame&);

void opportunisticallyRunIdleCallbacks();
void performOpportunisticallyScheduledTasks(MonotonicTime deadline);

bool isWaitingForFirstMeaningfulPaint() const { return m_isWaitingForFirstMeaningfulPaint; }

private:
struct Navigation {
RegistrableDomain domain;
Expand Down Expand Up @@ -1411,7 +1413,7 @@ class Page : public Supplementable<Page>, public CanMakeWeakPtr<Page> {
std::unique_ptr<AttachmentElementClient> m_attachmentElementClient;
#endif

std::unique_ptr<OpportunisticTaskDeferralScope> m_opportunisticTaskDeferralScopeForFirstPaint;
bool m_isWaitingForFirstMeaningfulPaint { false };
Ref<OpportunisticTaskScheduler> m_opportunisticTaskScheduler;

#if ENABLE(IMAGE_ANALYSIS)
Expand Down

0 comments on commit 7d1d62f

Please sign in to comment.