Alternative: schedule the display-phase event beat induce on the requesting surface - #58528
janicduplessis wants to merge 3 commits into
Conversation
`EventEmitter::experimental_flushSync` only *requests* a beat, which is processed at the next `EventBeat::induce`. On Android the induce happens within the frame, before drawing, so a synchronous request made during layout is processed in that frame. On iOS it is not: 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. `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 has its `display` called after the whole layout pass and before the transaction is committed. A layer is kept in every visible window of every foreground scene, since the request can come from any of them — a modal and the LogBox are windows of their own — and only a layer in the tree being committed is guaranteed a display this cycle. Requests within one cycle coalesce into a single induce, so mounting ten observing views is one beat rather than ten. Two related fixes 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), and `induce` becomes public so platform beats can call it from a callback. `AppleEventBeat.cpp` becomes `.mm` for the Objective-C. Covered by new unit tests in `EventBeatTest.cpp`. This is the platform half of the safe area insets work: it is what makes an inset change reported from `layoutSubviews` render in the frame it happened in. `VirtualView` uses the same mechanism.
The driver ticked the queue exactly twice, but the number of queued tasks depends on thread interleaving: when the synchronous access request is queued before the first tick, the scheduled beat's work item yields without executing and is re-queued for a third tick that never came. Drive the queue from the test thread with the same waitForTasks synchronization the RuntimeScheduler tests use, asserting each stage.
experimental_flushSync now carries the surface of the emitter that requested it (from the ShadowNodeFamily every emitter is given at creation) through EventDispatcher and EventQueue to EventBeat::requestSynchronous, as an optional: without a surface the induce falls back to the run loop observer's ordinary schedule. AppleEventBeat resolves the surface to its root view's layer via a resolver injected by RCTSurfacePresenter from the surface registry, and attaches the flusher layer there instead of to every visible window. A request made from layout runs inside the commit of exactly that tree, so the layer is guaranteed a display phase in the same cycle without assuming all windows commit in one transaction. VirtualView's sync flushes get the same targeting through its own emitter, unchanged.
|
Closing in favor of #58108 — the exploration answered the question, against this approach. The disqualifying finding: surface identity does not identify the committing layer tree for content inside a non-transparent Two implementation issues found in review, recorded for any future revival:
Scope correction on the test plan above: the capture proves no regression of the marker test, but it does not prove same-frame delivery for the modal case — the first inset event after The branch stays up for reference. |
|
Addendum — a variant that resolves the disqualifying finding, recorded here in case targeting is ever wanted: pass the emitting view's tag instead of its surface. |
|
The view-tag variant from the addendum above is now implemented and opened as #58530, verified with the marker methodology inside the full screen modal (982 frames, zero marker frames). |
Summary:
Alternative to #58108's display-phase scheduling, opened for comparison — one of the two should land, not both. The stack in #58108–#58113 works as-is on top of either.
Both PRs solve the same problem: a synchronous event beat requested while Core Animation is already laying out (an event emitted from
layoutSubviews) is only processed on the next frame by the run loop observer. #58108 schedules an induce in the display phase of the current commit cycle by keeping a zero-sized flusher layer in every visible window of every foreground scene, since only a layer inside a committing tree is guaranteed a display phase, and the beat does not know which tree that is.This PR gives it that knowledge instead.
experimental_flushSyncnow carries the surface of the requesting emitter — every emitter is given itsShadowNodeFamilyat creation, and the family knows its surface — throughEventDispatcherandEventQueuetoEventBeat::requestSynchronous(std::optional<SurfaceId>).AppleEventBeatresolves the surface to its root view's layer through a resolver injected byRCTSurfacePresenterfrom the surface registry, and attaches the single flusher layer there. A request made from layout runs inside the commit of exactly that tree, so the layer is guaranteed a display phase in the same cycle.What this buys over #58108:
VirtualView's synchronous flushes get the same targeting through their own emitter, with no change at the call site.The cost is API: the
experimental_flushSyncchain changes signature across four core types (allexperimental_, Android ignores the parameter), where #58108 is self-contained inAppleEventBeat. An absent surface — an emitter whose family is gone, or a future caller without one — falls back to the run loop observer's ordinary schedule.Known rough edges, flagged for review:
requestSynchronous's defaultedstd::optionalargument on a virtual should likely become an overload, and the C++ API snapshots are not regenerated for the new signatures yet.Changelog:
[INTERNAL] - Schedule the synchronous event beat induce on the requesting surface's root view
Test Plan:
Same marker methodology as #58108 (any presented frame of the yellow observing-without-insets marker means the dispatch was not synchronous), re-run on this implementation with RNTester on an iPhone 17 Pro simulator: the full apply → landscape → portrait capture below was decomposed with ffmpeg into 950 frames and every frame scanned for the marker color — zero marker frames. Mid-rotation frames already show the incoming orientation's insets (
top: 62, bottom: 34while the rotation animation is still in flight), so the padding animates with the transition.ViewSafeAreaInsets-itestpasses 4/4 with the prop stack merged on top;EventBeatTestcovers the unchanged beat semantics through the base-class default.proto-capture.mp4