Skip to content

Commit

Permalink
Implement AbortSignal.timeout
Browse files Browse the repository at this point in the history
AbortSignal.timeout is a static method that creates a new AbortSignal
that is automatically aborted after a specified duration. The
implementation is essentially PostDelayedTask(SignalAbort, ms).

Throttling: this API is specced to use the timer task source, but there
are three internally due to our throttling implementation. We use
immediate for the timeout == 0 case and high nesting for timeount > 0
(the typical case), i.e. all non-zero timeouts are eligible for
throttling (Note: this matches scheduler.postTask()).

Spec PR: whatwg/dom#1032
I2P: https://groups.google.com/a/chromium.org/g/blink-dev/c/9Y290P1WimY/m/bru989iAAgAJ

Bug: 1181925
Change-Id: I192d82a8bf12c368abcd47ae6c50e80f50654cf9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3425124
Reviewed-by: Domenic Denicola <domenic@chromium.org>
Reviewed-by: Mason Freed <masonf@chromium.org>
Commit-Queue: Scott Haseley <shaseley@chromium.org>
Cr-Commit-Position: refs/heads/main@{#966406}
NOKEYCHECK=True
GitOrigin-RevId: 6418fb5de0252c4cca60b9027237ad24f074465a
  • Loading branch information
shaseley authored and copybara-github committed Feb 3, 2022
1 parent fdc5daf commit 7a462a8
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 0 deletions.
1 change: 1 addition & 0 deletions blink/public/mojom/web_feature/web_feature.mojom
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 38 additions & 0 deletions blink/renderer/core/dom/abort_signal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <utility>

#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"
Expand Down Expand Up @@ -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 =
Expand All @@ -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<AbortSignal>(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<v8::Value> 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()) {
Expand Down
3 changes: 3 additions & 0 deletions blink/renderer/core/dom/abort_signal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions blink/renderer/core/dom/abort_signal.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
58 changes: 58 additions & 0 deletions blink/renderer/core/scheduler_integration_tests/throttling_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -241,6 +242,63 @@ TEST_F(BackgroundPageThrottlingTest, NestedSetIntervalZero) {
EXPECT_THAT(FilteredConsoleMessages(), Vector<String>(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(
"(<script>"
" let count = 0;"
" function scheduleTimeout() {"
" const signal = AbortSignal.timeout('%d');"
" signal.onabort = () => {"
" console.log('%s');"
" if (++count < '%d') {"
" scheduleTimeout();"
" }"
" }"
" }"
" scheduleTimeout();"
"</script>)",
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<String>(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<String>(kIterations, console_message()));
}

namespace {

class IntensiveWakeUpThrottlingTest : public ThrottlingTestBase {
Expand Down
4 changes: 4 additions & 0 deletions blink/renderer/platform/runtime_enabled_features.json5
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@
name: "AbortSignalThrowIfAborted",
status: "experimental",
},
{
name: "AbortSignalTimeout",
status: "experimental",
},
{
name: "Accelerated2dCanvas",
settable_from_internals: true,
Expand Down
28 changes: 28 additions & 0 deletions blink/web_tests/external/wpt/dom/abort/AbortSignal.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
19 changes: 19 additions & 0 deletions blink/web_tests/external/wpt/dom/abort/abort-signal-timeout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE HTML>
<meta charset=utf-8>
<title>AbortSignal.timeout frame detach</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<iframe id="iframe"></iframe>
<script>
async_test(t => {
const signal = iframe.contentWindow.AbortSignal.timeout(5);
signal.onabort = t.unreached_func("abort must not fire");

iframe.remove();

t.step_timeout(() => {
assert_false(signal.aborted);
t.done();
}, 10);
}, "Signal returned by AbortSignal.timeout() is not aborted after frame detach");
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface AbortPaymentEvent : ExtendableEvent
method respondWith
interface AbortSignal : EventTarget
static method abort
static method timeout
attribute @@toStringTag
getter aborted
getter onabort
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface AbortController
method constructor
interface AbortSignal : EventTarget
static method abort
static method timeout
attribute @@toStringTag
getter aborted
getter onabort
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7a462a8

Please sign in to comment.