Skip to content

Commit

Permalink
Implement timeout for idle callbacks
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=259866

Reviewed by Chris Dumez.

This PR adds the support for idle callbacks' timeout. It also moves the logic of computing the idle deadline
to WindowEventLoop, which now keeps track of WeakHashMap of Page to its next rendering update time.

* LayoutTests/imported/w3c/web-platform-tests/requestidlecallback/basic-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/requestidlecallback/callback-timeout-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/requestidlecallback/callback-timeout-when-busy-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/requestidlecallback/deadline-after-expired-timer-expected.txt:
* LayoutTests/platform/ios-wk2/TestExpectations:
* LayoutTests/platform/mac-wk2/TestExpectations:
* LayoutTests/requestidlecallback/requestidlecallback-deadline-expected.txt: Added.
* LayoutTests/requestidlecallback/requestidlecallback-deadline-shortened-by-rendering-update-expected.txt: Added.
* LayoutTests/requestidlecallback/requestidlecallback-deadline-shortened-by-rendering-update.html: Added.
* LayoutTests/requestidlecallback/requestidlecallback-deadline.html: Added.

* Source/WebCore/dom/IdleCallbackController.cpp:
* Source/WebCore/dom/IdleCallbackController.cpp:
(WebCore::IdleCallbackController::queueIdleCallback): Schedule a timer for the timeout.
(WebCore::IdleCallbackController::startIdlePeriod):
(WebCore::IdleCallbackController::queueTaskToInvokeIdleCallbacks):
(WebCore::IdleCallbackController::invokeIdleCallbacks):
(WebCore::IdleCallbackController::invokeIdleCallbackTimeout): Added.

* Source/WebCore/dom/IdleCallbackController.h:

* Source/WebCore/dom/IdleDeadline.cpp:
(WebCore::IdleDeadline::timeRemaining const):
(WebCore::IdleDeadline::didTimeout const): Moved to the header file.

* Source/WebCore/dom/IdleDeadline.h:
(WebCore::IdleDeadline::create): Now takes DidTimeout enum class.
(WebCore::IdleDeadline::IdleDeadline): Ditto.
(WebCore::IdleDeadline::didTimeout const): Moved. Now trivially checks m_didTimeout.

* Source/WebCore/dom/WindowEventLoop.cpp:
(WebCore::WindowEventLoop::didScheduleRenderingUpdate): Moved from the header. Now takes a Page adds to the map
of pages with pending rendering opportunities.
(WebCore::WindowEventLoop::didFinishRenderingUpdate): Ditto removes it from the map.
(WebCore::WindowEventLoop::opportunisticallyRunIdleCallbacks):
(WebCore::WindowEventLoop::shouldEndIdlePeriod):
(WebCore::WindowEventLoop::computeIdleDeadline): Added.

* Source/WebCore/dom/WindowEventLoop.h:

* Source/WebCore/page/Page.cpp:
(WebCore::Page::scheduleRenderingUpdateInternal):
(WebCore::Page::renderingUpdateCompleted):

* Source/WebCore/testing/Internals.cpp:
(WebCore::Internals::preferredRenderingUpdateInterval):
* Source/WebCore/testing/Internals.h:
* Source/WebCore/testing/Internals.idl:

Canonical link: https://commits.webkit.org/266745@main
  • Loading branch information
rniwa committed Aug 9, 2023
1 parent c9ef47f commit b7bc61d
Show file tree
Hide file tree
Showing 20 changed files with 220 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ PASS window.requestIdleCallback is defined
PASS window.cancelIdleCallback is defined
PASS window.requestIdleCallback() returns a number
PASS window.cancelIdleCallback() returns undefined
FAIL requestIdleCallback schedules callbacks assert_true: IdleDeadline.timeRemaining() MUST be less than or equal to 50ms in the future. expected true got false
PASS requestIdleCallback schedules callbacks
PASS cancelIdleCallback cancels callbacks

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

FAIL requestIdleCallback callback should time out assert_false: expected false got true
FAIL requestIdleCallback callback should not time out assert_false: expected false got true
PASS requestIdleCallback callback should time out
PASS requestIdleCallback callback should not time out

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Test of requestIdleCallback timeout behavior


FAIL requestIdleCallback not scheduled when event loop is busy. assert_false: IdleDeadline.didTimeout MUST be false if requestIdleCallback wasn't scheduled due to a timeout expected false got true
FAIL requestIdleCallback scheduled with timeout when event loop is busy. assert_true: IdleDeadline.timeRemaining MUST be equal to zero if requestIdleCallback was scheduled due to a timeout expected true got false
PASS requestIdleCallback not scheduled when event loop is busy.
PASS requestIdleCallback scheduled with timeout when event loop is busy.

Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@

FAIL The deadline after an expired timer must not be negative assert_false: expected false got true
PASS The deadline after an expired timer must not be negative

3 changes: 0 additions & 3 deletions LayoutTests/platform/ios-wk2/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,6 @@ storage/filesystemaccess/ [ Pass ]

requestidlecallback [ Pass ]
imported/w3c/web-platform-tests/requestidlecallback [ Pass ]
imported/w3c/web-platform-tests/requestidlecallback/callback-idle-periods.html [ Skip ]
imported/w3c/web-platform-tests/requestidlecallback/callback-multiple-calls.html [ Skip ]
imported/w3c/web-platform-tests/requestidlecallback/callback-xhr-sync.html [ Skip ]
imported/w3c/web-platform-tests/requestidlecallback/deadline-max-rAF-dynamic.html [ Pass Failure ]
imported/w3c/web-platform-tests/requestidlecallback/deadline-max-rAF.html [ Pass Failure ]
imported/w3c/web-platform-tests/requestidlecallback/deadline-max-timeout-dynamic.html [ Pass Failure ]
Expand Down
3 changes: 0 additions & 3 deletions LayoutTests/platform/mac-wk2/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,6 @@ storage/filesystemaccess/ [ Pass ]

requestidlecallback [ Pass ]
imported/w3c/web-platform-tests/requestidlecallback [ Pass ]
imported/w3c/web-platform-tests/requestidlecallback/callback-idle-periods.html [ Skip ]
imported/w3c/web-platform-tests/requestidlecallback/callback-multiple-calls.html [ Skip ]
imported/w3c/web-platform-tests/requestidlecallback/callback-xhr-sync.html [ Skip ]
imported/w3c/web-platform-tests/requestidlecallback/deadline-max-rAF-dynamic.html [ Pass Failure ]
imported/w3c/web-platform-tests/requestidlecallback/deadline-max-rAF.html [ Pass Failure ]
imported/w3c/web-platform-tests/requestidlecallback/deadline-max-timeout-dynamic.html [ Pass Failure ]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This tests that the deadline can be reached.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS idleDeadline.timeRemaining() is 0
PASS idleDeadline.didTimeout is true
PASS didRunIdleCallback is true
PASS successfullyParsed is true

TEST COMPLETE

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This tests that the deadline can be reached.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS idleDeadline.timeRemaining() > preferredRenderingUpdateInterval is true
PASS idleDeadline.timeRemaining() <= preferredRenderingUpdateInterval + 0.5 is true
PASS didRunIdleCallback is true
PASS successfullyParsed is true

TEST COMPLETE
hello
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html><!-- webkit-test-runner [ RequestIdleCallbackEnabled=true ] -->
<html>
<body>
<script src="../resources/js-test.js"></script>
<script>

description('This tests that the deadline can be reached.');

jsTestIsAsync = true;

let didRunIdleCallback = false;
onload = () => {
requestIdleCallback((idleDeadline) => {
didRunIdleCallback = true;
window.idleDeadline = idleDeadline;
window.preferredRenderingUpdateInterval = internals.preferredRenderingUpdateInterval();
shouldBeTrue('idleDeadline.timeRemaining() > preferredRenderingUpdateInterval');

document.body.appendChild(document.createElement('span')).textContent = 'hello';
requestAnimationFrame(() => { });

shouldBeTrue('idleDeadline.timeRemaining() <= preferredRenderingUpdateInterval + 0.5');
if (idleDeadline.timeRemaining() > preferredRenderingUpdateInterval + 0.5)
console.log(idleDeadline.timeRemaining() + ' > ' + preferredRenderingUpdateInterval);
});
setTimeout(() => {
shouldBeTrue('didRunIdleCallback');
finishJSTest();
}, 200);
}

</script>
</body>
</html>
33 changes: 33 additions & 0 deletions LayoutTests/requestidlecallback/requestidlecallback-deadline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html><!-- webkit-test-runner [ RequestIdleCallbackEnabled=true ] -->
<html>
<body>
<script src="../resources/js-test.js"></script>
<script>

description('This tests that the deadline can be reached.');

jsTestIsAsync = true;

function synchronouslyWait(milliseconds) {
let start = performance.now();
while (performance.now() - start < milliseconds);
}

let didRunIdleCallback = false;
onload = () => {
requestIdleCallback((idleDeadline) => {
window.idleDeadline = idleDeadline;
shouldBe('idleDeadline.timeRemaining()', '0');
shouldBeTrue('idleDeadline.didTimeout');
didRunIdleCallback = true;
}, {timeout: 20});
synchronouslyWait(50);
setTimeout(() => {
shouldBeTrue('didRunIdleCallback');
finishJSTest();
}, 100);
}

</script>
</body>
</html>
89 changes: 52 additions & 37 deletions Source/WebCore/dom/IdleCallbackController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,28 @@ IdleCallbackController::IdleCallbackController(Document& document)
{

}
int IdleCallbackController::queueIdleCallback(Ref<IdleRequestCallback>&& callback, Seconds)
{
bool startIdlePeriod = m_idleRequestCallbacks.isEmpty() && m_runnableIdleCallbacks.isEmpty();

int IdleCallbackController::queueIdleCallback(Ref<IdleRequestCallback>&& callback, Seconds timeout)
{
++m_idleCallbackIdentifier;
auto handle = m_idleCallbackIdentifier;

m_idleRequestCallbacks.append({ handle, WTFMove(callback) });

if (startIdlePeriod)
queueTaskToStartIdlePeriod();

// FIXME: Queue a task if timeout is positive.
if (timeout > 0_s) {
Timer::schedule(timeout, [weakThis = WeakPtr { *this }, handle]() {
if (!weakThis)
return;
RefPtr document = weakThis->m_document.get();
if (!document)
return;
document->eventLoop().queueTask(TaskSource::IdleTask, [weakThis, handle]() {
if (!weakThis)
return;
weakThis->invokeIdleCallbackTimeout(handle);
});
});
}

return handle;
}
Expand Down Expand Up @@ -80,59 +89,65 @@ void IdleCallbackController::queueTaskToStartIdlePeriod()
}

// https://w3c.github.io/requestidlecallback/#start-an-idle-period-algorithm
static const auto deadlineCapToEnsureResponsiveness = 50_ms;
void IdleCallbackController::startIdlePeriod()
{
auto now = MonotonicTime::now();
if (m_lastDeadline > now)
return;

// FIXME: Take other tasks in the WindowEventLoop into account.
auto deadline = now + deadlineCapToEnsureResponsiveness;

for (auto& request : m_idleRequestCallbacks)
m_runnableIdleCallbacks.append({ request.identifier, WTFMove(request.callback) });
m_idleRequestCallbacks.clear();

if (m_runnableIdleCallbacks.isEmpty())
return;

queueTaskToInvokeIdleCallbacks(deadline);

m_lastDeadline = deadline;
queueTaskToInvokeIdleCallbacks();
}

void IdleCallbackController::queueTaskToInvokeIdleCallbacks(MonotonicTime deadline)
void IdleCallbackController::queueTaskToInvokeIdleCallbacks()
{
m_document->eventLoop().queueTask(TaskSource::IdleTask, [protectedDocument = Ref { *m_document }, deadline, this] {
m_document->eventLoop().queueTask(TaskSource::IdleTask, [protectedDocument = Ref { *m_document }, this] {
RELEASE_ASSERT(protectedDocument->idleCallbackController() == this);
invokeIdleCallbacks(deadline);
invokeIdleCallbacks();
});
}

// https://w3c.github.io/requestidlecallback/#invoke-idle-callbacks-algorithm
void IdleCallbackController::invokeIdleCallbacks(MonotonicTime deadline)
void IdleCallbackController::invokeIdleCallbacks()
{
if (!m_document || !m_document->frame())
RefPtr document = m_document.get();
if (!document || !document->frame())
return;

Ref windowEventLoop = document->windowEventLoop();
// FIXME: Implement "if the user-agent believes it should end the idle period early due to newly scheduled high-priority work, return from the algorithm."

auto now = MonotonicTime::now();
if (now < deadline) {
// FIXME: Don't do this if there is a higher priority task in the event loop.
// https://github.com/w3c/requestidlecallback/issues/83
if (m_runnableIdleCallbacks.isEmpty())
return;

auto request = m_runnableIdleCallbacks.takeFirst();
auto idleDeadline = IdleDeadline::create(deadline);
request.callback->handleEvent(idleDeadline.get());
if (!m_runnableIdleCallbacks.isEmpty())
queueTaskToInvokeIdleCallbacks(deadline);
if (now >= windowEventLoop->computeIdleDeadline() || m_runnableIdleCallbacks.isEmpty())
return;

auto request = m_runnableIdleCallbacks.takeFirst();
auto idleDeadline = IdleDeadline::create(IdleDeadline::DidTimeout::No);
request.callback->handleEvent(idleDeadline.get());

if (!m_runnableIdleCallbacks.isEmpty())
queueTaskToInvokeIdleCallbacks();
}

// https://w3c.github.io/requestidlecallback/#dfn-invoke-idle-callback-timeout-algorithm
void IdleCallbackController::invokeIdleCallbackTimeout(unsigned identifier)
{
if (!m_document)
return;

auto it = m_idleRequestCallbacks.findIf([identifier](auto& request) {
return request.identifier == identifier;
});

if (it == m_idleRequestCallbacks.end())
return;
}

if (!m_idleRequestCallbacks.isEmpty() || !m_runnableIdleCallbacks.isEmpty())
queueTaskToStartIdlePeriod();
auto idleDeadline = IdleDeadline::create(IdleDeadline::DidTimeout::Yes);
auto callback = WTFMove(it->callback);
m_idleRequestCallbacks.remove(it);
callback->handleEvent(idleDeadline.get());
}

} // namespace WebCore
8 changes: 4 additions & 4 deletions Source/WebCore/dom/IdleCallbackController.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ namespace WebCore {
class Document;
class WeakPtrImplWithEventTargetData;

class IdleCallbackController {
class IdleCallbackController : public CanMakeWeakPtr<IdleCallbackController> {
WTF_MAKE_FAST_ALLOCATED;

public:
Expand All @@ -49,11 +49,11 @@ class IdleCallbackController {

private:
void queueTaskToStartIdlePeriod();
void queueTaskToInvokeIdleCallbacks(MonotonicTime deadline);
void invokeIdleCallbacks(MonotonicTime deadline);
void queueTaskToInvokeIdleCallbacks();
void invokeIdleCallbacks();
void invokeIdleCallbackTimeout(unsigned identifier);

unsigned m_idleCallbackIdentifier { 0 };
MonotonicTime m_lastDeadline;

struct IdleRequest {
unsigned identifier { 0 };
Expand Down
18 changes: 4 additions & 14 deletions Source/WebCore/dom/IdleDeadline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,11 @@ namespace WebCore {
DOMHighResTimeStamp IdleDeadline::timeRemaining(Document& document) const
{
RefPtr window { document.domWindow() };
if (!window)
if (!window || m_didTimeout == DidTimeout::Yes)
return 0;
return window->performance().relativeTimeFromTimeOriginInReducedResolution(m_deadline);
}

bool IdleDeadline::didTimeout(Document& document) const
{
RefPtr window { document.domWindow() };
if (!window)
return true;

// Reduce the resolution before the comparision to prevent resolution leakage.
auto deadline = window->performance().relativeTimeFromTimeOriginInReducedResolution(m_deadline);
auto now = window->performance().now();
return deadline >= now;
auto deadline = document.windowEventLoop().computeIdleDeadline();
auto remainingTime = window->performance().relativeTimeFromTimeOriginInReducedResolution(deadline) - window->performance().now();
return remainingTime < 0 ? 0 : remainingTime;
}

} // namespace WebCore
16 changes: 9 additions & 7 deletions Source/WebCore/dom/IdleDeadline.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,32 @@
#pragma once

#include "DOMHighResTimeStamp.h"
#include <wtf/MonotonicTime.h>
#include <wtf/Ref.h>
#include <wtf/RefCounted.h>
#include <wtf/WeakPtr.h>

namespace WebCore {

class Document;

class IdleDeadline final : public RefCounted<IdleDeadline> {
public:
static Ref<IdleDeadline> create(MonotonicTime deadline)

enum class DidTimeout : bool { No, Yes };
static Ref<IdleDeadline> create(DidTimeout didTimeout)
{
return adoptRef(*new IdleDeadline(deadline));
return adoptRef(*new IdleDeadline(didTimeout));
}

DOMHighResTimeStamp timeRemaining(Document&) const;
bool didTimeout(Document&) const;
bool didTimeout(Document&) const { return m_didTimeout == DidTimeout::Yes; }

private:
IdleDeadline(MonotonicTime deadline)
: m_deadline(deadline)
IdleDeadline(DidTimeout didTimeout)
: m_didTimeout(didTimeout)
{ }

const MonotonicTime m_deadline;
const DidTimeout m_didTimeout;
};

} // namespace WebCore
Loading

0 comments on commit b7bc61d

Please sign in to comment.