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