Scheduling APIs: Implement scheduler.yield() prototype (part 2: inherit)
This CL implements the scheduler.yield() "inherit" option:
- Signal selection: GetTaskSignalFromOptions() is updated to take
inheritance into account. If either the signal or priority options
are to inherit, the inherited signal is retrieved from V8 and used
in the computation:
- signal: "inherit", priority: unset - use the inherited signal
if there is one, otherwise default priority.
- signal: "inherit", priority: fixed - create a composite signal
from the inherited signal and fixed priority. If the inherited
signal is null, just use a fixed priority signal.
- signal: unset, priority: "inherit" - if there's nothing to
inherit use the default priority; if the inherited signal has
fixed priority and can't abort, use that; otherwise create a
new composite signal.
- DOMTask is updated to support continuation, passing its signal
when creating a task scope (main thread). For workers, DOMTask
will set the continuation preserved embedder data manually before
running the task.
- WPT tests are added and modified to cover the "inherit" option.
Bug: 979020
Change-Id: Ia9776b9292d70636e2b1eea191e9c09b1809fa84
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4326152
Commit-Queue: Scott Haseley <shaseley@chromium.org>
Reviewed-by: Nate Chapin <japhet@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1118868}
diff --git a/third_party/blink/renderer/modules/scheduler/dom_scheduler.cc b/third_party/blink/renderer/modules/scheduler/dom_scheduler.cc
index f4b5382..1ecc3774 100644
--- a/third_party/blink/renderer/modules/scheduler/dom_scheduler.cc
+++ b/third_party/blink/renderer/modules/scheduler/dom_scheduler.cc
@@ -4,6 +4,7 @@
#include "third_party/blink/renderer/modules/scheduler/dom_scheduler.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
#include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
@@ -119,29 +120,38 @@
return ScriptPromise();
}
- // TODO(crbug.com/979020): Remove once inheritance is implemented.
- if ((options->hasSignal() && options->signal()->IsSchedulerSignalInherit()) ||
- (options->hasPriority() && options->priority() == "inherit")) {
- exception_state.ThrowDOMException(
- DOMExceptionCode::kNotSupportedError,
- "Signal inheritance is not yet supported");
- return ScriptPromise();
- }
-
- AbortSignal* signal_option = nullptr;
+ // Abort and priority can be inherited together or separately. Abort
+ // inheritance only depends on the signal option. Signal inheritance implies
+ // priority inheritance, but can be overridden by specifying a fixed
+ // priority.
+ absl::variant<AbortSignal*, InheritOption> signal_option(nullptr);
if (options->hasSignal()) {
- signal_option = options->signal()->GetAsAbortSignal();
+ if (options->signal()->IsSchedulerSignalInherit()) {
+ // {signal: "inherit"}
+ signal_option = InheritOption::kInherit;
+ } else {
+ // {signal: signalObject}
+ signal_option = options->signal()->GetAsAbortSignal();
+ }
}
- AtomicString priority_option = g_null_atom;
- if (options->hasPriority()) {
+ absl::variant<AtomicString, InheritOption> priority_option(g_null_atom);
+ if ((options->hasPriority() && options->priority() == "inherit")) {
+ // {priority: "inherit"}
+ priority_option = InheritOption::kInherit;
+ } else if (!options->hasPriority() &&
+ absl::holds_alternative<InheritOption>(signal_option)) {
+ // {signal: "inherit"} with no priority override.
+ priority_option = InheritOption::kInherit;
+ } else if (options->hasPriority()) {
+ // Priority override.
priority_option = AtomicString(IDLEnumAsString(options->priority()));
}
auto* task_signal = GetTaskSignalFromOptions(script_state, exception_state,
signal_option, priority_option);
if (exception_state.HadException()) {
- // The given signal was aborted.
+ // The given or inherited signal was aborted.
return ScriptPromise();
}
@@ -227,11 +237,27 @@
DOMTaskSignal* DOMScheduler::GetTaskSignalFromOptions(
ScriptState* script_state,
ExceptionState& exception_state,
- AbortSignal* signal_option,
- AtomicString priority_option) {
- // TODO(crbug.com/979020): Make `signal_option` a variant such that it can be
- // a signal or "inherit", matching the yield() options.
- AbortSignal* abort_source = signal_option;
+ absl::variant<AbortSignal*, InheritOption> signal_option,
+ absl::variant<AtomicString, InheritOption> priority_option) {
+ // `inherited_signal` will be null if no inheritance was specified or there's
+ // nothing to inherit, e.g. yielding from a non-postTask task.
+ // Note: `inherited_signal` will be the one from the original task, i.e. it
+ // doesn't get reset by continuations.
+ DOMTaskSignal* inherited_signal = nullptr;
+ if (absl::holds_alternative<InheritOption>(signal_option) ||
+ absl::holds_alternative<InheritOption>(priority_option)) {
+ CHECK(RuntimeEnabledFeatures::SchedulerYieldEnabled());
+ if (auto* inherited_state =
+ ScriptWrappableTaskState::GetCurrent(script_state)) {
+ inherited_signal = inherited_state->GetSignal();
+ }
+ }
+
+ AbortSignal* abort_source =
+ absl::holds_alternative<AbortSignal*>(signal_option)
+ ? absl::get<AbortSignal*>(signal_option)
+ : inherited_signal;
+ // Short-circuit things now that we know if `abort_source` is aborted.
if (abort_source && abort_source->aborted()) {
exception_state.RethrowV8Exception(
ToV8Traits<IDLAny>::ToV8(script_state,
@@ -241,14 +267,19 @@
}
DOMTaskSignal* priority_source = nullptr;
- if (priority_option != g_null_atom) {
+ if (absl::holds_alternative<InheritOption>(priority_option)) {
+ priority_source = inherited_signal;
+ } else if (absl::get<AtomicString>(priority_option) != g_null_atom) {
// The priority option overrides the signal for priority.
priority_source = GetFixedPriorityTaskSignal(
- script_state, WebSchedulingPriorityFromString(priority_option));
- } else if (IsA<DOMTaskSignal>(signal_option)) {
- priority_source = To<DOMTaskSignal>(signal_option);
- } else {
- // No signal or priority was specified.
+ script_state, WebSchedulingPriorityFromString(
+ absl::get<AtomicString>(priority_option)));
+ } else if (IsA<DOMTaskSignal>(absl::get<AbortSignal*>(signal_option))) {
+ priority_source = To<DOMTaskSignal>(absl::get<AbortSignal*>(signal_option));
+ }
+ // `priority_source` is null if there was nothing to inherit or no signal or
+ // priority was specified.
+ if (!priority_source) {
priority_source =
GetFixedPriorityTaskSignal(script_state, kDefaultPriority);
}
diff --git a/third_party/blink/renderer/modules/scheduler/dom_scheduler.h b/third_party/blink/renderer/modules/scheduler/dom_scheduler.h
index 5a4b2324..b33d1cd 100644
--- a/third_party/blink/renderer/modules/scheduler/dom_scheduler.h
+++ b/third_party/blink/renderer/modules/scheduler/dom_scheduler.h
@@ -6,6 +6,7 @@
#define THIRD_PARTY_BLINK_RENDERER_MODULES_SCHEDULER_DOM_SCHEDULER_H_
#include "base/memory/scoped_refptr.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
#include "third_party/blink/public/common/scheduler/task_attribution_id.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
@@ -148,11 +149,13 @@
// Gets the task signal associated with a task or continuation, creating a
// composite task signal from the `signal_option` and `priority_option` if
// needed. The signal this returns is what gets used for scheduling the task
- // or continuation.
- DOMTaskSignal* GetTaskSignalFromOptions(ScriptState*,
- ExceptionState&,
- AbortSignal* signal_option,
- AtomicString priority_option);
+ // or continuation, and it's what gets propagated for yield() inheritance.
+ enum class InheritOption { kInherit };
+ DOMTaskSignal* GetTaskSignalFromOptions(
+ ScriptState*,
+ ExceptionState&,
+ absl::variant<AbortSignal*, InheritOption> signal_option,
+ absl::variant<AtomicString, InheritOption> priority_option);
// Gets the fixed priority TaskSignal for `priority`, creating it if needed.
DOMTaskSignal* GetFixedPriorityTaskSignal(ScriptState*,
diff --git a/third_party/blink/renderer/modules/scheduler/dom_task.cc b/third_party/blink/renderer/modules/scheduler/dom_task.cc
index 0c92240..e121979 100644
--- a/third_party/blink/renderer/modules/scheduler/dom_task.cc
+++ b/third_party/blink/renderer/modules/scheduler/dom_task.cc
@@ -8,6 +8,7 @@
#include "base/check_op.h"
#include "base/metrics/histogram_macros.h"
+#include "third_party/blink/public/common/scheduler/task_attribution_id.h"
#include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
@@ -22,7 +23,9 @@
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/modules/scheduler/dom_task_signal.h"
+#include "third_party/blink/renderer/modules/scheduler/script_wrappable_task_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/scheduler/public/task_attribution_tracker.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
#include "third_party/blink/renderer/platform/scheduler/public/web_scheduling_priority.h"
@@ -197,17 +200,26 @@
std::unique_ptr<scheduler::TaskAttributionTracker::TaskScope>
task_attribution_scope;
+ // For the main thread (tracker exists), create the task scope with the signal
+ // to set up propagation. On workers, set the current context here since there
+ // is no tracker.
if (auto* tracker = ThreadScheduler::Current()->GetTaskAttributionTracker()) {
task_attribution_scope = tracker->CreateTaskScope(
script_state, parent_task_id_,
- scheduler::TaskAttributionTracker::TaskScopeType::kSchedulerPostTask);
+ scheduler::TaskAttributionTracker::TaskScopeType::kSchedulerPostTask,
+ signal_);
+ } else if (RuntimeEnabledFeatures::SchedulerYieldEnabled()) {
+ ScriptWrappableTaskState::SetCurrent(
+ script_state, MakeGarbageCollected<ScriptWrappableTaskState>(
+ scheduler::TaskAttributionId(), signal_));
}
ScriptValue result;
- if (callback_->Invoke(nullptr).To(&result))
+ if (callback_->Invoke(nullptr).To(&result)) {
resolver_->Resolve(result.V8Value());
- else if (try_catch.HasCaught())
+ } else if (try_catch.HasCaught()) {
resolver_->Reject(try_catch.Exception());
+ }
}
void DOMTask::OnAbort() {
diff --git a/third_party/blink/web_tests/external/wpt/lint.ignore b/third_party/blink/web_tests/external/wpt/lint.ignore
index 6cc351e..ab06848 100644
--- a/third_party/blink/web_tests/external/wpt/lint.ignore
+++ b/third_party/blink/web_tests/external/wpt/lint.ignore
@@ -215,6 +215,7 @@
SET TIMEOUT: resource-timing/resources/nested-contexts.js
SET TIMEOUT: reporting/resources/first-csp-report.https.sub.html
SET TIMEOUT: reporting/resources/second-csp-report.https.sub.html
+SET TIMEOUT: scheduler/tentative/yield/yield-inherit-across-promises.any.js
SET TIMEOUT: scheduler/tentative/yield/yield-priority-timers.any.js
SET TIMEOUT: secure-contexts/basic-popup-and-iframe-tests.https.js
SET TIMEOUT: service-workers/cache-storage/cache-abort.https.any.js
diff --git a/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-abort.any.js b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-abort.any.js
index 610c8a981..e96694f 100644
--- a/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-abort.any.js
+++ b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-abort.any.js
@@ -11,10 +11,75 @@
promise_test(t => {
const controller = new TaskController();
const signal = controller.signal;
+ const task = scheduler.postTask(async () => {
+ controller.abort();
+ const p = scheduler.yield({signal: 'inherit'});
+ await promise_rejects_dom(t, 'AbortError', p);
+ }, {signal});
+ return promise_rejects_dom(t, 'AbortError', task);
+}, 'yield() with an aborted signal (inherit signal)');
+
+promise_test(t => {
+ const controller = new TaskController();
+ const signal = controller.signal;
+ const task = scheduler.postTask(async () => {
+ controller.abort();
+ const p = scheduler.yield({signal: 'inherit', priority: 'background'});
+ await promise_rejects_dom(t, 'AbortError', p);
+ }, {signal});
+ return promise_rejects_dom(t, 'AbortError', task);
+}, 'yield() with an aborted signal (inherit signal priority override)');
+
+promise_test(t => {
+ const controller = new TaskController();
+ const signal = controller.signal;
+ const task = scheduler.postTask(async () => {
+ controller.abort();
+ await scheduler.yield({priority: 'inherit'});
+ }, {signal});
+ return promise_rejects_dom(t, 'AbortError', task);
+}, 'yield() with an aborted signal (inherit priority only)');
+
+promise_test(t => {
+ const controller = new TaskController();
+ const signal = controller.signal;
return scheduler.postTask(async () => {
scheduler.postTask(async () => {controller.abort();}, {priority: 'user-blocking'});
- assert_false(signal.aborted);
+ t.step(() => assert_false(signal.aborted));
const p = scheduler.yield({signal});
await promise_rejects_dom(t, 'AbortError', p);
});
}, 'yield() aborted in a separate task');
+
+promise_test(t => {
+ const controller = new TaskController();
+ const signal = controller.signal;
+ return scheduler.postTask(async () => {
+ scheduler.postTask(async () => {controller.abort();}, {priority: 'user-blocking'});
+ t.step(() => assert_false(signal.aborted));
+ const p = scheduler.yield({signal: 'inherit'});
+ await promise_rejects_dom(t, 'AbortError', p);
+ }, {signal});
+}, 'yield() aborted in a separate task (inherit)');
+
+promise_test(t => {
+ const controller = new TaskController();
+ const signal = controller.signal;
+ return scheduler.postTask(async () => {
+ scheduler.postTask(async () => {controller.abort();}, {priority: 'user-blocking'});
+ t.step(() => assert_false(signal.aborted));
+ const p = scheduler.yield({signal: 'inherit', priority: 'background'});
+ await promise_rejects_dom(t, 'AbortError', p);
+ }, {signal});
+}, 'yield() aborted in a separate task (inherit signal priority override)');
+
+promise_test(t => {
+ const controller = new TaskController();
+ const signal = controller.signal;
+ return scheduler.postTask(async () => {
+ scheduler.postTask(async () => {controller.abort();}, {priority: 'user-blocking'});
+ t.step(() => assert_false(signal.aborted));
+ await scheduler.yield({priority: 'inherit'});
+ t.step(() => assert_true(signal.aborted));
+ }, {signal});
+}, 'yield() aborted in a separate task (inherit priority only)');
diff --git a/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-inherit-across-promises.any.js b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-inherit-across-promises.any.js
new file mode 100644
index 0000000..eaa0125a
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-inherit-across-promises.any.js
@@ -0,0 +1,65 @@
+'use strict';
+
+function postInheritPriorityTestTask(config) {
+ const ids = [];
+ const task = scheduler.postTask(async () => {
+ await new Promise(resolve => setTimeout(resolve));
+ await fetch('/common/blank.html');
+ await new Promise(resolve => setTimeout(resolve));
+ const subtask = scheduler.postTask(() => { ids.push('subtask'); }, {priority: config.subTaskPriority});
+ await scheduler.yield(config.yieldOptions);
+ ids.push('yield');
+ await subtask;
+ }, config.taskOptions);
+ return {task, ids}
+}
+
+for (let priority of ['user-blocking', 'background']) {
+ const expected = priority == 'user-blocking' ? 'yield,subtask' : 'subtask,yield';
+ promise_test(async t => {
+ const config = {
+ taskOptions: {priority},
+ subTaskPriority: 'user-blocking',
+ yieldOptions: {priority: 'inherit'},
+ };
+ const {task, ids} = postInheritPriorityTestTask(config);
+ await task;
+ assert_equals(ids.join(), expected);
+ }, `yield() inherits priority (string) across promises (${priority})`);
+
+ promise_test(async t => {
+ const signal = (new TaskController({priority})).signal;
+ const config = {
+ taskOptions: {signal},
+ subTaskPriority: 'user-blocking',
+ yieldOptions: {signal: 'inherit'},
+ };
+ const {task, ids} = postInheritPriorityTestTask(config);
+ await task;
+ assert_equals(ids.join(), expected);
+ }, `yield() inherits priority (signal) across promises (${priority})`);
+
+ promise_test(async t => {
+ const config = {
+ taskOptions: {priority},
+ subTaskPriority: 'user-blocking',
+ yieldOptions: {signal: 'inherit'},
+ };
+ const {task, ids} = postInheritPriorityTestTask(config);
+ await task;
+ assert_equals(ids.join(), expected);
+ }, `yield() inherits priority (priority string with signal inherit) across promises (${priority})`);
+}
+
+promise_test(async t => {
+ const controller = new TaskController();
+ const signal = controller.signal;
+ return scheduler.postTask(async () => {
+ await new Promise(resolve => setTimeout(resolve));
+ await fetch('/common/blank.html');
+ await new Promise(resolve => setTimeout(resolve));
+ controller.abort();
+ const p = scheduler.yield({signal: 'inherit'});
+ await promise_rejects_dom(t, 'AbortError', p);
+ }, {signal});
+}, `yield() inherits abort across promises`);
diff --git a/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-priority-posttask.any.js b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-priority-posttask.any.js
index dae3b93..0700094 100644
--- a/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-priority-posttask.any.js
+++ b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-priority-posttask.any.js
@@ -85,6 +85,24 @@
}, 'yield() with postTask tasks (signal option)');
promise_test(async t => {
+ for (const config of priorityConfigs) {
+ const {tasks, ids} =
+ postTestTasks(config.options, {priority: 'inherit'});
+ await Promise.all(tasks);
+ assert_equals(ids.join(), config.expected);
+ }
+}, 'yield() with postTask tasks (inherit priority)');
+
+promise_test(async t => {
+ for (const config of signalConfigs) {
+ const {tasks, ids} =
+ postTestTasks(config.options, {signal: 'inherit'});
+ await Promise.all(tasks);
+ assert_equals(ids.join(), config.expected);
+ }
+}, 'yield() with postTask tasks (inherit signal)');
+
+promise_test(async t => {
const expected = 'y0,ub1,ub2,uv1,uv2,y1,y2,y3,bg1,bg2';
const {tasks, ids} = postTestTasks(
{priority: 'user-blocking'}, {priority: 'background'});
@@ -109,3 +127,67 @@
await Promise.all(tasks);
assert_equals(ids.join(), taskOrders['user-blocking']);
}, 'yield() priority overrides signal');
+
+promise_test(async t => {
+ const ids = [];
+
+ const controller = new TaskController();
+ const signal = controller.signal;
+
+ await scheduler.postTask(async () => {
+ ids.push('y0');
+
+ const subtasks = [];
+ subtasks.push(scheduler.postTask(() => { ids.push('uv1'); }));
+ subtasks.push(scheduler.postTask(() => { ids.push('uv2'); }));
+
+ // 'user-visible' continuations.
+ await scheduler.yield({signal: 'inherit'});
+ ids.push('y1');
+ await scheduler.yield({signal: 'inherit'});
+ ids.push('y2');
+
+ controller.setPriority('background');
+
+ // 'background' continuations.
+ await scheduler.yield({signal: 'inherit'});
+ ids.push('y3');
+ await scheduler.yield({signal: 'inherit'});
+ ids.push('y4');
+
+ await Promise.all(subtasks);
+ }, {signal});
+
+ assert_equals(ids.join(), 'y0,y1,y2,uv1,uv2,y3,y4');
+}, 'yield() with TaskSignal has dynamic priority')
+
+promise_test(async t => {
+ const ids = [];
+
+ await scheduler.postTask(async () => {
+ ids.push('y0');
+
+ const subtasks = [];
+ subtasks.push(scheduler.postTask(() => { ids.push('ub1'); }, {priority: 'user-blocking'}));
+ subtasks.push(scheduler.postTask(() => { ids.push('uv1'); }));
+
+ // Ignore inherited signal (user-visible continuations).
+ await scheduler.yield();
+ ids.push('y1');
+ await scheduler.yield();
+ ids.push('y2');
+
+ subtasks.push(scheduler.postTask(() => { ids.push('ub2'); }, {priority: 'user-blocking'}));
+ subtasks.push(scheduler.postTask(() => { ids.push('uv2'); }));
+
+ // Now use inherited signal (user-blocking continuations).
+ await scheduler.yield({signal: 'inherit'});
+ ids.push('y3');
+ await scheduler.yield({signal: 'inherit'});
+ ids.push('y4');
+
+ await Promise.all(subtasks);
+ }, {priority: 'user-blocking'});
+
+ assert_equals(ids.join(), 'y0,ub1,y1,y2,y3,y4,ub2,uv1,uv2');
+}, 'yield() mixed inheritance and default')
diff --git a/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-priority-timers.any.js b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-priority-timers.any.js
index 81a2f9b..ff5a3d4 100644
--- a/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-priority-timers.any.js
+++ b/third_party/blink/web_tests/external/wpt/scheduler/tentative/yield/yield-priority-timers.any.js
@@ -80,3 +80,15 @@
assert_equals(ids.join(), config.expected);
}
}, 'yield() with timer tasks (signal option)');
+
+promise_test(async t => {
+ const {tasks, ids} = postTestTasks({priority: 'inherit'});
+ await Promise.all(tasks);
+ assert_equals(ids.join(), taskOrders['user-visible']);
+}, 'yield() with timer tasks (inherit priority)');
+
+promise_test(async t => {
+ const {tasks, ids} = postTestTasks({signal: 'inherit'});
+ await Promise.all(tasks);
+ assert_equals(ids.join(), taskOrders['user-visible']);
+}, 'yield() with timer tasks (inherit signal)');