Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/react-native/React/Fabric/RCTScheduler.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ NS_ASSUME_NONNULL_BEGIN

- (void)setupAnimationDriver:(const facebook::react::SurfaceHandler &)surfaceHandler;

// Permanently removes the layout-animation observer. Call on the main queue.
- (void)stopObservingLayoutAnimations;

- (void)onAnimationStarted;

- (void)onAllAnimationsComplete;
Expand Down
7 changes: 7 additions & 0 deletions packages/react-native/React/Fabric/RCTScheduler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#import "RCTScheduler.h"

#import <QuartzCore/CADisplayLink.h>
#import <React/RCTAssert.h>
#import <cxxreact/TraceSection.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/animations/LayoutAnimationDriver.h>
Expand Down Expand Up @@ -258,6 +259,12 @@ - (void)setupAnimationDriver:(const facebook::react::SurfaceHandler &)surfaceHan
surfaceHandler.getMountingCoordinator()->setMountingOverrideDelegate(_animationDriver);
}

- (void)stopObservingLayoutAnimations
{
RCTAssertMainQueue();
_uiRunLoopObserver.reset();
}

- (void)onAnimationStarted
{
if (_uiRunLoopObserver) {
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/React/Fabric/RCTSurfacePresenter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ - (BOOL)suspend
}

[self _stopAllSurfacesWithScheduler:scheduler];
scheduler.delegate = nil;

return YES;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ end

header_search_paths = [
"$(PODS_ROOT)/Headers/Private/React-Core",
"$(PODS_ROOT)/Headers/Private/Yoga",
"$(PODS_TARGET_SRCROOT)/../../../..",
"$(PODS_TARGET_SRCROOT)/../../../../..",
]
Expand Down Expand Up @@ -50,6 +51,7 @@ Pod::Spec.new do |s|
s.dependency "React-CoreModules"
s.dependency "React-NativeModulesApple"
s.dependency "React-RCTFabric"
s.dependency "Yoga"
s.dependency "React-RuntimeCore"
s.dependency "React-Mapbuffer"
s.dependency "React-jserrorhandler"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#import <React/RCTLogBox.h>
#import <React/RCTPerformanceLogger.h>
#import <React/RCTRedBox.h>
#import <React/RCTScheduler.h>
#import <React/RCTSurfacePresenter.h>
#import <ReactCommon/RCTTurboModule.h>
#import <ReactCommon/RCTTurboModuleManager.h>
Expand Down Expand Up @@ -191,28 +192,36 @@ - (void)invalidate
}
// suspend shouldn't be called under _invalidationMutex: it notifies surface observers,
// which can call callFunctionOnJSModule and deadlock on the same mutex.
// Queued rendering updates capture the scheduler's delegate by raw pointer.
// Stop its run-loop observer on main, but keep it alive until queued JS work
// has drained. Release it before the runtime so its queued JSI handles remain valid.
__block RCTScheduler *scheduler = _surfacePresenter.scheduler;
[_surfacePresenter suspend];
[_jsThreadManager dispatchToJSThread:^{
/**
* Every TurboModule is invalidated on its own method queue.
* TurboModuleManager invalidate blocks the calling thread until all TurboModules are invalidated.
*/
[self->_turboModuleManager invalidate];

// Clean up all the Resources
self->_reactInstance = nullptr;
self->_jsRuntimeFactory = nullptr;
self->_appTMMDelegate = nil;
self->_delegate = nil;
[self->_displayLink invalidate];
self->_displayLink = nil;

self->_turboModuleManager = nil;
self->_performanceLogger = nil;

// Terminate the JavaScript thread, so that no other work executes after this block.
self->_jsThreadManager = nil;
}];
RCTExecuteOnMainQueue(^{
[scheduler stopObservingLayoutAnimations];
[self->_jsThreadManager dispatchToJSThread:^{
/**
* Every TurboModule is invalidated on its own method queue.
* TurboModuleManager invalidate blocks the calling thread until all TurboModules are invalidated.
*/
[self->_turboModuleManager invalidate];

// Clean up all the Resources
scheduler = nil;
self->_reactInstance = nullptr;
self->_jsRuntimeFactory = nullptr;
self->_appTMMDelegate = nil;
self->_delegate = nil;
[self->_displayLink invalidate];
self->_displayLink = nil;

self->_turboModuleManager = nil;
self->_performanceLogger = nil;

// Terminate the JavaScript thread, so that no other work executes after this block.
self->_jsThreadManager = nil;
}];
});
}

- (std::shared_ptr<CallInvoker>)jsCallInvoker
Expand Down
4 changes: 4 additions & 0 deletions packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
F0D621C32BBB9E38005960AC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */; };
F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */; };
F1A0B1C23D4E5F6071829322 /* RCTTurboModuleStringConversionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829321 /* RCTTurboModuleStringConversionTests.mm */; };
F1A0B1C23D4E5F6071829342 /* RCTInstanceInvalidationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829341 /* RCTInstanceInvalidationTests.mm */; };
D1A60AD11B9247A080734F02 /* RCTDevLoadingViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -148,6 +149,7 @@
F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTurboModuleArrayBufferTests.mm; sourceTree = "<group>"; };
F1A0B1C23D4E5F6071829321 /* RCTTurboModuleStringConversionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTurboModuleStringConversionTests.mm; sourceTree = "<group>"; };
F1A0B1C23D4E5F6071829341 /* RCTInstanceInvalidationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTInstanceInvalidationTests.mm; sourceTree = "<group>"; };
D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTDevLoadingViewTests.m; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -309,6 +311,7 @@
E7DB20CB22B2BAA5005AC45F /* RCTModuleMethodTests.mm */,
F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */,
F1A0B1C23D4E5F6071829321 /* RCTTurboModuleStringConversionTests.mm */,
F1A0B1C23D4E5F6071829341 /* RCTInstanceInvalidationTests.mm */,
E7DB20CF22B2BAA5005AC45F /* RCTMultipartStreamReaderTests.m */,
A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */,
E7DB20BE22B2BAA4005AC45F /* RCTNativeAnimatedNodesManagerTests.m */,
Expand Down Expand Up @@ -620,6 +623,7 @@
E7DB20E822B2BAA6005AC45F /* RCTModuleMethodTests.mm in Sources */,
F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */,
F1A0B1C23D4E5F6071829322 /* RCTTurboModuleStringConversionTests.mm in Sources */,
F1A0B1C23D4E5F6071829342 /* RCTInstanceInvalidationTests.mm in Sources */,
E7DB20E222B2BAA6005AC45F /* RCTGzipTests.m in Sources */,
E7DB20ED22B2BAA6005AC45F /* RCTURLUtilsTests.m in Sources */,
E7DB20D322B2BAA6005AC45F /* RCTBlobManagerTests.m in Sources */,
Expand Down
196 changes: 196 additions & 0 deletions packages/rn-tester/RNTesterUnitTests/RCTInstanceInvalidationTests.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <CoreFoundation/CoreFoundation.h>
#import <XCTest/XCTest.h>

#import <React/RCTSurfacePresenter.h>
#import <ReactCommon/RCTInstance.h>

@interface RCTSchedulerLifetimeProbe : NSObject
@property (nonatomic, weak) id delegate;
@property (nonatomic, copy) void (^onDealloc)(BOOL onMainThread);
@property (nonatomic, copy) void (^onStopObserving)(BOOL onMainThread);
@property (nonatomic, copy) dispatch_block_t onTick;
- (void)startObserving;
- (void)stopObservingLayoutAnimations;
@end

@implementation RCTSchedulerLifetimeProbe {
CFRunLoopObserverRef _observer;
}

- (void)startObserving
{
void *unretainedSelf = (__bridge void *)self;
_observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef, CFRunLoopActivity) {
__unsafe_unretained RCTSchedulerLifetimeProbe *probe = (__bridge RCTSchedulerLifetimeProbe *)unretainedSelf;
dispatch_block_t tick = probe.onTick;
if (tick) {
tick();
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
}

- (void)stopObservingLayoutAnimations
{
if (_observer) {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = nullptr;
}
if (_onStopObserving) {
_onStopObserving([NSThread isMainThread]);
}
}

- (void)dealloc
{
if (_observer) {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
}
if (_onDealloc) {
_onDealloc([NSThread isMainThread]);
}
}
@end

@interface RCTJSThreadManagerStub : NSObject
@property (nonatomic, copy) dispatch_block_t pendingWork;
@end

@implementation RCTJSThreadManagerStub
- (void)dispatchToJSThread:(dispatch_block_t)work
{
self.pendingWork = work;
}
@end

@interface RCTInstanceInvalidationTests : XCTestCase
@end

@implementation RCTInstanceInvalidationTests

- (void)runJSWork:(dispatch_block_t)work
{
dispatch_semaphore_t finished = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
@autoreleasepool {
work();
}
dispatch_semaphore_signal(finished);
});
XCTAssertEqual(dispatch_semaphore_wait(finished, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC)), 0);
}

- (void)testInvalidateStopsObserverOnMainAndReleasesSchedulerDuringJSCleanup
{
XCTAssertTrue([NSThread isMainThread]);
RCTSurfacePresenter *presenter = [RCTSurfacePresenter new];
RCTJSThreadManagerStub *jsThreadManager = [RCTJSThreadManagerStub new];
RCTInstance *instance = [RCTInstance new];
[instance setValue:presenter forKey:@"surfacePresenter"];
[instance setValue:jsThreadManager forKey:@"jsThreadManager"];

XCTestExpectation *released = [self expectationWithDescription:@"Scheduler released during JS cleanup"];
__block BOOL observerStoppedOnMain = NO;
__block BOOL releasedBeforeJSThreadCleanup = NO;
__weak RCTInstance *weakInstance = instance;
__weak RCTSchedulerLifetimeProbe *weakScheduler;
@autoreleasepool {
RCTSchedulerLifetimeProbe *scheduler = [RCTSchedulerLifetimeProbe new];
scheduler.delegate = presenter;
scheduler.onStopObserving = ^(BOOL onMainThread) {
observerStoppedOnMain = onMainThread;
};
scheduler.onDealloc = ^(BOOL) {
releasedBeforeJSThreadCleanup = [weakInstance valueForKey:@"jsThreadManager"] != nil;
[released fulfill];
};
weakScheduler = scheduler;
// Exercise real instance invalidation and presenter suspension without a JS VM.
[presenter setValue:scheduler forKey:@"scheduler"];
}

@autoreleasepool {
[instance invalidate];
XCTAssertNil(presenter.scheduler);
XCTAssertNotNil(weakScheduler);
XCTAssertNil(weakScheduler.delegate);
XCTAssertNotNil(jsThreadManager.pendingWork);
XCTAssertTrue(observerStoppedOnMain);
}

dispatch_block_t drainWork = jsThreadManager.pendingWork;
[self runJSWork:drainWork];

[self waitForExpectations:@[ released ] timeout:3];
XCTAssertTrue(releasedBeforeJSThreadCleanup);
// Keeping the cleanup block itself must not keep the scheduler alive.
XCTAssertNil(weakScheduler);
XCTAssertNil([instance valueForKey:@"jsThreadManager"]);
jsThreadManager.pendingWork = nil;
}

- (void)testStoppedObserverDoesNotRunWhileWaitingForJSCleanup
{
XCTAssertTrue([NSThread isMainThread]);
RCTSurfacePresenter *presenter = [RCTSurfacePresenter new];
RCTJSThreadManagerStub *jsThreadManager = [RCTJSThreadManagerStub new];
RCTInstance *instance = [RCTInstance new];
[instance setValue:presenter forKey:@"surfacePresenter"];
[instance setValue:jsThreadManager forKey:@"jsThreadManager"];

XCTestExpectation *noTick = [self expectationWithDescription:@"Stopped observer must not run"];
noTick.inverted = YES;
XCTestExpectation *released = [self expectationWithDescription:@"Scheduler released during JS cleanup"];
__weak RCTSchedulerLifetimeProbe *weakScheduler;
@autoreleasepool {
RCTSchedulerLifetimeProbe *scheduler = [RCTSchedulerLifetimeProbe new];
weakScheduler = scheduler;
scheduler.onDealloc = ^(BOOL) {
[released fulfill];
};
scheduler.onTick = ^{
[noTick fulfill];
};
[presenter setValue:scheduler forKey:@"scheduler"];
[scheduler startObserving];
}

@autoreleasepool {
[instance invalidate];
}
[self waitForExpectations:@[ noTick ] timeout:0.05];
XCTAssertNotNil(weakScheduler);
[self runJSWork:jsThreadManager.pendingWork];
[self waitForExpectations:@[ released ] timeout:3];
XCTAssertNil(weakScheduler);
XCTAssertNil([instance valueForKey:@"jsThreadManager"]);
jsThreadManager.pendingWork = nil;
}

- (void)testInvalidateWithoutSchedulerCompletesJSThreadCleanup
{
RCTSurfacePresenter *presenter = [RCTSurfacePresenter new];
RCTJSThreadManagerStub *jsThreadManager = [RCTJSThreadManagerStub new];
RCTInstance *instance = [RCTInstance new];
[instance setValue:presenter forKey:@"surfacePresenter"];
[instance setValue:jsThreadManager forKey:@"jsThreadManager"];

[instance invalidate];
XCTAssertNotNil(jsThreadManager.pendingWork);
jsThreadManager.pendingWork();
XCTAssertNil(presenter.scheduler);
XCTAssertNil([instance valueForKey:@"jsThreadManager"]);
jsThreadManager.pendingWork = nil;
}

@end
1 change: 1 addition & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,7 @@ interface RCTScheduler : public NSObject {
public virtual void removeEventListener:(const std::shared_ptr<facebook::react::EventListener>& listener);
public virtual void reportMount:(facebook::react::SurfaceId surfaceId);
public virtual void setupAnimationDriver:(const facebook::react::SurfaceHandler& surfaceHandler);
public virtual void stopObservingLayoutAnimations();
public virtual void unregisterSurface:(const facebook::react::SurfaceHandler& surfaceHandler);
}

Expand Down
1 change: 1 addition & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,7 @@ interface RCTScheduler : public NSObject {
public virtual void removeEventListener:(const std::shared_ptr<facebook::react::EventListener>& listener);
public virtual void reportMount:(facebook::react::SurfaceId surfaceId);
public virtual void setupAnimationDriver:(const facebook::react::SurfaceHandler& surfaceHandler);
public virtual void stopObservingLayoutAnimations();
public virtual void unregisterSurface:(const facebook::react::SurfaceHandler& surfaceHandler);
}

Expand Down
1 change: 1 addition & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,7 @@ interface RCTScheduler : public NSObject {
public virtual void removeEventListener:(const std::shared_ptr<facebook::react::EventListener>& listener);
public virtual void reportMount:(facebook::react::SurfaceId surfaceId);
public virtual void setupAnimationDriver:(const facebook::react::SurfaceHandler& surfaceHandler);
public virtual void stopObservingLayoutAnimations();
public virtual void unregisterSurface:(const facebook::react::SurfaceHandler& surfaceHandler);
}

Expand Down
1 change: 1 addition & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,7 @@ interface RCTScheduler : public NSObject {
public virtual void removeEventListener:(const std::shared_ptr<facebook::react::EventListener>& listener);
public virtual void reportMount:(facebook::react::SurfaceId surfaceId);
public virtual void setupAnimationDriver:(const facebook::react::SurfaceHandler& surfaceHandler);
public virtual void stopObservingLayoutAnimations();
public virtual void unregisterSurface:(const facebook::react::SurfaceHandler& surfaceHandler);
}

Expand Down
Loading