Skip to content

Commit 1632b74

Browse files
javachemeta-codesync[bot]
authored andcommitted
Add unit tests for enableMainQueueCoordinatorOnIOS (#56934)
Summary: Pull Request resolved: #56934 The `enableMainQueueCoordinatorOnIOS` flag swaps two non-trivial threading primitives — `RCTUnsafeExecuteOnMainQueueSync` uses either `dispatch_sync(main, ...)` or `unsafeExecuteOnMainThreadSync`, and `executeSynchronouslyOnSameThread_CAN_DEADLOCK` switches between `saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK` and the legacy variant. There was zero in-tree test coverage for either branch. Adds three characterization tests: - `testRCTUnsafeExecuteOnMainQueueSync_flagOff_runsBlockOnMainFromBG` and `..._flagOn_...` pin the basic contract for both branches: from a background thread, the block runs on the main queue and the call returns synchronously. - `testExecuteSynchronouslyOnSameThread_flagOn_pumpsUITasksWhileWaitingForJS` is the coordinator's signature property: while the main thread is blocked in the safer impl's wait loop, a UI task posted from a background thread is pumped before the runtime work resumes. The test wires a deliberately slow `RuntimeExecutor` (300ms sleep before invoking its callback) so a BG thread can post a `RCTUnsafeExecuteOnMainQueueSync` work item during the window, then asserts the ordering via a monotonic sequence counter. - `basicUIThreadExecution` (in `RuntimeSchedulerTest.cpp`) mirrors the existing `basicSameThreadExecution` for the production call pattern: the caller is the GTest test body (main NSThread on Apple), so `executeNowOnTheSameThread` routes through the coordinator path. An off-main `driver` thread ticks the stub queue so main can wake up. Changelog: [Internal] ___ Differential Revision: D105972787 fbshipit-source-id: 98325021d0d770b4fe3a9da707c724ef8febbd62
1 parent 9563cb5 commit 1632b74

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,40 @@ TEST_P(RuntimeSchedulerTest, basicSameThreadExecution) {
951951
EXPECT_TRUE(didRunSynchronousTask);
952952
}
953953

954+
// Mirror of `basicSameThreadExecution` for the production call pattern: the
955+
// caller is the UI thread (XCTest test methods run on the main NSThread on
956+
// Apple), so `executeNowOnTheSameThread` routes through the coordinator path
957+
// in `executeSynchronouslyOnSameThread_CAN_DEADLOCK`. The off-main `driver`
958+
// thread drives the stub queue ("JS thread") so the main thread can wake up.
959+
TEST_P(RuntimeSchedulerTest, basicUIThreadExecution) {
960+
class CoordinatorFeatureFlags : public RuntimeSchedulerTestFeatureFlags {
961+
public:
962+
using RuntimeSchedulerTestFeatureFlags::RuntimeSchedulerTestFeatureFlags;
963+
bool enableMainQueueCoordinatorOnIOS() override {
964+
return true;
965+
}
966+
};
967+
ReactNativeFeatureFlags::dangerouslyReset();
968+
ReactNativeFeatureFlags::override(
969+
std::make_unique<CoordinatorFeatureFlags>(GetParam()));
970+
971+
bool didRunSynchronousTask = false;
972+
973+
std::thread driver([this]() {
974+
stubQueue_->waitForTask();
975+
stubQueue_->tick();
976+
});
977+
978+
runtimeScheduler_->executeNowOnTheSameThread(
979+
[&didRunSynchronousTask](jsi::Runtime& /*rt*/) {
980+
didRunSynchronousTask = true;
981+
});
982+
983+
driver.join();
984+
985+
EXPECT_TRUE(didRunSynchronousTask);
986+
}
987+
954988
TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesImmediatePriorityTask) {
955989
bool didRunSynchronousTask = false;
956990
bool didRunSubsequentTask = false;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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+
#import <XCTest/XCTest.h>
9+
10+
#import <React/RCTUtils.h>
11+
#import <ReactCommon/RuntimeExecutor.h>
12+
#import <ReactCommon/RuntimeExecutorSyncUIThreadUtils.h>
13+
#import <hermes/hermes.h>
14+
#import <react/featureflags/ReactNativeFeatureFlags.h>
15+
#import <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
16+
#import <atomic>
17+
#import <chrono>
18+
#import <thread>
19+
20+
using namespace facebook::react;
21+
22+
namespace {
23+
class MainQueueCoordinatorOverride : public ReactNativeFeatureFlagsDefaults {
24+
public:
25+
explicit MainQueueCoordinatorOverride(bool enabled) : enabled_(enabled) {}
26+
bool enableMainQueueCoordinatorOnIOS() override
27+
{
28+
return enabled_;
29+
}
30+
31+
private:
32+
bool enabled_;
33+
};
34+
} // namespace
35+
36+
@interface MainQueueCoordinatorTests : XCTestCase
37+
@end
38+
39+
@implementation MainQueueCoordinatorTests
40+
41+
- (void)setUp
42+
{
43+
[super setUp];
44+
ReactNativeFeatureFlags::dangerouslyReset();
45+
}
46+
47+
- (void)tearDown
48+
{
49+
ReactNativeFeatureFlags::dangerouslyReset();
50+
[super tearDown];
51+
}
52+
53+
#pragma mark - RCTUnsafeExecuteOnMainQueueSync
54+
55+
- (void)_assertRCTUnsafeExecuteOnMainQueueSyncRunsBlockOnMainFromBackgroundThread
56+
{
57+
XCTestExpectation *done = [self expectationWithDescription:@"BG-thread call completed"];
58+
__block BOOL ranOnMain = NO;
59+
__block BOOL returnedToBG = NO;
60+
61+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
62+
RCTUnsafeExecuteOnMainQueueSync(^{
63+
ranOnMain = RCTIsMainQueue();
64+
});
65+
returnedToBG = YES;
66+
[done fulfill];
67+
});
68+
69+
[self waitForExpectations:@[ done ] timeout:5.0];
70+
XCTAssertTrue(ranOnMain, @"Block must execute on main queue");
71+
XCTAssertTrue(returnedToBG, @"BG thread must resume after sync call returns");
72+
}
73+
74+
- (void)testRCTUnsafeExecuteOnMainQueueSync_flagOff_runsBlockOnMainFromBG
75+
{
76+
// Default off — exercises the dispatch_sync(main) branch.
77+
[self _assertRCTUnsafeExecuteOnMainQueueSyncRunsBlockOnMainFromBackgroundThread];
78+
}
79+
80+
- (void)testRCTUnsafeExecuteOnMainQueueSync_flagOn_runsBlockOnMainFromBG
81+
{
82+
ReactNativeFeatureFlags::override(std::make_unique<MainQueueCoordinatorOverride>(true));
83+
// Coordinator branch — `unsafeExecuteOnMainThreadSync` posts a UI task and waits.
84+
[self _assertRCTUnsafeExecuteOnMainQueueSyncRunsBlockOnMainFromBackgroundThread];
85+
}
86+
87+
#pragma mark - executeSynchronouslyOnSameThread_CAN_DEADLOCK
88+
89+
- (void)testExecuteSynchronouslyOnSameThread_flagOn_pumpsUITasksWhileWaitingForJS
90+
{
91+
ReactNativeFeatureFlags::override(std::make_unique<MainQueueCoordinatorOverride>(true));
92+
93+
// A real runtime — needed only for the reference type. We never call into it.
94+
auto runtime = facebook::hermes::makeHermesRuntime();
95+
96+
// A "JS thread" that the executor posts to. Sleeps before invoking the callback
97+
// so that the main thread is forced to wait, giving us a window to post a UI task.
98+
constexpr auto kJSDelay = std::chrono::milliseconds(300);
99+
100+
RuntimeExecutor slowJSExecutor = [&runtime, kJSDelay](std::function<void(facebook::jsi::Runtime &)> &&callback) {
101+
std::thread([cb = std::move(callback), &runtime, kJSDelay]() mutable {
102+
std::this_thread::sleep_for(kJSDelay);
103+
cb(*runtime);
104+
}).detach();
105+
};
106+
107+
// Both threads only ever write on main, so plain ints are race-free. Held in
108+
// a struct so we can capture a pointer from an ObjC block and a reference
109+
// from a C++ lambda without `__block` (which doesn't compose with lambdas).
110+
struct State {
111+
int sequence = 0;
112+
int uiTaskOrder = 0;
113+
int runtimeWorkOrder = 0;
114+
BOOL uiTaskRanOnMain = NO;
115+
BOOL runtimeWorkRanOnMain = NO;
116+
};
117+
State state;
118+
State *statePtr = &state;
119+
120+
// Background thread posts a UI task ~100ms after the main thread enters the
121+
// wait loop — well before the JS thread wakes up at +300ms.
122+
XCTestExpectation *bgDone = [self expectationWithDescription:@"BG-thread sync call completed"];
123+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
124+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
125+
RCTUnsafeExecuteOnMainQueueSync(^{
126+
statePtr->uiTaskOrder = ++statePtr->sequence;
127+
statePtr->uiTaskRanOnMain = RCTIsMainQueue();
128+
});
129+
[bgDone fulfill];
130+
});
131+
132+
// This call blocks main inside the coordinator's wait loop. While it waits,
133+
// it should pump the UI task posted by the BG thread above.
134+
executeSynchronouslyOnSameThread_CAN_DEADLOCK(slowJSExecutor, [&state](facebook::jsi::Runtime & /*_*/) {
135+
state.runtimeWorkOrder = ++state.sequence;
136+
state.runtimeWorkRanOnMain = RCTIsMainQueue();
137+
});
138+
139+
[self waitForExpectations:@[ bgDone ] timeout:5.0];
140+
141+
XCTAssertTrue(state.uiTaskRanOnMain, @"UI task posted from BG should run on main");
142+
XCTAssertTrue(state.runtimeWorkRanOnMain, @"runtimeWork should run on main");
143+
XCTAssertEqual(state.uiTaskOrder, 1, @"UI task should be pumped before runtimeWork");
144+
XCTAssertEqual(state.runtimeWorkOrder, 2, @"runtimeWork should run after the pumped UI task");
145+
}
146+
147+
@end

0 commit comments

Comments
 (0)