Skip to content

[iOS] Retire queued Scheduler delegate work and retain active callbacks - #58703

Closed
safaiyeh wants to merge 1 commit into
react:mainfrom
safaiyeh:fix/scheduler-rendering-lifetime
Closed

safaiyeh wants to merge 1 commit into
react:mainfrom
safaiyeh:fix/scheduler-rendering-lifetime

Conversation

@safaiyeh

@safaiyeh safaiyeh commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

Fabric's Scheduler queues rendering callbacks that capture a borrowed SchedulerDelegate*. Detaching the delegate or destroying the Scheduler does not revoke those captures. On iOS, RCTScheduler owns the delegate proxy, so pending commands or mounting work can call through a released proxy after ordinary host teardown. Error-time queue clearing does not cover this non-error path; the remaining gap is also described in 9682967a09 / #58138.

This change gives each delegate assignment its own registration. Queued callbacks acquire a lease when they execute. Retirement rejects new leases; an already acquired owned lease keeps its target alive until it returns. Replacement creates a separate registration so old queued work cannot target the replacement. Delegate calls and final releases occur outside synchronization primitives.

The iOS adapter passes a shared proxy into the Scheduler and uses a zeroing weak RCTScheduler backpointer, promoted locally before forwarding. This avoids both a dangling Objective-C owner and a retain cycle. The change covers deferred commands, transactions and React-revision merging, plus direct delegate calls.

The raw-pointer constructor/setter remain available for Android and existing integrations. They cancel retired queued work but retain their caller-managed lifetime contract for callbacks already admitted. Each assignment starts a new generation, even when the pointer is unchanged. Lifecycle mutations remain externally serialized; callbacks may overlap retirement. This does not claim to solve every RuntimeScheduler shutdown or surface-unregistration lifetime issue.

Changelog:

[IOS] [FIXED] - Prevent queued Fabric rendering callbacks from using a retired Scheduler delegate, and retain owned delegates while active callbacks finish.

Test Plan:

  • 108 native GoogleTests pass: 89 existing RuntimeScheduler/priority tests and 19 Scheduler/registration tests. These compile actual upstream sources in a standalone CMake harness, with Hermes and native dependencies. Seven registration tests also pass under AddressSanitizer + UndefinedBehaviorSanitizer and under ThreadSanitizer, with no findings.
  • The modified RCTScheduler.mm passes Objective-C++ ARC syntax compilation against upstream headers and the iOS Simulator SDK.
  • Two Foundation ARC lifecycle cases using the extracted production proxy pass: an expired owner safely no-ops without a retain cycle; a callback overlapping retirement keeps the owner alive through delegate access.
  • All nine C++ API snapshots regenerated and validated with the repository Python pipeline and its CI-pinned Doxygen 1.16.1.
  • Before this change, a separate fixture linked to prebuilt React Native 0.86.3 reproduced queued dispatch after delegate clearing/replacement and Scheduler destruction on both iOS 26.5 and iOS 27. Delegates remained alive throughout this control fixture; no freed-memory access was required.

Remaining validation: full RNTester/Expo host reload and termination tests, UIKit teardown-thread behavior under overlapping callbacks, and assessment of the additional shared-ownership operations on delegate hot paths. The ARC overlap test observes that the final owner release can occur on the callback thread. The isolated native tests establish the delegate ownership/cancellation contract, not complete application shutdown safety. Android still uses the borrowed delegate contract.

Local native validation commands and scope

The standalone CMake harness compiles the actual modified Scheduler.cpp, its ReactCommon dependency closure, and the checked-in regression tests. It uses Apple Clang 21, C++20, GoogleTest 1.17.0 (52eb8108c5bdec04579160ae17225d66034bd723), Hermes macOS package 250829098.0.17, Folly 0.58.0-dev, fmt 12.2.0 and glog 0.7.1. It uses the real Root/View component registry; the animation backend's unused react_codegen_rncore dependency is an empty interface target in this isolated harness. This is local source validation, not an official RNTester or Android build.

cmake --build /tmp/rn-native-lifecycle-tests/build \
  --target runtime_scheduler_tests scheduler_lifecycle_tests -j 8
/tmp/rn-native-lifecycle-tests/build/runtime_scheduler_tests
# 89 passed
/tmp/rn-native-lifecycle-tests/build/scheduler_lifecycle_tests
# 19 passed

# Separate helper builds use -DNATIVE_SANITIZERS=address,undefined or thread.
ASAN_OPTIONS=halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
  /tmp/rn-native-lifecycle-tests/build-asan/helper_registration_tests
# 7 passed
TSAN_OPTIONS=halt_on_error=1 \
  /tmp/rn-native-lifecycle-tests/build-tsan/helper_registration_tests
# 7 passed

# Doxygen 1.16.1, matching upstream CI:
python -m scripts.cxx-api.parser --validate
# All nine snapshot validations passed

Sanitizer builds instrument the production registration header, helper tests and GoogleTest; prebuilt dependency internals remain unsanitized. The Scheduler-destruction test deliberately retains UIManager while draining callbacks, because the runtime's separate revision-manager pointer lifetime is outside this patch.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 28, 2026
@safaiyeh
safaiyeh marked this pull request as ready for review September 28, 2026 06:18
@safaiyeh safaiyeh closed this Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant