From c457516f9f0f04bb6bf0f5642117ea520b918fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 17 Oct 2023 11:16:35 -0700 Subject: [PATCH] Create new version of RuntimeScheduler (#40944) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/40944 ## Summary This creates a new version of `RuntimeScheduler` that's intended to be backwards compatible but with a few notable changes: 1. `scheduleTask` is now thread-safe. 2. `scheduleWork` is now just an alias of `scheduleTask` with immediate priority (to preserve the yielding semantics it had over other tasks). 3. Yielding mechanism has changed, to make lower priority tasks to yield to higher priority tasks, instead of just yielding to `scheduleWork` and `executeNowOnTheSameThread`. We don't expect this to have any impact in performance or user perceivable behavior, so we consider it a short-lived refactor. When we validate this assumptions in a complex application we'll delete the old version and only keep the fork. ## Motivation The main motivation for this refactor is to reduce the amount of unnecessary interruptions of running tasks (via `shouldYield`) that are only used to schedule asynchronous tasks from native. The `scheduleWork` method is the only available mechanism exposed to native APIs to schedule work in the JS thread (as the existing version of `scheduleTask` is only meant to be called from JS). This mechanism **always** asks for any running tasks in the scheduler to yield, so these tasks are always considered to have the highest priority. This makes sense for discrete user events, but not for many other use cases coming from native (e.g.: notifying network responses could be UserBlocking, Normal or Low depending on the use case). We need a way to schedule tasks from native with other kinds of priorities, so we don't always have to interrupt what's currently executing if it has a higher priority than what we're scheduling. ## Changes **General APIs:** This centralizes scheduling in only 2 APIs in `RuntimeScheduler` (which already exist in the legacy version): * `scheduleTask`, which is non-blocking for the caller and can be used from any thread. This always uses the task queue in the scheduler and a new yielding mechanism. * `executeNowOnTheSameThread`, which is blocking for the caller and asks any task executing in the scheduler to yield. These tasks don't go through the task queue and instead queue through the existing synchronization mechanism in `RuntimeExecutor`. The yielding mechanism for these tasks is preserved. `scheduleWork` will be deprecated and it's just an alias for `scheduleTask` with an immediate priority (to preserve a similar behavior). **Yielding behavior:** Before, tasks would only yield to tasks scheduled via `scheduleWork` and `executeNowOnTheSameThread` (those tasks didn't go through the task queue). With this implementation, tasks would now yield to any task that has a higher position in the task queue. That means we reuse the existing mechanism to avoid lower priority tasks to never execute because higher priority tasks never stop coming. All tasks would yield to requests for synchronous access (via `executeNowOnTheSameThread`) as did the current implementation. Changelog: [internal] Reviewed By: javache, sammy-SC Differential Revision: D49316881 fbshipit-source-id: 55a72cabbf3f9e110cbf7e878e52e066fd77d8cf --- .../runtimescheduler/RuntimeScheduler.cpp | 24 +- .../runtimescheduler/RuntimeScheduler.h | 17 +- .../RuntimeScheduler_Legacy.cpp | 18 +- .../RuntimeScheduler_Legacy.h | 6 +- .../RuntimeScheduler_Modern.cpp | 258 +++++++++++++ .../RuntimeScheduler_Modern.h | 176 +++++++++ .../react/renderer/runtimescheduler/Task.h | 2 + .../tests/RuntimeSchedulerTest.cpp | 339 ++++++++++++++++-- 8 files changed, 803 insertions(+), 37 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp index 34aaeebc4b09eb..7ad00deecaa12e 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp @@ -7,6 +7,7 @@ #include "RuntimeScheduler.h" #include "RuntimeScheduler_Legacy.h" +#include "RuntimeScheduler_Modern.h" #include "SchedulerPriorityUtils.h" #include @@ -15,11 +16,28 @@ namespace facebook::react { +namespace { +std::unique_ptr getRuntimeSchedulerImplementation( + RuntimeExecutor runtimeExecutor, + bool useModernRuntimeScheduler, + std::function now) { + if (useModernRuntimeScheduler) { + return std::make_unique( + std::move(runtimeExecutor), std::move(now)); + } else { + return std::make_unique( + std::move(runtimeExecutor), std::move(now)); + } +} +} // namespace + RuntimeScheduler::RuntimeScheduler( RuntimeExecutor runtimeExecutor, + bool useModernRuntimeScheduler, std::function now) - : runtimeSchedulerImpl_(std::make_unique( + : runtimeSchedulerImpl_(getRuntimeSchedulerImplementation( std::move(runtimeExecutor), + useModernRuntimeScheduler, std::move(now))) {} void RuntimeScheduler::scheduleWork(RawCallback&& callback) const noexcept { @@ -28,13 +46,13 @@ void RuntimeScheduler::scheduleWork(RawCallback&& callback) const noexcept { std::shared_ptr RuntimeScheduler::scheduleTask( SchedulerPriority priority, - jsi::Function&& callback) noexcept { + jsi::Function&& callback) const noexcept { return runtimeSchedulerImpl_->scheduleTask(priority, std::move(callback)); } std::shared_ptr RuntimeScheduler::scheduleTask( SchedulerPriority priority, - RawCallback&& callback) noexcept { + RawCallback&& callback) const noexcept { return runtimeSchedulerImpl_->scheduleTask(priority, std::move(callback)); } diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h index d07d8f46889c1a..88f7ed898c69a5 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h @@ -18,14 +18,20 @@ namespace facebook::react { class RuntimeSchedulerBase { public: virtual ~RuntimeSchedulerBase() = default; + // FIXME(T167271466): remove `const` modified when the RuntimeScheduler + // refactor has been shipped. virtual void scheduleWork(RawCallback&& callback) const noexcept = 0; virtual void executeNowOnTheSameThread(RawCallback&& callback) = 0; + // FIXME(T167271466): remove `const` modified when the RuntimeScheduler + // refactor has been shipped. virtual std::shared_ptr scheduleTask( SchedulerPriority priority, - jsi::Function&& callback) noexcept = 0; + jsi::Function&& callback) const noexcept = 0; + // FIXME(T167271466): remove `const` modified when the RuntimeScheduler + // refactor has been shipped. virtual std::shared_ptr scheduleTask( SchedulerPriority priority, - RawCallback&& callback) noexcept = 0; + RawCallback&& callback) const noexcept = 0; virtual void cancelTask(Task& task) noexcept = 0; virtual bool getShouldYield() const noexcept = 0; virtual bool getIsSynchronous() const noexcept = 0; @@ -38,8 +44,9 @@ class RuntimeSchedulerBase { // at runtime based on a feature flag. class RuntimeScheduler final : RuntimeSchedulerBase { public: - RuntimeScheduler( + explicit RuntimeScheduler( RuntimeExecutor runtimeExecutor, + bool useModernRuntimeScheduler = false, std::function now = RuntimeSchedulerClock::now); @@ -74,11 +81,11 @@ class RuntimeScheduler final : RuntimeSchedulerBase { */ std::shared_ptr scheduleTask( SchedulerPriority priority, - jsi::Function&& callback) noexcept override; + jsi::Function&& callback) const noexcept override; std::shared_ptr scheduleTask( SchedulerPriority priority, - RawCallback&& callback) noexcept override; + RawCallback&& callback) const noexcept override; /* * Cancelled task will never be executed. diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp index 238084ea85f102..e95f7bc4b8c770 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp @@ -38,7 +38,14 @@ void RuntimeScheduler_Legacy::scheduleWork( std::shared_ptr RuntimeScheduler_Legacy::scheduleTask( SchedulerPriority priority, - jsi::Function&& callback) noexcept { + jsi::Function&& callback) const noexcept { + SystraceSection s( + "RuntimeScheduler::scheduleTask", + "priority", + serialize(priority), + "callbackType", + "jsi::Function"); + auto expirationTime = now_() + timeoutForSchedulerPriority(priority); auto task = std::make_shared(priority, std::move(callback), expirationTime); @@ -51,7 +58,14 @@ std::shared_ptr RuntimeScheduler_Legacy::scheduleTask( std::shared_ptr RuntimeScheduler_Legacy::scheduleTask( SchedulerPriority priority, - RawCallback&& callback) noexcept { + RawCallback&& callback) const noexcept { + SystraceSection s( + "RuntimeScheduler::scheduleTask", + "priority", + serialize(priority), + "callbackType", + "RawCallback"); + auto expirationTime = now_() + timeoutForSchedulerPriority(priority); auto task = std::make_shared(priority, std::move(callback), expirationTime); diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h index 6645a20d96834b..4617d05998382b 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h @@ -19,7 +19,7 @@ namespace facebook::react { class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { public: - RuntimeScheduler_Legacy( + explicit RuntimeScheduler_Legacy( RuntimeExecutor runtimeExecutor, std::function now = RuntimeSchedulerClock::now); @@ -55,11 +55,11 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { */ std::shared_ptr scheduleTask( SchedulerPriority priority, - jsi::Function&& callback) noexcept override; + jsi::Function&& callback) const noexcept override; std::shared_ptr scheduleTask( SchedulerPriority priority, - RawCallback&& callback) noexcept override; + RawCallback&& callback) const noexcept override; /* * Cancelled task will never be executed. diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp new file mode 100644 index 00000000000000..9e37e506d5a74a --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -0,0 +1,258 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "RuntimeScheduler_Modern.h" +#include "SchedulerPriorityUtils.h" + +#include +#include +#include "ErrorUtils.h" + +namespace facebook::react { + +#pragma mark - Public + +RuntimeScheduler_Modern::RuntimeScheduler_Modern( + RuntimeExecutor runtimeExecutor, + std::function now) + : runtimeExecutor_(std::move(runtimeExecutor)), now_(std::move(now)) {} + +void RuntimeScheduler_Modern::scheduleWork( + RawCallback&& callback) const noexcept { + SystraceSection s("RuntimeScheduler::scheduleWork"); + scheduleTask(SchedulerPriority::ImmediatePriority, std::move(callback)); +} + +std::shared_ptr RuntimeScheduler_Modern::scheduleTask( + SchedulerPriority priority, + jsi::Function&& callback) const noexcept { + SystraceSection s( + "RuntimeScheduler::scheduleTask", + "priority", + serialize(priority), + "callbackType", + "jsi::Function"); + + auto expirationTime = now_() + timeoutForSchedulerPriority(priority); + auto task = + std::make_shared(priority, std::move(callback), expirationTime); + + scheduleTask(task); + + return task; +} + +std::shared_ptr RuntimeScheduler_Modern::scheduleTask( + SchedulerPriority priority, + RawCallback&& callback) const noexcept { + SystraceSection s( + "RuntimeScheduler::scheduleTask", + "priority", + serialize(priority), + "callbackType", + "RawCallback"); + + auto expirationTime = now_() + timeoutForSchedulerPriority(priority); + auto task = + std::make_shared(priority, std::move(callback), expirationTime); + + scheduleTask(task); + + return task; +} + +bool RuntimeScheduler_Modern::getShouldYield() const noexcept { + std::shared_lock lock(schedulingMutex_); + + return syncTaskRequests_ > 0 || + (!taskQueue_.empty() && taskQueue_.top() != currentTask_); +} + +bool RuntimeScheduler_Modern::getIsSynchronous() const noexcept { + return isSynchronous_; +} + +void RuntimeScheduler_Modern::cancelTask(Task& task) noexcept { + task.callback.reset(); +} + +SchedulerPriority RuntimeScheduler_Modern::getCurrentPriorityLevel() + const noexcept { + return currentPriority_; +} + +RuntimeSchedulerTimePoint RuntimeScheduler_Modern::now() const noexcept { + return now_(); +} + +void RuntimeScheduler_Modern::executeNowOnTheSameThread( + RawCallback&& callback) { + SystraceSection s("RuntimeScheduler::executeNowOnTheSameThread"); + + syncTaskRequests_++; + + executeSynchronouslyOnSameThread_CAN_DEADLOCK( + runtimeExecutor_, + [this, callback = std::move(callback)](jsi::Runtime& runtime) mutable { + SystraceSection s2( + "RuntimeScheduler::executeNowOnTheSameThread callback"); + + syncTaskRequests_--; + + isSynchronous_ = true; + + auto currentTime = now_(); + auto priority = SchedulerPriority::ImmediatePriority; + auto expirationTime = + currentTime + timeoutForSchedulerPriority(priority); + auto task = std::make_shared( + priority, std::move(callback), expirationTime); + + executeTask(runtime, task, currentTime); + + isSynchronous_ = false; + }); + + bool shouldScheduleWorkLoop = false; + + { + // Unique access because we might write to `isWorkLoopScheduled_`. + std::unique_lock lock(schedulingMutex_); + + // We only need to schedule the work loop if there any remaining tasks + // in the queue. + if (!taskQueue_.empty() && !isWorkLoopScheduled_) { + isWorkLoopScheduled_ = true; + shouldScheduleWorkLoop = true; + } + } + + if (shouldScheduleWorkLoop) { + scheduleWorkLoop(); + } +} + +// This will be replaced by microtasks +void RuntimeScheduler_Modern::callExpiredTasks(jsi::Runtime& runtime) { + SystraceSection s("RuntimeScheduler::callExpiredTasks"); + startWorkLoop(runtime, true); +} + +#pragma mark - Private + +void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr task) const { + bool shouldScheduleWorkLoop = false; + + { + std::unique_lock lock(schedulingMutex_); + + // We only need to schedule the work loop if the task we're about to + // schedule is the only one in the queue. + // Otherwise, we don't need to schedule it because there's another one + // running already that will pick up the new task. + if (taskQueue_.empty() && !isWorkLoopScheduled_) { + isWorkLoopScheduled_ = true; + shouldScheduleWorkLoop = true; + } + + taskQueue_.push(task); + } + + if (shouldScheduleWorkLoop) { + scheduleWorkLoop(); + } +} + +void RuntimeScheduler_Modern::scheduleWorkLoop() const { + runtimeExecutor_( + [this](jsi::Runtime& runtime) { startWorkLoop(runtime, false); }); +} + +void RuntimeScheduler_Modern::startWorkLoop( + jsi::Runtime& runtime, + bool onlyExpired) const { + SystraceSection s("RuntimeScheduler::startWorkLoop"); + + auto previousPriority = currentPriority_; + + try { + while (syncTaskRequests_ == 0) { + auto currentTime = now_(); + auto topPriorityTask = selectTask(currentTime, onlyExpired); + + if (!topPriorityTask) { + // No pending work to do. + // Events will restart the loop when necessary. + break; + } + + executeTask(runtime, topPriorityTask, currentTime); + } + } catch (jsi::JSError& error) { + handleFatalError(runtime, error); + } + + currentPriority_ = previousPriority; +} + +std::shared_ptr RuntimeScheduler_Modern::selectTask( + RuntimeSchedulerTimePoint currentTime, + bool onlyExpired) const { + // We need a unique lock here because we'll also remove executed tasks from + // the top of the queue. + std::unique_lock lock(schedulingMutex_); + + // It's safe to reset the flag here, as its access is also synchronized with + // the access to the task queue. + isWorkLoopScheduled_ = false; + + // Skip executed tasks + while (!taskQueue_.empty() && !taskQueue_.top()->callback) { + taskQueue_.pop(); + } + + if (!taskQueue_.empty()) { + auto task = taskQueue_.top(); + auto didUserCallbackTimeout = task->expirationTime <= currentTime; + if (!onlyExpired || didUserCallbackTimeout) { + return task; + } + } + + return nullptr; +} + +void RuntimeScheduler_Modern::executeTask( + jsi::Runtime& runtime, + const std::shared_ptr& task, + RuntimeSchedulerTimePoint currentTime) const { + auto didUserCallbackTimeout = task->expirationTime <= currentTime; + + SystraceSection s( + "RuntimeScheduler::executeTask", + "priority", + serialize(task->priority), + "didUserCallbackTimeout", + didUserCallbackTimeout); + + currentTask_ = task; + currentPriority_ = task->priority; + + auto result = task->execute(runtime, didUserCallbackTimeout); + + if (result.isObject() && result.getObject(runtime).isFunction(runtime)) { + // If the task returned a continuation callback, we re-assign it to the task + // and keep the task in the queue. + task->callback = result.getObject(runtime).getFunction(runtime); + } + + // TODO execute microtasks + // TODO report long tasks + // TODO update rendering +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h new file mode 100644 index 00000000000000..c5eab04528fa7d --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -0,0 +1,176 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { + public: + explicit RuntimeScheduler_Modern( + RuntimeExecutor runtimeExecutor, + std::function now = + RuntimeSchedulerClock::now); + + /* + * Not copyable. + */ + RuntimeScheduler_Modern(const RuntimeScheduler_Modern&) = delete; + RuntimeScheduler_Modern& operator=(const RuntimeScheduler_Modern&) = delete; + + /* + * Not movable. + */ + RuntimeScheduler_Modern(RuntimeScheduler_Modern&&) = delete; + RuntimeScheduler_Modern& operator=(RuntimeScheduler_Modern&&) = delete; + + /* + * Alias for scheduleTask with immediate priority. + * + * To be removed when we finish testing this implementation. + * All callers should use scheduleTask with the right priority afte that. + */ + void scheduleWork(RawCallback&& callback) const noexcept override; + + /* + * Grants access to the runtime synchronously on the caller's thread. + * + * Shouldn't be called directly. it is expected to be used + * by dispatching a synchronous event via event emitter in your native + * component. + */ + void executeNowOnTheSameThread(RawCallback&& callback) override; + + /* + * Adds a JavaScript callback to the priority queue with the given priority. + * Triggers workloop if needed. + */ + std::shared_ptr scheduleTask( + SchedulerPriority priority, + jsi::Function&& callback) const noexcept override; + + /* + * Adds a custom callback to the priority queue with the given priority. + * Triggers workloop if needed. + */ + std::shared_ptr scheduleTask( + SchedulerPriority priority, + RawCallback&& callback) const noexcept override; + + /* + * Cancelled task will never be executed. + * + * Operates on JSI object. + * Thread synchronization must be enforced externally. + */ + void cancelTask(Task& task) noexcept override; + + /* + * Return value indicates if host platform has a pending access to the + * runtime. + * + * Can be called from any thread. + */ + bool getShouldYield() const noexcept override; + + /* + * Return value informs if the current task is executed inside synchronous + * block. + * + * Can be called from any thread. + */ + bool getIsSynchronous() const noexcept override; + + /* + * Returns value of currently executed task. Designed to be called from React. + * + * Thread synchronization must be enforced externally. + */ + SchedulerPriority getCurrentPriorityLevel() const noexcept override; + + /* + * Returns current monotonic time. This time is not related to wall clock + * time. + * + * Can be called from any thread. + */ + RuntimeSchedulerTimePoint now() const noexcept override; + + /* + * Expired task is a task that should have been already executed. Designed to + * be called in the event pipeline after an event is dispatched to React. + * React may schedule events with immediate priority which need to be handled + * before the next event is sent to React. + * + * Thread synchronization must be enforced externally. + * + * TODO remove when we add support for microtasks + */ + void callExpiredTasks(jsi::Runtime& runtime) override; + + private: + mutable std::atomic syncTaskRequests_{0}; + + mutable std::priority_queue< + std::shared_ptr, + std::vector>, + TaskPriorityComparer> + taskQueue_; + + mutable std::shared_ptr currentTask_; + + /** + * This protects the access to `taskQueue_` and `isWorkLoopScheduled_`. + */ + mutable std::shared_mutex schedulingMutex_; + + const RuntimeExecutor runtimeExecutor_; + mutable SchedulerPriority currentPriority_{SchedulerPriority::NormalPriority}; + + mutable std::atomic_bool isSynchronous_{false}; + + void scheduleWorkLoop() const; + void startWorkLoop(jsi::Runtime& runtime, bool onlyExpired) const; + + std::shared_ptr selectTask( + RuntimeSchedulerTimePoint currentTime, + bool onlyExpired) const; + + void scheduleTask(std::shared_ptr task) const; + + /** + * Follows all the steps necessary to execute the given task (in the future, + * this will include executing microtasks, flushing rendering work, etc.) + */ + void executeTask( + jsi::Runtime& runtime, + const std::shared_ptr& task, + RuntimeSchedulerTimePoint currentTime) const; + + /* + * Returns a time point representing the current point in time. May be called + * from multiple threads. + */ + std::function now_; + + /* + * Flag indicating if callback on JavaScript queue has been + * scheduled. + */ + mutable bool isWorkLoopScheduled_{false}; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/Task.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/Task.h index 0d292e2ad1d8f0..15155c60c82927 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/Task.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/Task.h @@ -17,6 +17,7 @@ namespace facebook::react { class RuntimeScheduler_Legacy; +class RuntimeScheduler_Modern; class TaskPriorityComparer; using RawCallback = std::function; @@ -34,6 +35,7 @@ struct Task final : public jsi::NativeState { private: friend RuntimeScheduler_Legacy; + friend RuntimeScheduler_Modern; friend TaskPriorityComparer; SchedulerPriority priority; diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp index b274cea0b89969..749d4e77a78dd5 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "StubClock.h" #include "StubErrorUtils.h" @@ -19,7 +20,7 @@ namespace facebook::react { using namespace std::chrono_literals; -class RuntimeSchedulerTest : public testing::Test { +class RuntimeSchedulerTest : public testing::TestWithParam { protected: void SetUp() override { hostFunctionCallCount_ = 0; @@ -41,8 +42,10 @@ class RuntimeSchedulerTest : public testing::Test { return stubClock_->getNow(); }; - runtimeScheduler_ = - std::make_unique(runtimeExecutor, stubNow); + auto useModernRuntimeScheduler = GetParam(); + + runtimeScheduler_ = std::make_unique( + runtimeExecutor, useModernRuntimeScheduler, stubNow); } jsi::Function createHostFunctionFromLambda( @@ -71,7 +74,7 @@ class RuntimeSchedulerTest : public testing::Test { std::shared_ptr stubErrorUtils_; }; -TEST_F(RuntimeSchedulerTest, now) { +TEST_P(RuntimeSchedulerTest, now) { stubClock_->setTimePoint(1ms); EXPECT_EQ(runtimeScheduler_->now(), RuntimeSchedulerTimePoint(1ms)); @@ -85,12 +88,12 @@ TEST_F(RuntimeSchedulerTest, now) { EXPECT_EQ(runtimeScheduler_->now(), RuntimeSchedulerTimePoint(6011ms)); } -TEST_F(RuntimeSchedulerTest, getShouldYield) { +TEST_P(RuntimeSchedulerTest, getShouldYield) { // Always returns false for now. EXPECT_FALSE(runtimeScheduler_->getShouldYield()); } -TEST_F(RuntimeSchedulerTest, scheduleSingleTask) { +TEST_P(RuntimeSchedulerTest, scheduleSingleTask) { bool didRunTask = false; auto callback = createHostFunctionFromLambda([&didRunTask](bool didUserCallbackTimeout) { @@ -111,7 +114,7 @@ TEST_F(RuntimeSchedulerTest, scheduleSingleTask) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, scheduleImmediatePriorityTask) { +TEST_P(RuntimeSchedulerTest, scheduleImmediatePriorityTask) { bool didRunTask = false; auto callback = createHostFunctionFromLambda([&didRunTask](bool didUserCallbackTimeout) { @@ -132,7 +135,7 @@ TEST_F(RuntimeSchedulerTest, scheduleImmediatePriorityTask) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, taskExpiration) { +TEST_P(RuntimeSchedulerTest, taskExpiration) { bool didRunTask = false; auto callback = createHostFunctionFromLambda([&didRunTask](bool didUserCallbackTimeout) { @@ -156,7 +159,7 @@ TEST_F(RuntimeSchedulerTest, taskExpiration) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, scheduleTwoTasksWithSamePriority) { +TEST_P(RuntimeSchedulerTest, scheduleTwoTasksWithSamePriority) { uint firstTaskCallOrder = 0; auto callbackOne = createHostFunctionFromLambda( [this, &firstTaskCallOrder](bool /*unused*/) { @@ -189,7 +192,7 @@ TEST_F(RuntimeSchedulerTest, scheduleTwoTasksWithSamePriority) { EXPECT_EQ(hostFunctionCallCount_, 2); } -TEST_F(RuntimeSchedulerTest, scheduleTwoTasksWithDifferentPriorities) { +TEST_P(RuntimeSchedulerTest, scheduleTwoTasksWithDifferentPriorities) { uint lowPriorityTaskCallOrder = 0; auto callbackOne = createHostFunctionFromLambda( [this, &lowPriorityTaskCallOrder](bool /*unused*/) { @@ -222,7 +225,7 @@ TEST_F(RuntimeSchedulerTest, scheduleTwoTasksWithDifferentPriorities) { EXPECT_EQ(hostFunctionCallCount_, 2); } -TEST_F(RuntimeSchedulerTest, cancelTask) { +TEST_P(RuntimeSchedulerTest, cancelTask) { bool didRunTask = false; auto callback = createHostFunctionFromLambda([&didRunTask](bool /*unused*/) { didRunTask = true; @@ -243,7 +246,7 @@ TEST_F(RuntimeSchedulerTest, cancelTask) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, continuationTask) { +TEST_P(RuntimeSchedulerTest, continuationTask) { bool didRunTask = false; bool didContinuationTask = false; @@ -275,7 +278,7 @@ TEST_F(RuntimeSchedulerTest, continuationTask) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, getCurrentPriorityLevel) { +TEST_P(RuntimeSchedulerTest, getCurrentPriorityLevel) { auto callback = createHostFunctionFromLambda([this](bool /*didUserCallbackTimeout*/) { EXPECT_EQ( @@ -315,7 +318,7 @@ TEST_F(RuntimeSchedulerTest, getCurrentPriorityLevel) { SchedulerPriority::NormalPriority); } -TEST_F(RuntimeSchedulerTest, scheduleWorkWithYielding) { +TEST_P(RuntimeSchedulerTest, scheduleWorkWithYielding) { bool wasCalled = false; runtimeScheduler_->scheduleWork( [&](const jsi::Runtime& /*unused*/) { wasCalled = true; }); @@ -333,7 +336,12 @@ TEST_F(RuntimeSchedulerTest, scheduleWorkWithYielding) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, normalTaskYieldsToPlatformEvent) { +TEST_P(RuntimeSchedulerTest, normalTaskYieldsToPlatformEvent) { + // Only for legacy runtime scheduler + if (GetParam()) { + return; + } + bool didRunJavaScriptTask = false; bool didRunPlatformWork = false; @@ -360,7 +368,12 @@ TEST_F(RuntimeSchedulerTest, normalTaskYieldsToPlatformEvent) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, expiredTaskDoesntYieldToPlatformEvent) { +TEST_P(RuntimeSchedulerTest, expiredTaskDoesntYieldToPlatformEvent) { + // Only for legacy runtime scheduler + if (GetParam()) { + return; + } + bool didRunJavaScriptTask = false; bool didRunPlatformWork = false; @@ -388,7 +401,12 @@ TEST_F(RuntimeSchedulerTest, expiredTaskDoesntYieldToPlatformEvent) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, immediateTaskDoesntYieldToPlatformEvent) { +TEST_P(RuntimeSchedulerTest, immediateTaskDoesntYieldToPlatformEvent) { + // Only for legacy runtime scheduler + if (GetParam()) { + return; + } + bool didRunJavaScriptTask = false; bool didRunPlatformWork = false; @@ -414,7 +432,184 @@ TEST_F(RuntimeSchedulerTest, immediateTaskDoesntYieldToPlatformEvent) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, scheduleTaskFromTask) { +TEST_P(RuntimeSchedulerTest, scheduleTaskWithYielding) { + // Only for modern runtime scheduler + if (!GetParam()) { + return; + } + + bool wasCalled = false; + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, + [&](const jsi::Runtime& /*unused*/) { wasCalled = true; }); + + EXPECT_FALSE(wasCalled); + + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + + EXPECT_EQ(stubQueue_->size(), 1); + + stubQueue_->tick(); + + EXPECT_TRUE(wasCalled); + EXPECT_FALSE(runtimeScheduler_->getShouldYield()); + EXPECT_EQ(stubQueue_->size(), 0); +} + +TEST_P(RuntimeSchedulerTest, normalTaskYieldsToSynchronousAccess) { + // Only for modern runtime scheduler + if (!GetParam()) { + return; + } + + uint syncTaskExecutionCount = 0; + uint normalTaskExecutionCount = 0; + + std::binary_semaphore signalTaskToSync{0}; + + // No tasks scheduled, not yielding necessary. + EXPECT_FALSE(runtimeScheduler_->getShouldYield()); + + std::thread t1([this, &signalTaskToSync, &syncTaskExecutionCount]() { + // Wait for the normal task to start executing + signalTaskToSync.acquire(); + + runtimeScheduler_->executeNowOnTheSameThread( + [&syncTaskExecutionCount](jsi::Runtime& /*runtime*/) { + syncTaskExecutionCount++; + }); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, + [this, &normalTaskExecutionCount, &signalTaskToSync]( + jsi::Runtime& /*unused*/) { + // Notify the "main" thread that it should request sync access. + signalTaskToSync.release(); + + // Wait for the sync access to request yielding + while (!runtimeScheduler_->getShouldYield()) { + // This is just to avoid the loop to take significant CPU while + // waiting for the yield request. + std::chrono::duration timespan(10); + std::this_thread::sleep_for(timespan); + } + + normalTaskExecutionCount++; + }); + + EXPECT_EQ(normalTaskExecutionCount, 0); + EXPECT_EQ(syncTaskExecutionCount, 0); + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + // Only the normal task has been scheduled at this point. + EXPECT_EQ(stubQueue_->size(), 1); + + // This will start executing the normal task, which will unblock the thread + // that will request sync access + stubQueue_->tick(); + + // The normal task yielded to the synchronous access, which is now waiting + // to execute + EXPECT_EQ(normalTaskExecutionCount, 1); + EXPECT_EQ(syncTaskExecutionCount, 0); + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + EXPECT_EQ(stubQueue_->size(), 1); + + // Execute the synchronous access and wait for completion + stubQueue_->tick(); + t1.join(); + + EXPECT_EQ(syncTaskExecutionCount, 1); + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + // The previous task is still in the queue (although it was executed already). + EXPECT_EQ(stubQueue_->size(), 1); + + // Just empty the queue + stubQueue_->tick(); + + EXPECT_EQ(normalTaskExecutionCount, 1); // It hasn't executed again + EXPECT_FALSE(runtimeScheduler_->getShouldYield()); + EXPECT_EQ(stubQueue_->size(), 0); +} + +TEST_P(RuntimeSchedulerTest, immediateTaskYieldsToSynchronousAccess) { + // Only for modern runtime scheduler + if (!GetParam()) { + return; + } + + uint syncTaskExecutionCount = 0; + uint normalTaskExecutionCount = 0; + + std::binary_semaphore signalTaskToSync{0}; + + // No tasks scheduled, not yielding necessary. + EXPECT_FALSE(runtimeScheduler_->getShouldYield()); + + std::thread t1([this, &signalTaskToSync, &syncTaskExecutionCount]() { + // Wait for the normal task to start executing + signalTaskToSync.acquire(); + + runtimeScheduler_->executeNowOnTheSameThread( + [&syncTaskExecutionCount](jsi::Runtime& /*runtime*/) { + syncTaskExecutionCount++; + }); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::ImmediatePriority, + [this, &normalTaskExecutionCount, &signalTaskToSync]( + jsi::Runtime& /*unused*/) { + // Notify the "main" thread that it should request sync access. + signalTaskToSync.release(); + + // Wait for the sync access to request yielding + while (!runtimeScheduler_->getShouldYield()) { + // This is just to avoid the loop to take significant CPU while + // waiting for the yield request. + std::chrono::duration timespan(10); + std::this_thread::sleep_for(timespan); + } + + normalTaskExecutionCount++; + }); + + EXPECT_EQ(normalTaskExecutionCount, 0); + EXPECT_EQ(syncTaskExecutionCount, 0); + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + // Only the normal task has been scheduled at this point. + EXPECT_EQ(stubQueue_->size(), 1); + + // This will start executing the normal task, which will unblock the thread + // that will request sync access + stubQueue_->tick(); + + // The normal task yielded to the synchronous access, which is now waiting + // to execute + EXPECT_EQ(normalTaskExecutionCount, 1); + EXPECT_EQ(syncTaskExecutionCount, 0); + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + EXPECT_EQ(stubQueue_->size(), 1); + + // Execute the synchronous access and wait for completion + stubQueue_->tick(); + t1.join(); + + EXPECT_EQ(syncTaskExecutionCount, 1); + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + // The previous task is still in the queue (although it was executed already), + // so the sync task scheduled the work loop to process it. + EXPECT_EQ(stubQueue_->size(), 1); + + // Just empty the queue + stubQueue_->tick(); + + EXPECT_EQ(normalTaskExecutionCount, 1); // It hasn't executed again + EXPECT_FALSE(runtimeScheduler_->getShouldYield()); + EXPECT_EQ(stubQueue_->size(), 0); +} + +TEST_P(RuntimeSchedulerTest, scheduleTaskFromTask) { bool didRunFirstTask = false; bool didRunSecondTask = false; auto firstCallback = createHostFunctionFromLambda( @@ -448,7 +643,7 @@ TEST_F(RuntimeSchedulerTest, scheduleTaskFromTask) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, handlingError) { +TEST_P(RuntimeSchedulerTest, handlingError) { bool didRunTask = false; auto firstCallback = createHostFunctionFromLambda([this, &didRunTask](bool /*unused*/) { @@ -470,7 +665,7 @@ TEST_F(RuntimeSchedulerTest, handlingError) { EXPECT_EQ(stubErrorUtils_->getReportFatalCallCount(), 1); } -TEST_F(RuntimeSchedulerTest, basicSameThreadExecution) { +TEST_P(RuntimeSchedulerTest, basicSameThreadExecution) { bool didRunSynchronousTask = false; std::thread t1([this, &didRunSynchronousTask]() { runtimeScheduler_->executeNowOnTheSameThread( @@ -494,7 +689,7 @@ TEST_F(RuntimeSchedulerTest, basicSameThreadExecution) { EXPECT_TRUE(didRunSynchronousTask); } -TEST_F(RuntimeSchedulerTest, sameThreadTaskCreatesImmediatePriorityTask) { +TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesImmediatePriorityTask) { bool didRunSynchronousTask = false; bool didRunSubsequentTask = false; std::thread t1([this, &didRunSynchronousTask, &didRunSubsequentTask]() { @@ -532,7 +727,7 @@ TEST_F(RuntimeSchedulerTest, sameThreadTaskCreatesImmediatePriorityTask) { EXPECT_TRUE(didRunSubsequentTask); } -TEST_F(RuntimeSchedulerTest, sameThreadTaskCreatesLowPriorityTask) { +TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesLowPriorityTask) { bool didRunSynchronousTask = false; bool didRunSubsequentTask = false; std::thread t1([this, &didRunSynchronousTask, &didRunSubsequentTask]() { @@ -550,7 +745,11 @@ TEST_F(RuntimeSchedulerTest, sameThreadTaskCreatesLowPriorityTask) { runtimeScheduler_->scheduleTask( SchedulerPriority::LowPriority, std::move(callback)); - runtimeScheduler_->callExpiredTasks(runtime); + + // Only for legacy runtime scheduler + if (!GetParam()) { + runtimeScheduler_->callExpiredTasks(runtime); + } EXPECT_FALSE(didRunSubsequentTask); }); @@ -579,7 +778,12 @@ TEST_F(RuntimeSchedulerTest, sameThreadTaskCreatesLowPriorityTask) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_F(RuntimeSchedulerTest, twoThreadsRequestAccessToTheRuntime) { +TEST_P(RuntimeSchedulerTest, legacyTwoThreadsRequestAccessToTheRuntime) { + // Only for legacy runtime scheduler + if (GetParam()) { + return; + } + bool didRunSynchronousTask = false; bool didRunWork = false; @@ -617,4 +821,91 @@ TEST_F(RuntimeSchedulerTest, twoThreadsRequestAccessToTheRuntime) { EXPECT_FALSE(runtimeScheduler_->getShouldYield()); } +TEST_P(RuntimeSchedulerTest, modernTwoThreadsRequestAccessToTheRuntime) { + // Only for modern runtime scheduler + if (!GetParam()) { + return; + } + + std::binary_semaphore signalTask1ToScheduleTask2{0}; + std::binary_semaphore signalTask2ToResumeTask1{0}; + + bool didRunSynchronousTask1 = false; + bool didRunSynchronousTask2 = false; + + std::thread t1([this, + &didRunSynchronousTask1, + &signalTask1ToScheduleTask2, + &signalTask2ToResumeTask1]() { + runtimeScheduler_->executeNowOnTheSameThread( + [&didRunSynchronousTask1, + &signalTask1ToScheduleTask2, + &signalTask2ToResumeTask1](jsi::Runtime& /*runtime*/) { + // Notify that the second task can be scheduled. + signalTask1ToScheduleTask2.release(); + + // Wait for the second task to be scheduled before finishing this task + signalTask2ToResumeTask1.acquire(); + + didRunSynchronousTask1 = true; + }); + }); + + std::thread t2([this, + &didRunSynchronousTask2, + &signalTask1ToScheduleTask2, + &signalTask2ToResumeTask1]() { + // Wait for the first task to start executing before scheduling this one. + signalTask1ToScheduleTask2.acquire(); + + // Notify the first task that it can resume execution. + // As we can't do this after the task this from thread has been scheduled + // (because it's synchronous), we can just do a short wait instead in a new + // thread. + std::thread t3([&signalTask2ToResumeTask1]() { + std::chrono::duration timespan(50); + std::this_thread::sleep_for(timespan); + signalTask2ToResumeTask1.release(); + }); + + runtimeScheduler_->executeNowOnTheSameThread( + [&didRunSynchronousTask2](jsi::Runtime& /*runtime*/) { + didRunSynchronousTask2 = true; + }); + + t3.join(); + }); + + auto hasTask = stubQueue_->waitForTasks(1); + + EXPECT_TRUE(hasTask); + EXPECT_FALSE(didRunSynchronousTask1); + EXPECT_FALSE(didRunSynchronousTask2); + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + // Only the first task would have been scheduled at this point. + EXPECT_EQ(stubQueue_->size(), 1); + + stubQueue_->tick(); + t1.join(); + + EXPECT_TRUE(didRunSynchronousTask1); + EXPECT_FALSE(didRunSynchronousTask2); + EXPECT_TRUE(runtimeScheduler_->getShouldYield()); + // Now we've scheduled the second task. + EXPECT_EQ(stubQueue_->size(), 1); + + stubQueue_->tick(); + t2.join(); + + EXPECT_TRUE(didRunSynchronousTask1); + EXPECT_TRUE(didRunSynchronousTask2); + EXPECT_FALSE(runtimeScheduler_->getShouldYield()); + EXPECT_EQ(stubQueue_->size(), 0); +} + +INSTANTIATE_TEST_SUITE_P( + UseModernRuntimeScheduler, + RuntimeSchedulerTest, + testing::Values(false, true)); + } // namespace facebook::react