Bound the display-phase event beat induce to one per run loop turn - #58642
Open
Abbondanzo wants to merge 1 commit into
Open
Abbondanzo wants to merge 1 commit into
Abbondanzo wants to merge 1 commit into
Conversation
Summary: The beat an Apple display phase induces runs the whole event loop tick on the calling thread, mounting included. A view whose layout metrics change during that mount can emit another synchronous request from inside the display that is servicing the first one, and Core Animation honours a `setNeedsDisplay` made during a display by running the commit's layout and display phases again, with no bound. One Core Animation commit can therefore perform an unbounded number of blocking JavaScript round trips. `AppleEventBeat` now induces at most once per run loop turn from the display phase. The run loop observer, which runs before Core Animation's commit observer, opens each turn. Requests arriving after that first induce keep the ordinary observer timing — what they had before the display-phase induce existed — so this bounds the tail without giving up the guarantee the display-phase induce was added for: the first layout-driven request of a frame is still processed in that frame. Measured on an iPhone 11 / iOS 26.0 simulator with a virtualized list whose placeholder is taller than its content, so making one row visible pulls siblings into the viewport. Identical scenario, 13 synchronous requests in every arm: | arm | worst single main-thread block | induces in one commit | | --- | --- | --- | | display-phase induce, unbounded | 145.8 ms | 11 | | no display-phase induce | 15.5 ms | n/a | | display-phase induce, bounded (this change) | 14.9 ms | 1 | A separate probe established that Core Animation itself imposes no bound: a zero-sized layer that re-dirties itself from inside its own `display` ran 200 display passes in one commit, stopped only by the probe's own cap. Changelog: [iOS][Fixed] - Process at most one synchronous event beat per Core Animation commit, so a mount performed during the display phase cannot re-enter it without bound Differential Revision: D120951592
|
@Abbondanzo has exported this pull request. If you are a Meta employee, you can view the originating Diff in D120951592. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
The beat an Apple display phase induces runs the whole event loop tick on the
calling thread, mounting included. A view whose layout metrics change during
that mount can emit another synchronous request from inside the display that is
servicing the first one, and Core Animation honours a
setNeedsDisplaymadeduring a display by running the commit's layout and display phases again, with
no bound. One Core Animation commit can therefore perform an unbounded number of
blocking JavaScript round trips.
AppleEventBeatnow induces at most once per run loop turn from the displayphase. The run loop observer, which runs before Core Animation's commit
observer, opens each turn. Requests arriving after that first induce keep the
ordinary observer timing — what they had before the display-phase induce
existed — so this bounds the tail without giving up the guarantee the
display-phase induce was added for: the first layout-driven request of a frame
is still processed in that frame.
Measured on an iPhone 11 / iOS 26.0 simulator with a virtualized list whose
placeholder is taller than its content, so making one row visible pulls siblings
into the viewport. Identical scenario, 13 synchronous requests in every arm:
A separate probe established that Core Animation itself imposes no bound: a
zero-sized layer that re-dirties itself from inside its own
displayran 200display passes in one commit, stopped only by the probe's own cap.
Changelog:
[iOS][Fixed] - Process at most one synchronous event beat per Core Animation commit, so a mount performed during the display phase cannot re-enter it without bound
Differential Revision: D120951592