From 9160d6dfa09ae59020b8952aff8df90f0ed6deb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 23 Oct 2023 04:13:43 -0700 Subject: [PATCH] Implement support for microtasks in RuntimeScheduler (#41084) Summary: Adds support for executing microtasks in `RuntimeScheduler`, the same way we did in `JSIExecutor` before (removed in D49536251 / https://github.com/facebook/react-native/pull/40870) but now after each actual task in the scheduler. When we use microtasks in the scheduler, we ignore calls to execute expired tasks (which was used to call "React Native microtasks" that we had before). Those should now be regular microtasks in the runtime. This is gated behind a feature flag until we've tested this broadly. This is going to be tested in Hermes but we need to add support for microtasks in JSC (which has a no-op in its JSI interface). Changelog: [internal] Reviewed By: sammy-SC Differential Revision: D49536262 --- .../React-runtimescheduler.podspec | 1 + .../RuntimeScheduler_Modern.cpp | 56 ++++++++++- .../RuntimeScheduler_Modern.h | 11 ++- .../tests/RuntimeSchedulerTest.cpp | 93 ++++++++++++++++--- .../react/runtime/ReactInstance.cpp | 11 ++- .../react/runtime/TimerManager.cpp | 3 +- .../ReactCommon/react/utils/CoreFeatures.cpp | 1 + .../ReactCommon/react/utils/CoreFeatures.h | 4 + 8 files changed, 155 insertions(+), 25 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec index 915f5326bbcc11..89a62a9a85a4c5 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec @@ -54,6 +54,7 @@ Pod::Spec.new do |s| s.dependency "React-jsi" s.dependency "React-runtimeexecutor" s.dependency "React-callinvoker" + s.dependency "React-cxxreact" s.dependency "React-debug" s.dependency "React-rendererdebug" s.dependency "React-utils" diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp index 9f77961d1a914c..e9e8cd89ea670b 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -8,12 +8,42 @@ #include "RuntimeScheduler_Modern.h" #include "SchedulerPriorityUtils.h" +#include #include +#include #include #include "ErrorUtils.h" namespace facebook::react { +namespace { +// Looping on \c drainMicrotasks until it completes or hits the retries bound. +void executeMicrotasks(jsi::Runtime& runtime) { + SystraceSection s("RuntimeScheduler::executeMicrotasks"); + + uint8_t retries = 0; + // A heuristic number to guard infinite or absurd numbers of retries. + const static unsigned int kRetriesBound = 255; + + while (retries < kRetriesBound) { + try { + // The default behavior of \c drainMicrotasks is unbounded execution. + // We may want to make it bounded in the future. + if (runtime.drainMicrotasks()) { + break; + } + } catch (jsi::JSError& error) { + handleJSError(runtime, error, true); + } + retries++; + } + + if (retries == kRetriesBound) { + throw std::runtime_error("Hits microtasks retries bound."); + } +} +} // namespace + #pragma mark - Public RuntimeScheduler_Modern::RuntimeScheduler_Modern( @@ -135,8 +165,12 @@ void RuntimeScheduler_Modern::executeNowOnTheSameThread( } } -// This will be replaced by microtasks void RuntimeScheduler_Modern::callExpiredTasks(jsi::Runtime& runtime) { + // If we have first-class support for microtasks, this a no-op. + if (CoreFeatures::enableMicrotasks) { + return; + } + SystraceSection s("RuntimeScheduler::callExpiredTasks"); startWorkLoop(runtime, true); } @@ -241,6 +275,22 @@ void RuntimeScheduler_Modern::executeTask( currentTask_ = task; currentPriority_ = task->priority; + executeMacrotask(runtime, task, didUserCallbackTimeout); + + if (CoreFeatures::enableMicrotasks) { + executeMicrotasks(runtime); + } + + // TODO report long tasks + // TODO update rendering +} + +void RuntimeScheduler_Modern::executeMacrotask( + jsi::Runtime& runtime, + std::shared_ptr task, + bool didUserCallbackTimeout) const { + SystraceSection s("RuntimeScheduler::executeMacrotask"); + auto result = task->execute(runtime, didUserCallbackTimeout); if (result.isObject() && result.getObject(runtime).isFunction(runtime)) { @@ -248,10 +298,6 @@ void RuntimeScheduler_Modern::executeTask( // 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 index a654a3722ba888..609ea34b76bd7b 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -152,14 +152,21 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { void scheduleTask(std::shared_ptr task); /** - * Follows all the steps necessary to execute the given task (in the future, - * this will include executing microtasks, flushing rendering work, etc.) + * Follows all the steps necessary to execute the given task. + * Depending on feature flags, this could also execute its microtasks. + * In the future, this will include other steps in the Web event loop, like + * updating the UI in native, executing resize observer callbacks, etc. */ void executeTask( jsi::Runtime& runtime, const std::shared_ptr& task, RuntimeSchedulerTimePoint currentTime); + void executeMacrotask( + jsi::Runtime& runtime, + std::shared_ptr task, + bool didUserCallbackTimeout) const; + /* * Returns a time point representing the current point in time. May be called * from multiple threads. 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 b279a425b81459..ced061c61f93f8 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -24,7 +25,18 @@ class RuntimeSchedulerTest : public testing::TestWithParam { protected: void SetUp() override { hostFunctionCallCount_ = 0; - runtime_ = facebook::hermes::makeHermesRuntime(); + + auto useModernRuntimeScheduler = GetParam(); + + CoreFeatures::enableMicrotasks = useModernRuntimeScheduler; + + // Configuration that enables microtasks + ::hermes::vm::RuntimeConfig::Builder runtimeConfigBuilder = + ::hermes::vm::RuntimeConfig::Builder().withMicrotaskQueue( + useModernRuntimeScheduler); + + runtime_ = + facebook::hermes::makeHermesRuntime(runtimeConfigBuilder.build()); stubErrorUtils_ = StubErrorUtils::createAndInstallIfNeeded(*runtime_); stubQueue_ = std::make_unique(); @@ -42,8 +54,6 @@ class RuntimeSchedulerTest : public testing::TestWithParam { return stubClock_->getNow(); }; - auto useModernRuntimeScheduler = GetParam(); - runtimeScheduler_ = std::make_unique( runtimeExecutor, useModernRuntimeScheduler, stubNow); } @@ -114,6 +124,54 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTask) { EXPECT_EQ(stubQueue_->size(), 0); } +TEST_P(RuntimeSchedulerTest, scheduleSingleTaskWithMicrotasks) { + // Only for modern runtime scheduler + if (!GetParam()) { + return; + } + + bool didRunTask = false; + bool didRunMicrotask = false; + + auto callback = createHostFunctionFromLambda([&](bool /* unused */) { + didRunTask = true; + + auto microtaskCallback = jsi::Function::createFromHostFunction( + *runtime_, + jsi::PropNameID::forUtf8(*runtime_, ""), + 3, + [&](jsi::Runtime& /*unused*/, + const jsi::Value& /*unused*/, + const jsi::Value* arguments, + size_t /*unused*/) -> jsi::Value { + didRunMicrotask = true; + return jsi::Value::undefined(); + }); + + // Hermes doesn't expose a C++ API to schedule microtasks, so we just access + // the API that it exposes to JS. + auto global = runtime_->global(); + auto enqueueJobFn = global.getPropertyAsObject(*runtime_, "HermesInternal") + .getPropertyAsFunction(*runtime_, "enqueueJob"); + + enqueueJobFn.call(*runtime_, std::move(microtaskCallback)); + + return jsi::Value::undefined(); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, std::move(callback)); + + EXPECT_FALSE(didRunTask); + EXPECT_EQ(stubQueue_->size(), 1); + + stubQueue_->tick(); + + EXPECT_TRUE(didRunTask); + EXPECT_TRUE(didRunMicrotask); + EXPECT_EQ(stubQueue_->size(), 0); +} + TEST_P(RuntimeSchedulerTest, scheduleImmediatePriorityTask) { bool didRunTask = false; auto callback = @@ -521,7 +579,8 @@ TEST_P(RuntimeSchedulerTest, normalTaskYieldsToSynchronousAccess) { EXPECT_EQ(syncTaskExecutionCount, 1); EXPECT_TRUE(runtimeScheduler_->getShouldYield()); - // The previous task is still in the queue (although it was executed already). + // The previous task is still in the queue (although it was executed + // already). EXPECT_EQ(stubQueue_->size(), 1); // Just empty the queue @@ -597,8 +656,8 @@ TEST_P(RuntimeSchedulerTest, immediateTaskYieldsToSynchronousAccess) { 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. + // 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 @@ -708,7 +767,7 @@ TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesImmediatePriorityTask) { runtimeScheduler_->scheduleTask( SchedulerPriority::ImmediatePriority, std::move(callback)); - runtimeScheduler_->callExpiredTasks(runtime); + EXPECT_FALSE(didRunSubsequentTask); }); }); @@ -724,7 +783,15 @@ TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesImmediatePriorityTask) { t1.join(); EXPECT_TRUE(didRunSynchronousTask); + EXPECT_FALSE(didRunSubsequentTask); + + EXPECT_EQ(stubQueue_->size(), 1); + + stubQueue_->tick(); + EXPECT_TRUE(didRunSubsequentTask); + + EXPECT_EQ(stubQueue_->size(), 0); } TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesLowPriorityTask) { @@ -746,11 +813,6 @@ TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesLowPriorityTask) { runtimeScheduler_->scheduleTask( SchedulerPriority::LowPriority, std::move(callback)); - // Only for legacy runtime scheduler - if (!GetParam()) { - runtimeScheduler_->callExpiredTasks(runtime); - } - EXPECT_FALSE(didRunSubsequentTask); }); }); @@ -844,7 +906,8 @@ TEST_P(RuntimeSchedulerTest, modernTwoThreadsRequestAccessToTheRuntime) { // Notify that the second task can be scheduled. signalTask1ToScheduleTask2.release(); - // Wait for the second task to be scheduled before finishing this task + // Wait for the second task to be scheduled before finishing this + // task signalTask2ToResumeTask1.acquire(); didRunSynchronousTask1 = true; @@ -860,8 +923,8 @@ TEST_P(RuntimeSchedulerTest, modernTwoThreadsRequestAccessToTheRuntime) { // 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. + // (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); diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp index ecb994e9f3c0a2..d671bace703efc 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -65,8 +66,14 @@ ReactInstance::ReactInstance( SystraceSection s("ReactInstance::_runtimeExecutor[Callback]"); try { callback(*strongRuntime); - if (auto strongTimerManager = weakTimerManager.lock()) { - strongTimerManager->callReactNativeMicrotasks(*strongRuntime); + + // If we have first-class support for microtasks, + // they would've been called as part of the previous callback. + if (!CoreFeatures::enableMicrotasks) { + if (auto strongTimerManager = weakTimerManager.lock()) { + strongTimerManager->callReactNativeMicrotasks( + *strongRuntime); + } } } catch (jsi::JSError& originalError) { handleJSError(*strongRuntime, originalError, true); diff --git a/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp b/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp index 3eca065fbb80e0..d8b33ddb648ff7 100644 --- a/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp +++ b/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp @@ -148,7 +148,8 @@ void TimerManager::callTimer(uint32_t timerID) { void TimerManager::attachGlobals(jsi::Runtime& runtime) { // Install host functions for timers. // TODO (T45786383): Add missing timer functions from JSTimers - // TODL (T96212789): Skip immediate APIs when JSVM microtask queue is used. + // TODO (T96212789): Remove when JSVM microtask queue is used everywhere in + // bridgeless mode. This is being overwritten in JS in that case. runtime.global().setProperty( runtime, "setImmediate", diff --git a/packages/react-native/ReactCommon/react/utils/CoreFeatures.cpp b/packages/react-native/ReactCommon/react/utils/CoreFeatures.cpp index cdc24ae9c3b75e..c24e9e1d4c3727 100644 --- a/packages/react-native/ReactCommon/react/utils/CoreFeatures.cpp +++ b/packages/react-native/ReactCommon/react/utils/CoreFeatures.cpp @@ -22,5 +22,6 @@ bool CoreFeatures::enableGranularShadowTreeStateReconciliation = false; bool CoreFeatures::enableDefaultAsyncBatchedPriority = false; bool CoreFeatures::enableClonelessStateProgression = false; bool CoreFeatures::excludeYogaFromRawProps = false; +bool CoreFeatures::enableMicrotasks = false; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/utils/CoreFeatures.h b/packages/react-native/ReactCommon/react/utils/CoreFeatures.h index 62a9a139ab3450..400e1f90509908 100644 --- a/packages/react-native/ReactCommon/react/utils/CoreFeatures.h +++ b/packages/react-native/ReactCommon/react/utils/CoreFeatures.h @@ -63,6 +63,10 @@ class CoreFeatures { // When enabled, rawProps in Props will not include Yoga specific props. static bool excludeYogaFromRawProps; + + // Enables the use of microtasks in Hermes (scheduling) and RuntimeScheduler + // (execution). + static bool enableMicrotasks; }; } // namespace facebook::react