[go: nahoru, domu]

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)');