Skip to content

Commit 40cdb16

Browse files
lavenzgfacebook-github-bot
authored andcommitted
Implement IEventLoopControl in RuntimeScheduler
Summary: Make `RuntimeScheduler` implement `IEventLoopControl`. Wire `ReactInstance` to register that scheduler on Hermes runtimes that expose `ISetEventLoopControl`. Clear the pointer during teardown before `RuntimeScheduler` is destroyed. Regenerate React Native C++ API snapshots for the public header change. Only the modern scheduler have the actual implementation, it's no-op in the legacy scheduler. Changelog: [Internal] Reviewed By: javache Differential Revision: D106744175
1 parent 34ccf4f commit 40cdb16

19 files changed

Lines changed: 242 additions & 20 deletions

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ void RuntimeScheduler::scheduleWork(RawCallback&& callback) noexcept {
6161
return runtimeSchedulerImpl_->scheduleWork(std::move(callback));
6262
}
6363

64+
void RuntimeScheduler::scheduleTask(const std::function<void()>& task) {
65+
return runtimeSchedulerImpl_->scheduleTask(task);
66+
}
67+
68+
uint64_t RuntimeScheduler::registerTaskQueueSource() {
69+
return runtimeSchedulerImpl_->registerTaskQueueSource();
70+
}
71+
72+
void RuntimeScheduler::unregisterTaskQueueSource(uint64_t sourceId) {
73+
return runtimeSchedulerImpl_->unregisterTaskQueueSource(sourceId);
74+
}
75+
6476
std::shared_ptr<Task> RuntimeScheduler::scheduleTask(
6577
SchedulerPriority priority,
6678
jsi::Function&& callback) noexcept {

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#pragma once
99

1010
#include <ReactCommon/RuntimeExecutor.h>
11+
#include <jsi/hermes-interfaces.h>
1112
#include <react/performance/timeline/PerformanceEntryReporter.h>
1213
#include <react/renderer/consistency/ShadowTreeRevisionConsistencyManager.h>
1314
#include <react/renderer/runtimescheduler/SchedulerPriorityUtils.h>
@@ -27,9 +28,12 @@ extern const char RuntimeSchedulerKey[];
2728

2829
// This is a temporary abstract class for RuntimeScheduler forks to implement
2930
// (and use them interchangeably).
30-
class RuntimeSchedulerBase {
31+
class RuntimeSchedulerBase : public facebook::hermes::IEventLoopControl {
3132
public:
3233
virtual ~RuntimeSchedulerBase() = default;
34+
35+
using facebook::hermes::IEventLoopControl::scheduleTask;
36+
3337
virtual void scheduleWork(RawCallback &&callback) noexcept = 0;
3438
virtual void executeNowOnTheSameThread(RawCallback &&callback) = 0;
3539
virtual std::shared_ptr<Task> scheduleTask(SchedulerPriority priority, jsi::Function &&callback) noexcept = 0;
@@ -55,7 +59,7 @@ class RuntimeSchedulerBase {
5559

5660
// This is a proxy for RuntimeScheduler implementation, which will be selected
5761
// at runtime based on a feature flag.
58-
class RuntimeScheduler final : RuntimeSchedulerBase {
62+
class RuntimeScheduler final : public RuntimeSchedulerBase {
5963
public:
6064
explicit RuntimeScheduler(
6165
RuntimeExecutor runtimeExecutor,
@@ -76,6 +80,13 @@ class RuntimeScheduler final : RuntimeSchedulerBase {
7680

7781
void scheduleWork(RawCallback &&callback) noexcept override;
7882

83+
/// IEventLoopControl implementation, forwarded to the underlying fork.
84+
void scheduleTask(const std::function<void()> &task) override;
85+
86+
uint64_t registerTaskQueueSource() override;
87+
88+
void unregisterTaskQueueSource(uint64_t sourceId) override;
89+
7990
/*
8091
* Grants access to the runtime synchronously on the caller's thread.
8192
*

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ void RuntimeScheduler_Legacy::scheduleWork(RawCallback&& callback) noexcept {
4646
});
4747
}
4848

49+
void RuntimeScheduler_Legacy::scheduleTask(
50+
[[maybe_unused]] const std::function<void()>& task) {}
51+
uint64_t RuntimeScheduler_Legacy::registerTaskQueueSource() {
52+
return 0;
53+
}
54+
void RuntimeScheduler_Legacy::unregisterTaskQueueSource(
55+
[[maybe_unused]] uint64_t sourceId) {}
56+
4957
std::shared_ptr<Task> RuntimeScheduler_Legacy::scheduleTask(
5058
SchedulerPriority priority,
5159
jsi::Function&& callback) noexcept {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase {
4040

4141
void scheduleWork(RawCallback &&callback) noexcept override;
4242

43+
/// IEventLoopControl implementation. No-op for legacy scheduler.
44+
void scheduleTask(const std::function<void()> &task) override;
45+
uint64_t registerTaskQueueSource() override;
46+
void unregisterTaskQueueSource(uint64_t sourceId) override;
47+
4348
/*
4449
* Grants access to the runtime synchronously on the caller's thread.
4550
*

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ void RuntimeScheduler_Modern::scheduleWork(RawCallback&& callback) noexcept {
4747
scheduleTask(SchedulerPriority::ImmediatePriority, std::move(callback));
4848
}
4949

50+
void RuntimeScheduler_Modern::scheduleTask(const std::function<void()>& task) {
51+
scheduleIdleTask([task](jsi::Runtime& /*runtime*/) { task(); });
52+
}
53+
54+
uint64_t RuntimeScheduler_Modern::registerTaskQueueSource() {
55+
// It's fine to wrap around, as it's impossible to hold so many live task
56+
// queue sources in practice.
57+
return nextTaskQueueSourceId_.fetch_add(1) + 1;
58+
}
59+
60+
void RuntimeScheduler_Modern::unregisterTaskQueueSource(uint64_t /*sourceId*/) {
61+
// For now, we don't need to do unregistering. The reason is that the event
62+
// loop of the runtime scheduler doesn't need to be blocked on the task
63+
// producer of IEventLoopControl. When the event loop ends, we just ignore
64+
// all queueing tasks.
65+
}
66+
5067
std::shared_ptr<Task> RuntimeScheduler_Modern::scheduleTask(
5168
SchedulerPriority priority,
5269
jsi::Function&& callback) noexcept {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase {
4545
*/
4646
void scheduleWork(RawCallback &&callback) noexcept override;
4747

48+
/// IEventLoopControl implementation. \p task is always scheduled as an idle
49+
/// task.
50+
void scheduleTask(const std::function<void()> &task) override;
51+
52+
uint64_t registerTaskQueueSource() override;
53+
54+
void unregisterTaskQueueSource(uint64_t sourceId) override;
55+
4856
/*
4957
* Grants access to the runtime synchronously on the caller's thread.
5058
*
@@ -144,6 +152,10 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase {
144152
RuntimeSchedulerIntersectionObserverDelegate *intersectionObserverDelegate) override;
145153

146154
private:
155+
/// Monotonic counter handing out IDs for IEventLoopControl task queue
156+
/// sources.
157+
std::atomic<uint64_t> nextTaskQueueSourceId_{0};
158+
147159
std::atomic<uint_fast8_t> syncTaskRequests_{0};
148160

149161
std::priority_queue<std::shared_ptr<Task>, std::vector<std::shared_ptr<Task>>, TaskPriorityComparer> taskQueue_;

packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp

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

88
#include <gtest/gtest.h>
99
#include <hermes/hermes.h>
10+
#include <jsi/hermes-interfaces.h>
1011
#include <jsi/jsi.h>
1112
#include <react/featureflags/ReactNativeFeatureFlags.h>
1213
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
@@ -170,6 +171,31 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTask) {
170171
EXPECT_EQ(stubQueue_->size(), 0);
171172
}
172173

174+
TEST_P(RuntimeSchedulerTest, scheduleTaskViaEventLoopControl) {
175+
// The RuntimeScheduler proxy is what gets registered with Hermes as an
176+
// IEventLoopControl. scheduleTask() forwards to the selected fork, which
177+
// schedules the work as an idle task.
178+
facebook::hermes::IEventLoopControl& eventLoopControl = *runtimeScheduler_;
179+
180+
bool didRunTask = false;
181+
eventLoopControl.scheduleTask([&didRunTask]() { didRunTask = true; });
182+
183+
if (GetParam()) {
184+
// Modern scheduler: the task is queued on the event loop and runs on tick.
185+
EXPECT_FALSE(didRunTask);
186+
EXPECT_EQ(stubQueue_->size(), 1);
187+
188+
stubQueue_->tick();
189+
190+
EXPECT_TRUE(didRunTask);
191+
EXPECT_EQ(stubQueue_->size(), 0);
192+
} else {
193+
// Legacy scheduler: idle tasks are not supported, so this is a no-op.
194+
EXPECT_FALSE(didRunTask);
195+
EXPECT_EQ(stubQueue_->size(), 0);
196+
}
197+
}
198+
173199
TEST_P(
174200
RuntimeSchedulerTest,
175201
scheduleSingleTaskWithMicrotasksAndBatchedRenderingUpdate) {

packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ std::shared_ptr<RuntimeScheduler> createRuntimeScheduler(
4848
return scheduler;
4949
}
5050

51+
void setHermesEventLoopControl(
52+
jsi::Runtime& runtime,
53+
facebook::hermes::IEventLoopControl* eventLoopControl) {
54+
auto* setEventLoopControl =
55+
jsi::castInterface<facebook::hermes::ISetEventLoopControl>(&runtime);
56+
if (setEventLoopControl != nullptr) {
57+
setEventLoopControl->setEventLoopControl(eventLoopControl);
58+
}
59+
}
60+
5161
std::string getSyntheticBundlePath(uint32_t bundleId) {
5262
std::array<char, 32> buffer{};
5363
std::snprintf(buffer.data(), buffer.size(), "seg-%u.js", bundleId);
@@ -156,13 +166,23 @@ ReactInstance::ReactInstance(
156166
});
157167
}
158168

169+
runtimeExecutor(
170+
[runtimeScheduler = runtimeScheduler_.get()](jsi::Runtime& runtime) {
171+
setHermesEventLoopControl(runtime, runtimeScheduler);
172+
});
173+
159174
bufferedRuntimeExecutor_ = std::make_shared<BufferedRuntimeExecutor>(
160175
[runtimeScheduler = runtimeScheduler_.get()](
161176
std::function<void(jsi::Runtime & runtime)>&& callback) {
162177
runtimeScheduler->scheduleWork(std::move(callback));
163178
});
164179
}
165180
ReactInstance::~ReactInstance() noexcept {
181+
// This is thread safe because there is no JSI call at this point, and there
182+
// won't be any concurrent calls to getEventLoopControl().
183+
// We need to clear this pointer before runtimeScheduler_ is destroyed.
184+
setHermesEventLoopControl(runtime_->getRuntime(), nullptr);
185+
166186
if (timerManager_ != nullptr) {
167187
timerManager_->quit();
168188
}

packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <ReactCommon/RuntimeExecutor.h>
1616
#include <hermes/hermes.h>
1717
#include <jserrorhandler/JsErrorHandler.h>
18+
#include <jsi/hermes-interfaces.h>
1819
#include <jsi/jsi.h>
1920
#include <react/runtime/ReactInstance.h>
2021

@@ -138,6 +139,12 @@ class ReactInstanceTest : public ::testing::Test {
138139
std::move(onJsError));
139140
timerManager_->setRuntimeExecutor(instance_->getBufferedRuntimeExecutor());
140141

142+
// ReactInstance construction defers registering the RuntimeScheduler as the
143+
// Hermes IEventLoopControl onto the runtime executor, which enqueues a
144+
// callback on messageQueueThread_. Drain it so each test starts from an
145+
// empty queue and its step() bookkeeping is unaffected.
146+
step();
147+
141148
// Install a C++ error handler
142149
errorHandler_ = std::make_shared<ErrorUtils>();
143150
runtime_->global().setProperty(
@@ -223,6 +230,23 @@ class ReactInstanceTest : public ::testing::Test {
223230
std::shared_ptr<ErrorUtils> errorHandler_;
224231
};
225232

233+
TEST_F(ReactInstanceTest, testRegistersRuntimeSchedulerAsEventLoopControl) {
234+
auto* setEventLoopControl =
235+
jsi::castInterface<facebook::hermes::ISetEventLoopControl>(runtime_);
236+
if (setEventLoopControl == nullptr) {
237+
// This Hermes build does not implement ISetEventLoopControl, so
238+
// ReactInstance's registration is a no-op and there is nothing to observe.
239+
GTEST_SKIP()
240+
<< "Hermes runtime does not expose ISetEventLoopControl in this build";
241+
}
242+
243+
// SetUp() already drained the deferred registration callback, so by now the
244+
// RuntimeScheduler is registered as the runtime's event loop control.
245+
facebook::hermes::IEventLoopControl* expected =
246+
instance_->getRuntimeScheduler().get();
247+
EXPECT_EQ(setEventLoopControl->getEventLoopControl(), expected);
248+
}
249+
226250
TEST_F(ReactInstanceTest, testBridgelessFlagIsSet) {
227251
auto valBefore = tryEval("RN$Bridgeless === true", "false");
228252
EXPECT_EQ(valBefore.getBool(), false);

packages/react-native/ReactCommon/react/runtime/tests/cxx/RuntimeExecutorShutdownTest.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ class RuntimeExecutorShutdownTest : public ::testing::Test {
7373
messageQueueThread_,
7474
timerManager,
7575
std::move(onJsError));
76+
77+
// ReactInstance construction defers registering the RuntimeScheduler as the
78+
// Hermes IEventLoopControl onto the runtime executor, which enqueues a
79+
// callback on messageQueueThread_. Drain it so each shutdown scenario
80+
// starts from an empty queue.
81+
messageQueueThread_->tick();
7682
}
7783

7884
std::shared_ptr<MockMessageQueueThread> messageQueueThread_;

0 commit comments

Comments
 (0)