Skip to content

Commit eb79820

Browse files
rubennortefacebook-github-bot
authored andcommitted
Create new version of RuntimeScheduler
Summary: ## Summary This creates a new version of `RuntimeScheduler` that's intended to be backwards compatible but with a few notable changes: 1. `scheduleTask` is now thread-safe. 2. `scheduleWork` is now just an alias of `scheduleTask` with immediate priority (to preserve the yielding semantics it had over other tasks). 3. Yielding mechanism has changed, to make lower priority tasks to yield to higher priority tasks, instead of just yielding to `scheduleWork` and `executeNowOnTheSameThread`. We don't expect this to have any impact in performance or user perceivable behavior, so we consider it a short-lived refactor. When we validate this assumptions in a complex application we'll delete the old version and only keep the fork. ## Motivation The main motivation for this refactor is to reduce the amount of unnecessary interruptions of running tasks (via `shouldYield`) that are only used to schedule asynchronous tasks from native. The `scheduleWork` method is the only available mechanism exposed to native APIs to schedule work in the JS thread (as the existing version of `scheduleTask` is only meant to be called from JS). This mechanism **always** asks for any running tasks in the scheduler to yield, so these tasks are always considered to have the highest priority. This makes sense for discrete user events, but not for many other use cases coming from native (e.g.: notifying network responses could be UserBlocking, Normal or Low depending on the use case). We need a way to schedule tasks from native with other kinds of priorities, so we don't always have to interrupt what's currently executing if it has a higher priority than what we're scheduling. ## Changes **General APIs:** This centralizes scheduling in only 2 APIs in `RuntimeScheduler` (which already exist in the legacy version): * `scheduleTask`, which is non-blocking for the caller and can be used from any thread. This always uses the task queue in the scheduler and a new yielding mechanism. * `executeNowOnTheSameThread`, which is blocking for the caller and asks any task executing in the scheduler to yield. These tasks don't go through the task queue and instead queue through the existing synchronization mechanism in `RuntimeExecutor`. The yielding mechanism for these tasks is preserved. `scheduleWork` will be deprecated and it's just an alias for `scheduleTask` with an immediate priority (to preserve a similar behavior). **Yielding behavior:** Before, tasks would only yield to tasks scheduled via `scheduleWork` and `executeNowOnTheSameThread` (those tasks didn't go through the task queue). With this implementation, tasks would now yield to any task that has a higher position in the task queue. That means we reuse the existing mechanism to avoid lower priority tasks to never execute because higher priority tasks never stop coming. All tasks would yield to requests for synchronous access (via `executeNowOnTheSameThread`) as did the current implementation. Changelog: [internal] Differential Revision: D49316881 fbshipit-source-id: 1d7d8f4f0733b50a29b74fcb679951b01c45a039
1 parent 0973815 commit eb79820

File tree

9 files changed

+1198
-15
lines changed

9 files changed

+1198
-15
lines changed

packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "RuntimeScheduler.h"
99
#include "RuntimeScheduler_Legacy.h"
10+
#include "RuntimeScheduler_Modern.h"
1011
#include "SchedulerPriorityUtils.h"
1112

1213
#include <react/renderer/debug/SystraceSection.h>
@@ -15,11 +16,28 @@
1516

1617
namespace facebook::react {
1718

19+
namespace {
20+
std::unique_ptr<RuntimeSchedulerBase> getRuntimeSchedulerImplementation(
21+
RuntimeExecutor runtimeExecutor,
22+
bool useModernRuntimeScheduler,
23+
std::function<RuntimeSchedulerTimePoint()> now) {
24+
if (useModernRuntimeScheduler) {
25+
return std::make_unique<RuntimeScheduler_Modern>(
26+
std::move(runtimeExecutor), std::move(now));
27+
} else {
28+
return std::make_unique<RuntimeScheduler_Legacy>(
29+
std::move(runtimeExecutor), std::move(now));
30+
}
31+
}
32+
} // namespace
33+
1834
RuntimeScheduler::RuntimeScheduler(
1935
RuntimeExecutor runtimeExecutor,
36+
bool useModernRuntimeScheduler,
2037
std::function<RuntimeSchedulerTimePoint()> now)
21-
: runtimeSchedulerImpl_(std::make_unique<RuntimeScheduler_Legacy>(
38+
: runtimeSchedulerImpl_(getRuntimeSchedulerImplementation(
2239
std::move(runtimeExecutor),
40+
useModernRuntimeScheduler,
2341
std::move(now))) {}
2442

2543
void RuntimeScheduler::scheduleWork(RawCallback&& callback) const noexcept {
@@ -28,13 +46,13 @@ void RuntimeScheduler::scheduleWork(RawCallback&& callback) const noexcept {
2846

2947
std::shared_ptr<Task> RuntimeScheduler::scheduleTask(
3048
SchedulerPriority priority,
31-
jsi::Function&& callback) noexcept {
49+
jsi::Function&& callback) const noexcept {
3250
return runtimeSchedulerImpl_->scheduleTask(priority, std::move(callback));
3351
}
3452

3553
std::shared_ptr<Task> RuntimeScheduler::scheduleTask(
3654
SchedulerPriority priority,
37-
RawCallback&& callback) noexcept {
55+
RawCallback&& callback) const noexcept {
3856
return runtimeSchedulerImpl_->scheduleTask(priority, std::move(callback));
3957
}
4058

packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ class RuntimeSchedulerBase {
2222
virtual void executeNowOnTheSameThread(RawCallback&& callback) = 0;
2323
virtual std::shared_ptr<Task> scheduleTask(
2424
SchedulerPriority priority,
25-
jsi::Function&& callback) noexcept = 0;
25+
jsi::Function&& callback) const noexcept = 0;
2626
virtual std::shared_ptr<Task> scheduleTask(
2727
SchedulerPriority priority,
28-
RawCallback&& callback) noexcept = 0;
28+
RawCallback&& callback) const noexcept = 0;
2929
virtual void cancelTask(Task& task) noexcept = 0;
3030
virtual bool getShouldYield() const noexcept = 0;
3131
virtual bool getIsSynchronous() const noexcept = 0;
@@ -38,8 +38,9 @@ class RuntimeSchedulerBase {
3838
// at runtime based on a feature flag.
3939
class RuntimeScheduler final : RuntimeSchedulerBase {
4040
public:
41-
RuntimeScheduler(
41+
explicit RuntimeScheduler(
4242
RuntimeExecutor runtimeExecutor,
43+
bool useModernRuntimeScheduler = false,
4344
std::function<RuntimeSchedulerTimePoint()> now =
4445
RuntimeSchedulerClock::now);
4546

@@ -74,11 +75,11 @@ class RuntimeScheduler final : RuntimeSchedulerBase {
7475
*/
7576
std::shared_ptr<Task> scheduleTask(
7677
SchedulerPriority priority,
77-
jsi::Function&& callback) noexcept override;
78+
jsi::Function&& callback) const noexcept override;
7879

7980
std::shared_ptr<Task> scheduleTask(
8081
SchedulerPriority priority,
81-
RawCallback&& callback) noexcept override;
82+
RawCallback&& callback) const noexcept override;
8283

8384
/*
8485
* Cancelled task will never be executed.

packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ void RuntimeScheduler_Legacy::scheduleWork(
3838

3939
std::shared_ptr<Task> RuntimeScheduler_Legacy::scheduleTask(
4040
SchedulerPriority priority,
41-
jsi::Function&& callback) noexcept {
41+
jsi::Function&& callback) const noexcept {
42+
SystraceSection s(
43+
"RuntimeScheduler::scheduleTask",
44+
"priority",
45+
serialize(priority),
46+
"callbackType",
47+
"jsi::Function");
48+
4249
auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
4350
auto task =
4451
std::make_shared<Task>(priority, std::move(callback), expirationTime);
@@ -51,7 +58,14 @@ std::shared_ptr<Task> RuntimeScheduler_Legacy::scheduleTask(
5158

5259
std::shared_ptr<Task> RuntimeScheduler_Legacy::scheduleTask(
5360
SchedulerPriority priority,
54-
RawCallback&& callback) noexcept {
61+
RawCallback&& callback) const noexcept {
62+
SystraceSection s(
63+
"RuntimeScheduler::scheduleTask",
64+
"priority",
65+
serialize(priority),
66+
"callbackType",
67+
"RawCallback");
68+
5569
auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
5670
auto task =
5771
std::make_shared<Task>(priority, std::move(callback), expirationTime);

packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace facebook::react {
1919

2020
class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase {
2121
public:
22-
RuntimeScheduler_Legacy(
22+
explicit RuntimeScheduler_Legacy(
2323
RuntimeExecutor runtimeExecutor,
2424
std::function<RuntimeSchedulerTimePoint()> now =
2525
RuntimeSchedulerClock::now);
@@ -55,11 +55,11 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase {
5555
*/
5656
std::shared_ptr<Task> scheduleTask(
5757
SchedulerPriority priority,
58-
jsi::Function&& callback) noexcept override;
58+
jsi::Function&& callback) const noexcept override;
5959

6060
std::shared_ptr<Task> scheduleTask(
6161
SchedulerPriority priority,
62-
RawCallback&& callback) noexcept override;
62+
RawCallback&& callback) const noexcept override;
6363

6464
/*
6565
* Cancelled task will never be executed.
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "RuntimeScheduler_Modern.h"
9+
#include "SchedulerPriorityUtils.h"
10+
11+
#include <react/renderer/debug/SystraceSection.h>
12+
#include <utility>
13+
#include "ErrorUtils.h"
14+
15+
namespace facebook::react {
16+
17+
#pragma mark - Public
18+
19+
RuntimeScheduler_Modern::RuntimeScheduler_Modern(
20+
RuntimeExecutor runtimeExecutor,
21+
std::function<RuntimeSchedulerTimePoint()> now)
22+
: runtimeExecutor_(std::move(runtimeExecutor)), now_(std::move(now)) {}
23+
24+
void RuntimeScheduler_Modern::scheduleWork(
25+
RawCallback&& callback) const noexcept {
26+
SystraceSection s("RuntimeScheduler::scheduleWork");
27+
scheduleTask(SchedulerPriority::ImmediatePriority, std::move(callback));
28+
}
29+
30+
std::shared_ptr<Task> RuntimeScheduler_Modern::scheduleTask(
31+
SchedulerPriority priority,
32+
jsi::Function&& callback) const noexcept {
33+
SystraceSection s(
34+
"RuntimeScheduler::scheduleTask",
35+
"priority",
36+
serialize(priority),
37+
"callbackType",
38+
"jsi::Function");
39+
40+
auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
41+
auto task =
42+
std::make_shared<Task>(priority, std::move(callback), expirationTime);
43+
44+
scheduleTask(task);
45+
46+
return task;
47+
}
48+
49+
std::shared_ptr<Task> RuntimeScheduler_Modern::scheduleTask(
50+
SchedulerPriority priority,
51+
RawCallback&& callback) const noexcept {
52+
SystraceSection s(
53+
"RuntimeScheduler::scheduleTask",
54+
"priority",
55+
serialize(priority),
56+
"callbackType",
57+
"RawCallback");
58+
59+
auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
60+
auto task =
61+
std::make_shared<Task>(priority, std::move(callback), expirationTime);
62+
63+
scheduleTask(task);
64+
65+
return task;
66+
}
67+
68+
bool RuntimeScheduler_Modern::getShouldYield() const noexcept {
69+
std::shared_lock lock(taskQueueMutex_);
70+
71+
return syncTaskRequests_ > 0 ||
72+
(!taskQueue_.empty() && taskQueue_.top() != currentTask_);
73+
}
74+
75+
bool RuntimeScheduler_Modern::getIsSynchronous() const noexcept {
76+
return isSynchronous_;
77+
}
78+
79+
void RuntimeScheduler_Modern::cancelTask(Task& task) noexcept {
80+
task.callback.reset();
81+
}
82+
83+
SchedulerPriority RuntimeScheduler_Modern::getCurrentPriorityLevel()
84+
const noexcept {
85+
return currentPriority_;
86+
}
87+
88+
RuntimeSchedulerTimePoint RuntimeScheduler_Modern::now() const noexcept {
89+
return now_();
90+
}
91+
92+
void RuntimeScheduler_Modern::executeNowOnTheSameThread(
93+
RawCallback&& callback) {
94+
SystraceSection s("RuntimeScheduler::executeNowOnTheSameThread");
95+
96+
syncTaskRequests_++;
97+
98+
executeSynchronouslyOnSameThread_CAN_DEADLOCK(
99+
runtimeExecutor_,
100+
[this, callback = std::move(callback)](jsi::Runtime& runtime) mutable {
101+
SystraceSection s2(
102+
"RuntimeScheduler::executeNowOnTheSameThread callback");
103+
104+
syncTaskRequests_--;
105+
106+
isSynchronous_ = true;
107+
108+
auto currentTime = now_();
109+
auto priority = SchedulerPriority::ImmediatePriority;
110+
auto expirationTime =
111+
currentTime + timeoutForSchedulerPriority(priority);
112+
auto task = std::make_shared<Task>(
113+
priority, std::move(callback), expirationTime);
114+
115+
executeTask(runtime, task, currentTime);
116+
117+
isSynchronous_ = false;
118+
});
119+
120+
bool shouldScheduleWorkLoop = false;
121+
122+
{
123+
std::unique_lock lock(taskQueueMutex_);
124+
125+
if (!taskQueue_.empty() && !isWorkLoopScheduled_) {
126+
isWorkLoopScheduled_ = true;
127+
shouldScheduleWorkLoop = true;
128+
}
129+
}
130+
131+
if (shouldScheduleWorkLoop) {
132+
scheduleWorkLoop();
133+
}
134+
}
135+
136+
// This will be replaced by microtasks
137+
void RuntimeScheduler_Modern::callExpiredTasks(jsi::Runtime& runtime) {
138+
SystraceSection s("RuntimeScheduler::callExpiredTasks");
139+
startWorkLoop(runtime, true);
140+
}
141+
142+
#pragma mark - Private
143+
144+
void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr<Task> task) const {
145+
bool shouldScheduleWorkLoop = false;
146+
147+
{
148+
std::unique_lock lock(taskQueueMutex_);
149+
150+
if (taskQueue_.empty() && !isWorkLoopScheduled_) {
151+
isWorkLoopScheduled_ = true;
152+
shouldScheduleWorkLoop = true;
153+
}
154+
155+
taskQueue_.push(task);
156+
}
157+
158+
if (shouldScheduleWorkLoop) {
159+
scheduleWorkLoop();
160+
}
161+
}
162+
163+
void RuntimeScheduler_Modern::scheduleWorkLoop() const {
164+
runtimeExecutor_(
165+
[this](jsi::Runtime& runtime) { startWorkLoop(runtime, false); });
166+
}
167+
168+
void RuntimeScheduler_Modern::startWorkLoop(
169+
jsi::Runtime& runtime,
170+
bool onlyExpired) const {
171+
SystraceSection s("RuntimeScheduler::startWorkLoop");
172+
173+
auto previousPriority = currentPriority_;
174+
175+
try {
176+
while (syncTaskRequests_ == 0) {
177+
auto currentTime = now_();
178+
auto topPriorityTask = selectTask(currentTime, onlyExpired);
179+
180+
if (!topPriorityTask) {
181+
// No pending work to do.
182+
// Events will restart the loop when necessary.
183+
break;
184+
}
185+
186+
executeTask(runtime, topPriorityTask, currentTime);
187+
}
188+
} catch (jsi::JSError& error) {
189+
handleFatalError(runtime, error);
190+
}
191+
192+
currentPriority_ = previousPriority;
193+
}
194+
195+
std::shared_ptr<Task> RuntimeScheduler_Modern::selectTask(
196+
RuntimeSchedulerTimePoint currentTime,
197+
bool onlyExpired) const {
198+
std::unique_lock lock(taskQueueMutex_);
199+
200+
// It's safe to reset the flag here, as its access is also synchronized with
201+
// the access to the task queue.
202+
isWorkLoopScheduled_ = false;
203+
204+
// Skip executed tasks
205+
while (!taskQueue_.empty() && !taskQueue_.top()->callback) {
206+
taskQueue_.pop();
207+
}
208+
209+
if (!taskQueue_.empty()) {
210+
auto task = taskQueue_.top();
211+
auto didUserCallbackTimeout = task->expirationTime <= currentTime;
212+
if (!onlyExpired || didUserCallbackTimeout) {
213+
return task;
214+
}
215+
}
216+
217+
return nullptr;
218+
}
219+
220+
void RuntimeScheduler_Modern::executeTask(
221+
jsi::Runtime& runtime,
222+
const std::shared_ptr<Task>& task,
223+
RuntimeSchedulerTimePoint currentTime) const {
224+
auto didUserCallbackTimeout = task->expirationTime <= currentTime;
225+
226+
SystraceSection s(
227+
"RuntimeScheduler::executeTask",
228+
"priority",
229+
serialize(task->priority),
230+
"didUserCallbackTimeout",
231+
didUserCallbackTimeout);
232+
233+
currentTask_ = task;
234+
currentPriority_ = task->priority;
235+
236+
auto result = task->execute(runtime, didUserCallbackTimeout);
237+
238+
if (result.isObject() && result.getObject(runtime).isFunction(runtime)) {
239+
// If the task returned a continuation callback, we re-assign it to the task
240+
// and keep the task in the queue.
241+
task->callback = result.getObject(runtime).getFunction(runtime);
242+
}
243+
244+
// TODO execute microtasks
245+
// TODO report long tasks
246+
// TODO update rendering
247+
}
248+
249+
} // namespace facebook::react

0 commit comments

Comments
 (0)