Files
Janic Duplessis b439ef7c3f Process synchronous event beats in the frame that requested them (#58530)
Summary:
`EventEmitter::experimental_flushSync` only *requests* an event beat; the beat is processed at the next `EventBeat::induce`. On iOS the run loop observer that induces the beat runs before Core Animation's commit observer, so a request made from `layoutSubviews` — inside CA's commit cycle — is only processed one frame later. Anything that reports layout-driven state to JS synchronously (`VirtualView` mode changes, and safe area insets in the PRs that build on this) renders a frame late in exactly the cases that matter.

`AppleEventBeat` now also schedules an induce in the **display phase of the current commit cycle**. Core Animation runs a commit as layout → display → commit, so a zero-sized layer marked as needing display during layout gets its `display` call after the whole layout pass and before the transaction is committed. That layer needs to live in the tree being committed, so the beat has to know which tree that is — and the emitter tells it:

- `experimental_flushSync` carries the **tag of the emitting view** through `EventDispatcher` and `EventQueue` to `EventBeat::requestSynchronous(Tag)`, with `kNoTag` (https://github.com/react/react-native/issues/58531) meaning no view attribution; a no-argument overload keeps unattributed requesters and the existing tests unchanged. The emitter reads the tag from its `ShadowNodeFamily` at flush time; `kNoTag` if the family is already gone.
- `AppleEventBeat` resolves the tag to the layer of the view's **window** through a resolver injected by `RCTSurfacePresenter` (`findComponentViewWithTag:` on the mounting registry — nullable, non-creating, main thread) and attaches its flusher layer there. The requesting view's window is by definition the root of the layer tree whose layout emitted the request, so the flusher is guaranteed a display phase in the current commit cycle — including for content UIKit mounts in a window of its own, like a full screen modal or LogBox. Requests within one cycle coalesce into a single induce.

One related fix in `EventBeat` itself: a synchronous request is no longer stranded behind an already-scheduled asynchronous beat (it would silently lose its this-frame guarantee, and the leftover flag would make an unrelated later beat blocking). `AppleEventBeat.cpp` becomes `.mm` for the Objective-C.

**Risk:** this changes when queued events are flushed on iOS for every `experimental_flushSync` caller — today `VirtualView`, and safe area insets with the PRs on top. The worst case is a beat processed a frame *earlier* than before, inside a Core Animation commit; the run loop observer path is untouched and still catches anything the display phase misses (an emitter with no tag, an unmounted view, a request off the main thread). Android ignores the tag. Revert is self-contained.

## Design Q&A:

**What happens when two views in different windows update at once?**
Each requesting window gets its own dirty flusher layer (the map is keyed by host layer), and the first `display` to fire induces the beat, which drains the whole event queue — every window's updates mount before that commit presents. The remaining flushers hit the `isEventBeatRequested_` guard and no-op, so it is one beat total, not one per window. If windows ever commit in separate transactions, each request still resolves within its own window's cycle, since its layer sits in the tree that emitted it. Only requesting windows carry a dirty layer.

**Can the tag point at the wrong view — after an unmount, or a recycled view?**
No. The tag comes from the emitter's `ShadowNodeFamily`, and a family keeps one tag for its whole life, across clones and state updates; if the family is already gone the flush carries `kNoTag` and skips the resolver. What changes over a view's life — its window — is read live: the tag resolves to a view at flush time and `view.window.layer` is looked up then, so a view that moved between windows targets its current tree. A view mid-unmount or recycled resolves to nil (the registry erases the entry and recycled views get tag `0`) and degrades to run-loop-observer timing. The tag only ever influences *where the induce is scheduled*, never what is delivered or to whom, so the blast radius of any staleness is one frame of timing, not correctness.

**Does `VirtualView` need changes to benefit?**
No — its sync mode-change flush goes through its own emitter, so the tag attribution is automatic. The case this improves is a mode change emitted during Core Animation layout (a resize pulling a virtualized item into view): on `main` that renders one frame late; here the induce lands in the display phase of the VirtualView's own window, including inside a full screen modal.

## Changelog:

[INTERNAL] - Process synchronous event beats in the frame that requested them on iOS, scheduling the induce on the requesting view's window

Pull Request resolved: https://github.com/react/react-native/pull/58530

Test Plan:
New unit tests in `EventBeatTest.cpp` cover the beat semantics: a synchronous request during an already-scheduled asynchronous beat, coalescing, and induce ordering. They drive the protected `induce` through a subclass standing in for the platform.

On device, with the safe area insets prop from the PRs above merged on top: an RNTester example renders a loud marker (yellow background) while a view observes the safe area but has not received an inset event yet, so any presented marker frame means the dispatch was not synchronous. The full apply → landscape → portrait sequence **inside a full screen modal** on an iPhone 17 Pro simulator, decomposed with ffmpeg into 982 frames and every frame scanned for the marker color — **zero marker frames**, and mid-rotation frames already carry the incoming orientation's insets, so the padding animates with the rotation. Scoped honestly: the first inset event after setting the prop is processed at the call site, so the marker primarily proves no regression; the same-frame path for layout-driven changes rests on the by-construction argument above plus the rotation frames.

https://github.com/user-attachments/assets/0f2db837-c9c0-4457-96c2-847b7aecf10e

`yarn fantom .../ViewSafeAreaInsets-itest.js` passes 4/4 with the prop merged on top. C++ API snapshots regenerated (`scripts/cxx-api/parser`, Doxygen 1.16.1): the deltas are the `requestSynchronous` overload pair, `EventEmitter::getTag`, the resolver type, and the `AppleEventBeat` constructor and destructor.

 ---

**Stack** — split out of https://github.com/react/react-native/issues/57967, which stays open as the prototype and design discussion. GitHub will not take a fork branch as a pull request base, so each of these targets `main` and its diff contains the ones below it until they merge. Each PR is one commit on top of the previous one.

This is the bottom of the stack, so its diff is already just this change.

👉 1. https://github.com/react/react-native/issues/58530 — Process synchronous event beats in the frame that requested them
    2. https://github.com/react/react-native/issues/58109 — Add an `experimental_onSafeAreaInsetsChange` view prop
    3. https://github.com/react/react-native/issues/58110 — Report the window safe area insets through Dimensions
    4. https://github.com/react/react-native/issues/58112 — Render the internal SafeAreaView from the safe area insets prop
    5. https://github.com/react/react-native/issues/58113 — Remove the native SafeAreaView and the deprecated public export

An earlier variant that targeted the surface's root view instead of the view's window was closed in https://github.com/react/react-native/issues/58528; its review thread carries the analysis behind the window-based resolution. https://github.com/react/react-native/issues/58108 was the per-window predecessor this supersedes.

Reviewed By: javache

Differential Revision: D120200496

Pulled By: Abbondanzo

fbshipit-source-id: b06ecb30935837abd6561f54674afef0cdf215a8
2026-09-21 02:10:25 -07:00
..

scripts/cxx-api

Python build pipeline for React Native's C++ (and Objective-C) API snapshots.

Overview

scripts/cxx-api generates human-readable snapshots of React Native's public C++ API surface. It uses Doxygen to parse C/C++/Objective-C headers and a custom Python parser to produce a simplified, sorted representation of every public symbol.

The pipeline produces one .api snapshot file per configured API view × variant combination:

Snapshot Description
ReactCommonDebugCxx.api Platform-independent C++ API (debug)
ReactCommonReleaseCxx.api Platform-independent C++ API (release)
ReactAndroidDebugCxx.api Android-specific C++ API (debug)
ReactAndroidReleaseCxx.api Android-specific C++ API (release)
ReactAppleDebugCxx.api Apple-specific C++/Obj-C API (debug)
ReactAppleReleaseCxx.api Apple-specific C++/Obj-C API (release)

For each view, debug and release variants are generated with different preprocessor definitions (e.g. REACT_NATIVE_DEBUG vs NDEBUG), since #ifdef guards in the source headers can produce a different public API surface per variant.

Snapshot files are committed to the repo under scripts/cxx-api/api-snapshots/.

Usage

Generate snapshots

Maintainers should run this command whenever making intentional C++ API changes:

python -m scripts.cxx-api.parser

Validate snapshots against committed baseline

This mode generates snapshots to a temporary directory and compares them against the committed .api files. It is designed for CI:

python -m scripts.cxx-api.parser --validate

If any snapshot differs, a unified diff is printed and the process exits with a non-zero status. To fix a failing validation, regenerate the snapshots with python -m scripts.cxx-api.parser and commit the updated .api files.

How it works

The pipeline has two main stages:

1. Doxygen XML generation

Doxygen is configured via a generated config file (built from .doxygen.config.template) with the input directories, exclude patterns, and preprocessor definitions specified in config.yml. It outputs XML describing every symbol found in the headers.

2. Snapshot parsing

The Python parser (parser/) reads the Doxygen XML output and builds a scope tree of the public API surface. The tree is then serialized to a deterministically sorted, human-readable .api text format.

When to use it

The snapshot should be regenerated whenever making intentional changes to the public C++ API surface. This includes additions, removals, and changes to files located in:

  • xplat/js/react-native-github/
  • xplat/js/react-native-github/ReactCommon/
  • xplat/js/react-native-github/ReactAndroid/
  • xplat/js/react-native-github/ReactApple/
  • xplat/js/react-native-github/Libraries/

Configuration

All API views and their variants are defined in config.yml. Each view specifies:

Field Description
inputs Directories to scan for headers
exclude_patterns Glob patterns for files to skip
definitions Preprocessor macros to define
variants Named build variants (e.g. debug/release) with extra definitions
codegen Optional codegen platform (android, ios) to generate TurboModule/Component headers before scanning
private_directories Directories whose headers are scanned (they may be transitively included) but should not contribute public symbols. If any public API entity is defined in a private directory, a warning is printed to help catch accidental API exposure.

Snapshot format

The .api files use a minimal pseudo-C++ syntax designed for easy diffing:

namespace facebook::react {
  class ComponentDescriptor {
    public ComponentDescriptor(ComponentDescriptorParameters params);
    public ComponentHandle getComponentHandle();
  }

  enum class AccessibilityRole {
    None = 0,
    Button = 1,
  }
}
  • Scopes and members are sorted alphabetically.
  • Access specifiers (public, protected) are preserved.
  • Template parameters are included.
  • Doc comments and source file names are stripped.