Skip to content

fix(iOS): Make components measuring safer for the Main Thread - #852

Merged
szydlovsky merged 3 commits into
mainfrom
@szydlovsky/fix-ios-measuring-deadlocks
Nov 5, 2026
Merged

szydlovsky merged 3 commits into
mainfrom
@szydlovsky/fix-ios-measuring-deadlocks

Conversation

@szydlovsky

@szydlovsky szydlovsky commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR aims to fix issue: #848

Basically, there was a possibility to have a native main thread deadlock because we were force-running some operations on it.

What the fix does:

  • Previously, both input and text components would pass whole refs to themselves to the ShadowNode, and it ran measuring directly on them, which required the operation to be done on MainThread. Now, we only pass a snapshot which is the attributed string currently sitting in the component (and also maximumNumberOfLines + lineBreakMode for the text component, because measuring depends on them except the attributed string snapshot).
  • The measuring inside ShadowNode now looks the following:
    • if the snapshot exists, grab it and run measuring using it - now this process doesn't need the main thread at all. Only snapshot creation uses it, but it's not a piece of code that other operation could wait on (causing deadlocks).
    • if there's no snapshot and we are on the main thread, we can create a mock component with native component props and measure it
    • if there's no snapshot but we're not on the main thread, we schedule a "create mock + measure it" block on the main thread, but if the scheduled operation waits for it for longer than 100ms, we abort it and return a 0 height measurement.

These let us free the main thread - there was a possible scenario that some layout operation locks the main thread and calls our component's measure on a non-main one. In the previous iteration we would endlessly wait for the main thread dispatch to go through. Now the regular measures don't need to be run on main thread and the special "mock" ones (which still require the main thread) have a safeguard to not wait endlessly for it.

Other fix

The second commit also fixes related dirty-layouting issues I found when fixing this one:

  • input could be either not running or running too many on-demand layouts because force layout counter was not initialised properly for cloned shadow nodes
  • the text component changes we just did were not running on-demand layouts when snapshots changed for newly cloned shadow nodes

React Native anyway always marks freshly cloned nodes dirty (up for a layout) but I made these changes to not blindly rely on this behavior.

Test Plan

As I could not reproduce the issue, it's all up to the issue author to test out the patch from this PR.

Screenshots / Videos

Compatibility

OS Implemented
iOS ✅
Android ❌
Web ❌

Checklist

  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

@szydlovsky
szydlovsky requested review from exploIF and a balanced review from Copilot November 3, 2026 18:43
@szydlovsky szydlovsky linked an issue Nov 3, 2026 that may be closed by this pull request
3 tasks done
@szydlovsky szydlovsky changed the title fix(iOS): Make components measuring safer for Main Thread fix(iOS): Make components measuring safer for the Main Thread Nov 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A timed-out initial measurement can leave nonempty EnrichedText collapsed because snapshot updates do not invalidate Yoga’s cached zero height, requiring measurement invalidation when the snapshot changes.

1 open issue
What changed in this PR

This PR addresses the iOS measurement deadlock reported in #848 by replacing live component references with attributed-string snapshots, allowing mounted components to be measured without waiting on the main thread.

Changes:

  • Publish measurement snapshots through Fabric state for text and input components.
  • Measure snapshots independently of their UIKit views.
  • Limit main-thread waits for initial mock measurements to 100 ms, falling back to zero height.
File Description
ios/​internals/​EnrichedTextViewState.h Stores the snapshot and text layout settings.
ios/​internals/​EnrichedTextViewState.cpp Exposes snapshot and layout-setting getters.
ios/​internals/​EnrichedTextViewShadowNode.mm Measures snapshots with a bounded mock fallback.
ios/​internals/​EnrichedTextViewShadowNode.h Makes mock creation independent of node lifetime.
ios/​internals/​EnrichedTextInputViewState.h Replaces the component reference with a snapshot.
ios/​internals/​EnrichedTextInputViewState.cpp Exposes the input snapshot getter.
ios/​internals/​EnrichedTextInputViewShadowNode.mm Measures input snapshots with a bounded mock fallback.
ios/​internals/​EnrichedTextInputViewShadowNode.h Makes input mock creation independent of node lifetime.
ios/​internals/​EnrichedMainThreadMeasure.h Adds the shared timeout-based measurement helper.
ios/​EnrichedTextView.mm Creates, publishes, and measures text snapshots.
ios/​EnrichedTextView.h Declares snapshot-based text measurement.
ios/​EnrichedTextInputView.mm Creates, publishes, and measures input snapshots.
ios/​EnrichedTextInputView.h Declares snapshot-based input measurement.

Review effort: Balanced

@szydlovsky
szydlovsky merged commit f87bba8 into main Nov 5, 2026
8 checks passed
@szydlovsky
szydlovsky deleted the @szydlovsky/fix-ios-measuring-deadlocks branch November 5, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] Deadlock: measureContent dispatch_syncs to main while holding the shadow tree lock

3 participants