diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp index 24c9f617f02..9228f08edc8 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp @@ -65,7 +65,7 @@ std::shared_ptr RuntimeScheduler::scheduleIdleTask( return runtimeSchedulerImpl_->scheduleIdleTask(std::move(callback), timeout); } -bool RuntimeScheduler::getShouldYield() const noexcept { +bool RuntimeScheduler::getShouldYield() noexcept { return runtimeSchedulerImpl_->getShouldYield(); } diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h index 6cd772e447d..9cf832c259e 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h @@ -41,7 +41,7 @@ class RuntimeSchedulerBase { RuntimeSchedulerTimeout timeout = timeoutForSchedulerPriority( SchedulerPriority::IdlePriority)) noexcept = 0; virtual void cancelTask(Task& task) noexcept = 0; - virtual bool getShouldYield() const noexcept = 0; + virtual bool getShouldYield() noexcept = 0; virtual SchedulerPriority getCurrentPriorityLevel() const noexcept = 0; virtual RuntimeSchedulerTimePoint now() const noexcept = 0; virtual void callExpiredTasks(jsi::Runtime& runtime) = 0; @@ -123,7 +123,7 @@ class RuntimeScheduler final : RuntimeSchedulerBase { * * Can be called from any thread. */ - bool getShouldYield() const noexcept override; + bool getShouldYield() noexcept override; /* * Returns value of currently executed task. Designed to be called from React. 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 5129d416a4f..b1b2afe8bff 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp @@ -98,7 +98,7 @@ std::shared_ptr RuntimeScheduler_Legacy::scheduleIdleTask( return nullptr; } -bool RuntimeScheduler_Legacy::getShouldYield() const noexcept { +bool RuntimeScheduler_Legacy::getShouldYield() noexcept { return runtimeAccessRequests_ > 0; } 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 abd7a13a287..117b36c7ab8 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h @@ -93,7 +93,7 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { * * Can be called from any thread. */ - bool getShouldYield() const noexcept override; + bool getShouldYield() noexcept override; /* * Returns value of currently executed task. Designed to be called from React. 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 2969fb3de6b..969160d0ce1 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -118,9 +118,13 @@ std::shared_ptr RuntimeScheduler_Modern::scheduleIdleTask( return task; } -bool RuntimeScheduler_Modern::getShouldYield() const noexcept { +bool RuntimeScheduler_Modern::getShouldYield() noexcept { std::shared_lock lock(schedulingMutex_); + if (ReactNativeFeatureFlags::enableLongTaskAPI()) { + markYieldingOpportunity(now_()); + } + return syncTaskRequests_ > 0 || (!taskQueue_.empty() && taskQueue_.top().get() != currentTask_); } @@ -309,30 +313,35 @@ void RuntimeScheduler_Modern::runEventLoopTick( RuntimeSchedulerTimePoint taskStartTime) { SystraceSection s("RuntimeScheduler::runEventLoopTick"); + ScopedShadowTreeRevisionLock revisionLock( + shadowTreeRevisionConsistencyManager_); + currentTask_ = &task; currentPriority_ = task.priority; - { - ScopedShadowTreeRevisionLock revisionLock( - shadowTreeRevisionConsistencyManager_); + if (ReactNativeFeatureFlags::enableLongTaskAPI()) { + lastYieldingOpportunity_ = taskStartTime; + longestPeriodWithoutYieldingOpportunity_ = + std::chrono::milliseconds::zero(); + } - auto didUserCallbackTimeout = task.expirationTime <= taskStartTime; - executeTask(runtime, task, didUserCallbackTimeout); + auto didUserCallbackTimeout = task.expirationTime <= taskStartTime; + executeTask(runtime, task, didUserCallbackTimeout); - if (ReactNativeFeatureFlags::enableMicrotasks()) { - // "Perform a microtask checkpoint" step. - performMicrotaskCheckpoint(runtime); - } + if (ReactNativeFeatureFlags::enableMicrotasks()) { + // "Perform a microtask checkpoint" step. + performMicrotaskCheckpoint(runtime); + } - if (ReactNativeFeatureFlags::batchRenderingUpdatesInEventLoop()) { - // "Update the rendering" step. - updateRendering(); - } + if (ReactNativeFeatureFlags::batchRenderingUpdatesInEventLoop()) { + // "Update the rendering" step. + updateRendering(); + } - if (ReactNativeFeatureFlags::enableLongTaskAPI()) { - auto taskEndTime = now_(); - reportLongTasks(task, taskStartTime, taskEndTime); - } + if (ReactNativeFeatureFlags::enableLongTaskAPI()) { + auto taskEndTime = now_(); + markYieldingOpportunity(taskEndTime); + reportLongTasks(task, taskStartTime, taskEndTime); } currentTask_ = nullptr; @@ -428,11 +437,22 @@ void RuntimeScheduler_Modern::reportLongTasks( return; } - auto durationMs = chronoToDOMHighResTimeStamp(endTime - startTime); - if (durationMs >= LONG_TASK_DURATION_THRESHOLD_MS) { + auto checkedDurationMs = + chronoToDOMHighResTimeStamp(longestPeriodWithoutYieldingOpportunity_); + if (checkedDurationMs >= LONG_TASK_DURATION_THRESHOLD_MS) { + auto durationMs = chronoToDOMHighResTimeStamp(endTime - startTime); auto startTimeMs = chronoToDOMHighResTimeStamp(startTime); reporter->logLongTaskEntry(startTimeMs, durationMs); } } +void RuntimeScheduler_Modern::markYieldingOpportunity( + RuntimeSchedulerTimePoint currentTime) { + auto currentPeriod = currentTime - lastYieldingOpportunity_; + if (currentPeriod > longestPeriodWithoutYieldingOpportunity_) { + longestPeriodWithoutYieldingOpportunity_ = currentPeriod; + } + lastYieldingOpportunity_ = currentTime; +} + } // 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 67c01a25365..f380a6974c5 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -102,7 +102,7 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { * * Can be called from any thread. */ - bool getShouldYield() const noexcept override; + bool getShouldYield() noexcept override; /* * Returns value of currently executed task. Designed to be called from React. @@ -157,6 +157,10 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { taskQueue_; Task* currentTask_{}; + RuntimeSchedulerTimePoint lastYieldingOpportunity_; + RuntimeSchedulerDuration longestPeriodWithoutYieldingOpportunity_{}; + + void markYieldingOpportunity(RuntimeSchedulerTimePoint currentTime); /** * This protects the access to `taskQueue_` and `isevent loopScheduled_`. 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 9aa37a14283..d198bc361ac 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp @@ -1232,6 +1232,87 @@ TEST_P(RuntimeSchedulerTest, reportsLongTasks) { EXPECT_EQ(pendingEntries.entries[0].duration, 50); } +TEST_P(RuntimeSchedulerTest, reportsLongTasksWithYielding) { + // Only for modern runtime scheduler + if (!GetParam()) { + return; + } + + bool didRunTask1 = false; + stubClock_->setTimePoint(10ms); + + auto callback1 = createHostFunctionFromLambda([&](bool /* unused */) { + // The task executes for 80ms, but all the interval between getShouldYield + // are shorter than 50ms + didRunTask1 = true; + + stubClock_->advanceTimeBy(20ms); + + runtimeScheduler_->getShouldYield(); + + stubClock_->advanceTimeBy(20ms); + + runtimeScheduler_->getShouldYield(); + + stubClock_->advanceTimeBy(20ms); + + runtimeScheduler_->getShouldYield(); + + stubClock_->advanceTimeBy(20ms); + + return jsi::Value::undefined(); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, std::move(callback1)); + + stubQueue_->tick(); + + EXPECT_EQ(didRunTask1, 1); + EXPECT_EQ(stubQueue_->size(), 0); + auto pendingEntries = performanceEntryReporter_->popPendingEntries(); + EXPECT_EQ(pendingEntries.entries.size(), 0); + + bool didRunTask2 = false; + stubClock_->setTimePoint(100ms); + + auto callback2 = createHostFunctionFromLambda([&](bool /* unused */) { + // The task executes for 100ms, and one of the intervals is longer than 50. + didRunTask2 = true; + + stubClock_->advanceTimeBy(20ms); + + runtimeScheduler_->getShouldYield(); + + // Long period! + stubClock_->advanceTimeBy(60ms); + + runtimeScheduler_->getShouldYield(); + + stubClock_->advanceTimeBy(20ms); + + runtimeScheduler_->getShouldYield(); + + stubClock_->advanceTimeBy(20ms); + + return jsi::Value::undefined(); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, std::move(callback2)); + + stubQueue_->tick(); + + EXPECT_EQ(didRunTask2, 1); + EXPECT_EQ(stubQueue_->size(), 0); + pendingEntries = performanceEntryReporter_->popPendingEntries(); + EXPECT_EQ(pendingEntries.entries.size(), 1); + EXPECT_EQ( + pendingEntries.entries[0].entryType, PerformanceEntryType::LONGTASK); + EXPECT_EQ(pendingEntries.entries[0].startTime, 100); + EXPECT_EQ(pendingEntries.entries[0].duration, 120); +} + INSTANTIATE_TEST_SUITE_P( UseModernRuntimeScheduler, RuntimeSchedulerTest,