diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp index 0a6d3608dc2bea..df44f0523a6639 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp @@ -84,4 +84,10 @@ void RuntimeScheduler::callExpiredTasks(jsi::Runtime& runtime) { return runtimeSchedulerImpl_->callExpiredTasks(runtime); } +void RuntimeScheduler::scheduleRenderingUpdate( + RuntimeSchedulerRenderingUpdate&& renderingUpdate) { + return runtimeSchedulerImpl_->scheduleRenderingUpdate( + std::move(renderingUpdate)); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h index 3152dfc882f463..40482a9872a838 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h @@ -13,6 +13,8 @@ namespace facebook::react { +using RuntimeSchedulerRenderingUpdate = std::function; + // This is a temporary abstract class for RuntimeScheduler forks to implement // (and use them interchangeably). class RuntimeSchedulerBase { @@ -32,6 +34,8 @@ class RuntimeSchedulerBase { virtual SchedulerPriority getCurrentPriorityLevel() const noexcept = 0; virtual RuntimeSchedulerTimePoint now() const noexcept = 0; virtual void callExpiredTasks(jsi::Runtime& runtime) = 0; + virtual void scheduleRenderingUpdate( + RuntimeSchedulerRenderingUpdate&& renderingUpdate) = 0; }; // This is a proxy for RuntimeScheduler implementation, which will be selected @@ -130,6 +134,9 @@ class RuntimeScheduler final : RuntimeSchedulerBase { */ void callExpiredTasks(jsi::Runtime& runtime) override; + void scheduleRenderingUpdate( + RuntimeSchedulerRenderingUpdate&& renderingUpdate) override; + private: // Actual implementation, stored as a unique pointer to simplify memory // management. 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 316929c8b0b9fe..812eac2456d8c0 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp @@ -142,6 +142,15 @@ void RuntimeScheduler_Legacy::callExpiredTasks(jsi::Runtime& runtime) { currentPriority_ = previousPriority; } +void RuntimeScheduler_Legacy::scheduleRenderingUpdate( + RuntimeSchedulerRenderingUpdate&& renderingUpdate) { + SystraceSection s("RuntimeScheduler::scheduleRenderingUpdate"); + + if (renderingUpdate != nullptr) { + renderingUpdate(); + } +} + #pragma mark - Private void RuntimeScheduler_Legacy::scheduleWorkLoopIfNecessary() { 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 f15ab40b364cae..d4ad98d59a1f43 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h @@ -110,6 +110,9 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { */ void callExpiredTasks(jsi::Runtime& runtime) override; + void scheduleRenderingUpdate( + RuntimeSchedulerRenderingUpdate&& renderingUpdate) override; + private: std::priority_queue< std::shared_ptr, 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 e9e8cd89ea670b..033d5a2219e451 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -17,7 +17,13 @@ namespace facebook::react { namespace { -// Looping on \c drainMicrotasks until it completes or hits the retries bound. +/** + * This is partially equivalent to the "Perform a microtask checkpoint" step in + * the Web event loop. See + * https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint. + * + * Iterates on \c drainMicrotasks until it completes or hits the retries bound. + */ void executeMicrotasks(jsi::Runtime& runtime) { SystraceSection s("RuntimeScheduler::executeMicrotasks"); @@ -175,6 +181,19 @@ void RuntimeScheduler_Modern::callExpiredTasks(jsi::Runtime& runtime) { startWorkLoop(runtime, true); } +void RuntimeScheduler_Modern::scheduleRenderingUpdate( + RuntimeSchedulerRenderingUpdate&& renderingUpdate) { + SystraceSection s("RuntimeScheduler::scheduleRenderingUpdate"); + + if (CoreFeatures::blockPaintForUseLayoutEffect) { + pendingRenderingUpdates_.push(renderingUpdate); + } else { + if (renderingUpdate != nullptr) { + renderingUpdate(); + } + } +} + #pragma mark - Private void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr task) { @@ -278,11 +297,31 @@ void RuntimeScheduler_Modern::executeTask( executeMacrotask(runtime, task, didUserCallbackTimeout); if (CoreFeatures::enableMicrotasks) { + // "Perform a microtask checkpoint" step. executeMicrotasks(runtime); } - // TODO report long tasks - // TODO update rendering + if (CoreFeatures::blockPaintForUseLayoutEffect) { + // "Update the rendering" step. + updateRendering(); + } +} + +/** + * This is partially equivalent to the "Update the rendering" step in the Web + * event loop. See + * https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering. + */ +void RuntimeScheduler_Modern::updateRendering() { + SystraceSection s("RuntimeScheduler::updateRendering"); + + while (!pendingRenderingUpdates_.empty()) { + auto& pendingRenderingUpdate = pendingRenderingUpdates_.front(); + if (pendingRenderingUpdate != nullptr) { + pendingRenderingUpdate(); + } + pendingRenderingUpdates_.pop(); + } } void RuntimeScheduler_Modern::executeMacrotask( 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 609ea34b76bd7b..34c8eb4cf16cbf 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -121,6 +121,15 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { */ void callExpiredTasks(jsi::Runtime& runtime) override; + /** + * Schedules a function that notifies or applies UI changes in the host + * platform, to be executed during the "Update the rendering" step of the + * event loop. If the step is not enabled, the function is executed + * immediately. + */ + void scheduleRenderingUpdate( + RuntimeSchedulerRenderingUpdate&& renderingUpdate) override; + private: std::atomic syncTaskRequests_{0}; @@ -167,6 +176,8 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { std::shared_ptr task, bool didUserCallbackTimeout) const; + void updateRendering(); + /* * Returns a time point representing the current point in time. May be called * from multiple threads. @@ -178,6 +189,8 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { * scheduled. */ bool isWorkLoopScheduled_{false}; + + std::queue pendingRenderingUpdates_; }; } // namespace facebook::react 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 ced061c61f93f8..8897a223d05b87 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp @@ -124,17 +124,41 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTask) { EXPECT_EQ(stubQueue_->size(), 0); } -TEST_P(RuntimeSchedulerTest, scheduleSingleTaskWithMicrotasks) { +TEST_P(RuntimeSchedulerTest, scheduleNonBatchedRenderingUpdate) { + CoreFeatures::blockPaintForUseLayoutEffect = false; + + bool didRunRenderingUpdate = false; + + runtimeScheduler_->scheduleRenderingUpdate( + [&]() { didRunRenderingUpdate = true; }); + + EXPECT_TRUE(didRunRenderingUpdate); +} + +TEST_P( + RuntimeSchedulerTest, + scheduleSingleTaskWithMicrotasksAndBatchedRenderingUpdate) { // Only for modern runtime scheduler if (!GetParam()) { return; } - bool didRunTask = false; - bool didRunMicrotask = false; + CoreFeatures::blockPaintForUseLayoutEffect = true; + + uint nextOperationPosition = 1; + + uint taskPosition = 0; + uint microtaskPosition = 0; + uint updateRenderingPosition = 0; auto callback = createHostFunctionFromLambda([&](bool /* unused */) { - didRunTask = true; + taskPosition = nextOperationPosition; + nextOperationPosition++; + + runtimeScheduler_->scheduleRenderingUpdate([&]() { + updateRenderingPosition = nextOperationPosition; + nextOperationPosition++; + }); auto microtaskCallback = jsi::Function::createFromHostFunction( *runtime_, @@ -144,7 +168,8 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTaskWithMicrotasks) { const jsi::Value& /*unused*/, const jsi::Value* arguments, size_t /*unused*/) -> jsi::Value { - didRunMicrotask = true; + microtaskPosition = nextOperationPosition; + nextOperationPosition++; return jsi::Value::undefined(); }); @@ -162,13 +187,16 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTaskWithMicrotasks) { runtimeScheduler_->scheduleTask( SchedulerPriority::NormalPriority, std::move(callback)); - EXPECT_FALSE(didRunTask); + EXPECT_EQ(taskPosition, 0); + EXPECT_EQ(microtaskPosition, 0); + EXPECT_EQ(updateRenderingPosition, 0); EXPECT_EQ(stubQueue_->size(), 1); stubQueue_->tick(); - EXPECT_TRUE(didRunTask); - EXPECT_TRUE(didRunMicrotask); + EXPECT_EQ(taskPosition, 1); + EXPECT_EQ(microtaskPosition, 2); + EXPECT_EQ(updateRenderingPosition, 3); EXPECT_EQ(stubQueue_->size(), 0); } diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index df3c9ada438899..1777b73374dd8e 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -294,24 +294,18 @@ void Scheduler::uiManagerDidFinishTransaction( SystraceSection s("Scheduler::uiManagerDidFinishTransaction"); if (delegate_ != nullptr) { - if (CoreFeatures::blockPaintForUseLayoutEffect) { - auto weakRuntimeScheduler = - contextContainer_->find>( - "RuntimeScheduler"); - auto runtimeScheduler = weakRuntimeScheduler.has_value() - ? weakRuntimeScheduler.value().lock() - : nullptr; - if (runtimeScheduler && !mountSynchronously) { - runtimeScheduler->scheduleTask( - SchedulerPriority::UserBlockingPriority, - [delegate = delegate_, - mountingCoordinator = - std::move(mountingCoordinator)](jsi::Runtime&) { - delegate->schedulerDidFinishTransaction(mountingCoordinator); - }); - } else { - delegate_->schedulerDidFinishTransaction(mountingCoordinator); - } + auto weakRuntimeScheduler = + contextContainer_->find>( + "RuntimeScheduler"); + auto runtimeScheduler = weakRuntimeScheduler.has_value() + ? weakRuntimeScheduler.value().lock() + : nullptr; + if (runtimeScheduler && !mountSynchronously) { + runtimeScheduler->scheduleRenderingUpdate( + [delegate = delegate_, + mountingCoordinator = std::move(mountingCoordinator)]() { + delegate->schedulerDidFinishTransaction(mountingCoordinator); + }); } else { delegate_->schedulerDidFinishTransaction(mountingCoordinator); }