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
This commit is contained in:
Rubén Norte
2024-07-18 05:08:58 -07:00
committed by Facebook GitHub Bot
parent 64c4e385cb
commit 15b8ac8db2
7 changed files with 131 additions and 26 deletions
@@ -65,7 +65,7 @@ std::shared_ptr<Task> RuntimeScheduler::scheduleIdleTask(
return runtimeSchedulerImpl_->scheduleIdleTask(std::move(callback), timeout);
}
bool RuntimeScheduler::getShouldYield() const noexcept {
bool RuntimeScheduler::getShouldYield() noexcept {
return runtimeSchedulerImpl_->getShouldYield();
}
@@ -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.
@@ -98,7 +98,7 @@ std::shared_ptr<Task> RuntimeScheduler_Legacy::scheduleIdleTask(
return nullptr;
}
bool RuntimeScheduler_Legacy::getShouldYield() const noexcept {
bool RuntimeScheduler_Legacy::getShouldYield() noexcept {
return runtimeAccessRequests_ > 0;
}
@@ -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.
@@ -118,9 +118,13 @@ std::shared_ptr<Task> 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
@@ -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_`.
@@ -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,