From 15b8ac8db26d4c83fc38da4889ad9d72c8a405ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 18 Jul 2024 05:08:58 -0700 Subject: [PATCH] Add support for longer tasks with explicit yielding in LongTask API (#45471) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45471 Changelog: [internal] This is a React Native specific modification of the Long Tasks API that refines the logic to detect long tasks considering voluntary yielding checks. In RN, as opposed to Web, we can have a very long task executing in the JS thread without causing any issues to the responsiveness of the app, as long as the task checks whether it should yield in short intervals. In this case, if the app always checks whether it should yield at least once every 50ms, the task will not be considered "long". Check the new unit tests to see this behavior in practice. Reviewed By: sammy-SC Differential Revision: D55647992 fbshipit-source-id: 82ab41173d4d9deee65b8ade2268c40d7f58c6e2 --- .../runtimescheduler/RuntimeScheduler.cpp | 2 +- .../runtimescheduler/RuntimeScheduler.h | 4 +- .../RuntimeScheduler_Legacy.cpp | 2 +- .../RuntimeScheduler_Legacy.h | 2 +- .../RuntimeScheduler_Modern.cpp | 60 +++++++++----- .../RuntimeScheduler_Modern.h | 6 +- .../tests/RuntimeSchedulerTest.cpp | 81 +++++++++++++++++++ 7 files changed, 131 insertions(+), 26 deletions(-) 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,