diff --git a/blink/public/mojom/web_feature/web_feature.mojom b/blink/public/mojom/web_feature/web_feature.mojom index 496df724f18..82be28372a1 100644 --- a/blink/public/mojom/web_feature/web_feature.mojom +++ b/blink/public/mojom/web_feature/web_feature.mojom @@ -3477,6 +3477,7 @@ enum WebFeature { kV8UDPSocket_RemoteAddress_AttributeGetter = 4156, kV8UDPSocket_RemotePort_AttributeGetter = 4157, kV8UDPSocket_Writable_AttributeGetter = 4158, + kAbortSignalTimeout = 4159, // Add new features immediately above this line. Don't change assigned // numbers of any item, and don't reuse removed slots. diff --git a/blink/renderer/core/dom/abort_signal.cc b/blink/renderer/core/dom/abort_signal.cc index 4bd989e015b..424e5e50684 100644 --- a/blink/renderer/core/dom/abort_signal.cc +++ b/blink/renderer/core/dom/abort_signal.cc @@ -7,6 +7,8 @@ #include #include "base/callback.h" +#include "base/time/time.h" +#include "third_party/blink/public/platform/task_type.h" #include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h" #include "third_party/blink/renderer/core/dom/events/event.h" #include "third_party/blink/renderer/core/event_target_names.h" @@ -77,6 +79,7 @@ AbortSignal* AbortSignal::abort(ScriptState* script_state) { return abort(script_state, reason); } +// static AbortSignal* AbortSignal::abort(ScriptState* script_state, ScriptValue reason) { DCHECK(!reason.IsEmpty()); AbortSignal* signal = @@ -85,6 +88,41 @@ AbortSignal* AbortSignal::abort(ScriptState* script_state, ScriptValue reason) { return signal; } +// static +AbortSignal* AbortSignal::timeout(ScriptState* script_state, + uint64_t milliseconds) { + ExecutionContext* context = ExecutionContext::From(script_state); + AbortSignal* signal = MakeGarbageCollected(context); + // The spec requires us to use the timer task source, but there are a few + // timer task sources due to our throttling implementation. We match + // setTimeout for immediate timeouts, but use the high-nesting task type for + // all positive timeouts so they are eligible for throttling (i.e. no + // nesting-level exception). + TaskType task_type = milliseconds == 0 + ? TaskType::kJavascriptTimerImmediate + : TaskType::kJavascriptTimerDelayedHighNesting; + // `signal` needs to be held with a strong reference to keep it alive in case + // there are or will be event handlers attached. + context->GetTaskRunner(task_type)->PostDelayedTask( + FROM_HERE, + WTF::Bind(&AbortSignal::AbortTimeoutFired, WrapPersistent(signal), + WrapPersistent(script_state)), + base::Milliseconds(milliseconds)); + return signal; +} + +void AbortSignal::AbortTimeoutFired(ScriptState* script_state) { + if (GetExecutionContext()->IsContextDestroyed() || + !script_state->ContextIsValid()) { + return; + } + ScriptState::Scope scope(script_state); + auto* isolate = script_state->GetIsolate(); + v8::Local reason = V8ThrowDOMException::CreateOrEmpty( + isolate, DOMExceptionCode::kTimeoutError, "signal timed out"); + SignalAbort(script_state, ScriptValue(isolate, reason)); +} + ScriptValue AbortSignal::reason(ScriptState* script_state) const { DCHECK(script_state->GetIsolate()->InContext()); if (abort_reason_.IsEmpty()) { diff --git a/blink/renderer/core/dom/abort_signal.h b/blink/renderer/core/dom/abort_signal.h index 1573d1483d9..edb5da5c0ce 100644 --- a/blink/renderer/core/dom/abort_signal.h +++ b/blink/renderer/core/dom/abort_signal.h @@ -44,6 +44,7 @@ class CORE_EXPORT AbortSignal : public EventTargetWithInlineData { // abort_signal.idl static AbortSignal* abort(ScriptState*); static AbortSignal* abort(ScriptState*, ScriptValue reason); + static AbortSignal* timeout(ScriptState*, uint64_t milliseconds); ScriptValue reason(ScriptState*) const; bool aborted() const { return !abort_reason_.IsEmpty(); } void throwIfAborted(ScriptState*, ExceptionState&) const; @@ -88,6 +89,8 @@ class CORE_EXPORT AbortSignal : public EventTargetWithInlineData { void Trace(Visitor*) const override; private: + void AbortTimeoutFired(ScriptState*); + // https://dom.spec.whatwg.org/#abortsignal-abort-reason // There is one difference from the spec. The value is empty instead of // undefined when this signal is not aborted. This is because diff --git a/blink/renderer/core/dom/abort_signal.idl b/blink/renderer/core/dom/abort_signal.idl index 022d8c07744..fa50f590e97 100644 --- a/blink/renderer/core/dom/abort_signal.idl +++ b/blink/renderer/core/dom/abort_signal.idl @@ -12,6 +12,12 @@ Measure, NewObject ] static AbortSignal abort(optional any reason); + [ + CallWith=ScriptState, + MeasureAs=AbortSignalTimeout, + NewObject, + RuntimeEnabled=AbortSignalTimeout + ] static AbortSignal timeout([EnforceRange] unsigned long long milliseconds); readonly attribute boolean aborted; [CallWith=ScriptState] readonly attribute any reason; diff --git a/blink/renderer/core/scheduler_integration_tests/throttling_test.cc b/blink/renderer/core/scheduler_integration_tests/throttling_test.cc index 7cc181d9285..7872c50897c 100644 --- a/blink/renderer/core/scheduler_integration_tests/throttling_test.cc +++ b/blink/renderer/core/scheduler_integration_tests/throttling_test.cc @@ -16,6 +16,7 @@ #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" #include "third_party/blink/renderer/platform/testing/testing_platform_support_with_mock_scheduler.h" #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" +#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h" using testing::AnyOf; using testing::ElementsAre; @@ -241,6 +242,63 @@ TEST_F(BackgroundPageThrottlingTest, NestedSetIntervalZero) { EXPECT_THAT(FilteredConsoleMessages(), Vector(5, console_message)); } +class AbortSignalTimeoutThrottlingTest : public BackgroundPageThrottlingTest { + public: + AbortSignalTimeoutThrottlingTest() + : console_message_(BuildTimerConsoleMessage()) {} + + String GetTestSource(wtf_size_t iterations, wtf_size_t timeout) { + return String::Format( + "()", + timeout, console_message_.Utf8().c_str(), iterations); + } + + const String& console_message() { return console_message_; } + + protected: + const String console_message_; +}; + +TEST_F(AbortSignalTimeoutThrottlingTest, TimeoutsThrottledInBackgroundPage) { + SimRequest main_resource("https://example.com/", "text/html"); + LoadURL("https://example.com/"); + main_resource.Complete(GetTestSource(/*iterations=*/20, /*timeout=*/10)); + + GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); + + // Make sure that we run no more than one task a second. + platform_->RunForPeriod(base::Seconds(3)); + EXPECT_THAT(FilteredConsoleMessages(), Vector(3, console_message())); +} + +TEST_F(AbortSignalTimeoutThrottlingTest, ZeroMsTimersNotThrottled) { + SimRequest main_resource("https://example.com/", "text/html"); + LoadURL("https://example.com/"); + + constexpr wtf_size_t kIterations = 20; + main_resource.Complete(GetTestSource(kIterations, /*timeout=*/0)); + + GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); + + // All tasks should run after 1 ms since time does not advance during the + // test, the timeout was 0 ms, and the timeouts are not throttled. + platform_->RunForPeriod(base::Milliseconds(1)); + EXPECT_THAT(FilteredConsoleMessages(), + Vector(kIterations, console_message())); +} + namespace { class IntensiveWakeUpThrottlingTest : public ThrottlingTestBase { diff --git a/blink/renderer/platform/runtime_enabled_features.json5 b/blink/renderer/platform/runtime_enabled_features.json5 index f33e9c3bd26..4caae76abd4 100644 --- a/blink/renderer/platform/runtime_enabled_features.json5 +++ b/blink/renderer/platform/runtime_enabled_features.json5 @@ -110,6 +110,10 @@ name: "AbortSignalThrowIfAborted", status: "experimental", }, + { + name: "AbortSignalTimeout", + status: "experimental", + }, { name: "Accelerated2dCanvas", settable_from_internals: true, diff --git a/blink/web_tests/external/wpt/dom/abort/AbortSignal.any.js b/blink/web_tests/external/wpt/dom/abort/AbortSignal.any.js index 1d7d7678eb1..3bbdc11a92f 100644 --- a/blink/web_tests/external/wpt/dom/abort/AbortSignal.any.js +++ b/blink/web_tests/external/wpt/dom/abort/AbortSignal.any.js @@ -10,3 +10,31 @@ async_test(t => { s.onabort = t.unreached_func("abort event handler called"); t.step_timeout(() => { t.done(); }, 2000); }, "signal returned by AbortSignal.abort() should not fire abort event"); + +test(t => { + const signal = AbortSignal.timeout(0); + assert_true(signal instanceof AbortSignal, "returned object is an AbortSignal"); + assert_false(signal.aborted, "returned signal is not already aborted"); +}, "AbortSignal.timeout() returns a non-aborted signal"); + +async_test(t => { + const signal = AbortSignal.timeout(5); + signal.onabort = t.step_func_done(() => { + assert_true(signal.aborted, "signal is aborted"); + assert_true(signal.reason instanceof DOMException, "signal.reason is a DOMException"); + assert_equals(signal.reason.name, "TimeoutError", "signal.reason is a TimeoutError"); + }); +}, "Signal returned by AbortSignal.timeout() times out"); + +async_test(t => { + let result = ""; + for (const value of ["1", "2", "3"]) { + const signal = AbortSignal.timeout(5); + signal.onabort = t.step_func(() => { result += value; }); + } + + const signal = AbortSignal.timeout(5); + signal.onabort = t.step_func_done(() => { + assert_equals(result, "123", "Timeout order should be 123"); + }); +}, "AbortSignal timeouts fire in order"); diff --git a/blink/web_tests/external/wpt/dom/abort/abort-signal-timeout.html b/blink/web_tests/external/wpt/dom/abort/abort-signal-timeout.html new file mode 100644 index 00000000000..2a9c13d6143 --- /dev/null +++ b/blink/web_tests/external/wpt/dom/abort/abort-signal-timeout.html @@ -0,0 +1,19 @@ + + +AbortSignal.timeout frame detach + + + + diff --git a/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt b/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt index 7b77741404d..051fc7554fc 100644 --- a/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt +++ b/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt @@ -10,6 +10,7 @@ interface AbortPaymentEvent : ExtendableEvent method respondWith interface AbortSignal : EventTarget static method abort + static method timeout attribute @@toStringTag getter aborted getter onabort diff --git a/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt b/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt index ce0b6a3512a..cd0b02b85cc 100644 --- a/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt +++ b/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt @@ -11,6 +11,7 @@ Starting worker: resources/global-interface-listing-worker.js [Worker] method constructor [Worker] interface AbortSignal : EventTarget [Worker] static method abort +[Worker] static method timeout [Worker] attribute @@toStringTag [Worker] getter aborted [Worker] getter onabort diff --git a/blink/web_tests/webexposed/global-interface-listing-expected.txt b/blink/web_tests/webexposed/global-interface-listing-expected.txt index 8aafcad00c9..cc2a84658dc 100644 --- a/blink/web_tests/webexposed/global-interface-listing-expected.txt +++ b/blink/web_tests/webexposed/global-interface-listing-expected.txt @@ -10,6 +10,7 @@ interface AbortController method constructor interface AbortSignal : EventTarget static method abort + static method timeout attribute @@toStringTag getter aborted getter onabort diff --git a/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt b/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt index ba107308e55..d220defef0f 100644 --- a/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt +++ b/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt @@ -11,6 +11,7 @@ Starting worker: resources/global-interface-listing-worker.js [Worker] method constructor [Worker] interface AbortSignal : EventTarget [Worker] static method abort +[Worker] static method timeout [Worker] attribute @@toStringTag [Worker] getter aborted [Worker] getter onabort