Skip to content

Share one JNI global reference between copies of a prepared text layout on Android - #59592

Merged
meta-codesync[bot] merged 2 commits into
react:mainfrom
hryhoriiK97:android-text-prepared-layout-shared-ref
Oct 29, 2026
Merged

meta-codesync[bot] merged 2 commits into
react:mainfrom
hryhoriiK97:android-text-prepared-layout-shared-ref

Conversation

@hryhoriiK97

@hryhoriiK97 hryhoriiK97 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

On Android, TextLayoutManager::PreparedTextLayout was an alias of SafeReleaseJniRef<jni::global_ref<JPreparedLayout>>, so every copy of a prepared layout created a new JNI global reference: the cache keeps one, prepareLayout returns a copy of the cached entry, and ParagraphShadowNode copies the measured layout into ParagraphState. Creating a global reference is not free: ART adds it to its global reference table under a lock, and with references being created and deleted all the time, NewGlobalRef was 9–11% of the JS thread on a screen of 200 updating numbers.

With this change, PreparedTextLayout keeps its SafeReleaseJniRef in a std::shared_ptr, so copies share one global reference. It keeps get() and operator bool, which is what its users call, so ParagraphShadowNode, ParagraphState and TextLayoutManager.cpp are unchanged. The Java PreparedLayout is treated as immutable (new react tags produce a new one through reusePreparedLayoutWithNewReactTags), so sharing one reference between the owners is the same as each holding its own; the last owner releases it, still through SafeReleaseJniRef so it can happen on any thread.

A new prepared layout now creates one global reference, as does a cached layout reused with new react tags; reusing a cached layout as is, or copying a layout into state, creates none. Besides the time, this keeps fewer global references alive (about one per distinct layout instead of one per text node per owner), further from ART's global reference limit on screens with a lot of text.

TextLayoutManager still reads FabricUIManager from the context container on every call, which copies its global reference as well; that is left for a separate change.

TextLayoutManager::PreparedTextLayout changes from an alias to a class in the Android C++ API snapshot.

Changelog:

[ANDROID] [CHANGED] - Share one JNI global reference between copies of a prepared text layout, instead of creating one per copy

Test Plan:

  • node scripts/clang-format.js --check reports no changes for the touched header.
  • The C++ API snapshot ReactAndroidFrameworksCxx.api has the PreparedTextLayout class (regenerated with scripts/cxx-api for the first version of this PR; the split only removed a private member, which isn't in the snapshot).
  • ./gradlew :packages:rn-tester:android:app:assembleRelease -PreactNativeArchitectures=arm64-v8a succeeds with this change.
  • On a device. Two RNTester release APKs built from the same local checkout (with local benchmark screens and a counter of prepareTextLayout calls; TextLayoutManager identical to main at 92629fd), without and with this change, installed alternately on a moto g57 power (Android 16). Playground screens where texts change every 100 ms.
    • simpleperf, 12 s of the screen of 200 single-style numbers, two profiles per build:
before this PR
art::JNI::NewGlobalRef, share of the JS thread 8.5% / 10.8% 3.2% / 2.6%
ContextContainer::at (the FabricUIManager reads), share of the JS thread 3.2% / 3.6% 1.6% / 1.1%
JS thread samples 4,196 / 4,239 4,024 / 4,082 (−4%)
  • JS and main thread CPU time from /proc over 20 s, divided by the number of text layouts, three alternating rounds:
before this PR change
200 single-style numbers, JS time per text layout 205 / 206 / 200 µs 249 / 189 / 199 µs within noise (median −3%)
60 single-style rows, JS time per text layout 499 / 506 / 469 µs 487 / 447 / 451 µs −6%
60 rich text rows, JS time per text layout 966 / 984 / 1034 µs 1058 / 993 / 999 µs within noise
60 rows remounted every 100 ms, JS thread over 20 s (layouts reused with new react tags) 3.68 / 3.84 / 3.71 s 3.71 / 3.71 / 3.71 s within noise

The main thread time is unchanged in all of them. The saving is a few percent of the JS thread on simple text, which the profile shows clearly and the per-layout CPU time only partly, given the run-to-run variation.

  • An earlier version of this PR, which also read FabricUIManager once in the constructor, rendered a screen of static rich text (nested bold, colored and underlined <Text onPress>, wrapping) pixel-identically to main, and tapping its link fired onPress. This version only removes that part.

🤖 Generated with Claude Code

Every text layout created several JNI global references: TextLayoutManager
read FabricUIManager from the context container, which copies its global
reference, in prepareLayout, measurePreparedLayout and the other measure
paths, and PreparedTextLayout was itself a global reference, so returning a
cached layout and storing it in a node's state each created another one.

PreparedTextLayout now holds its reference in a shared_ptr, so copies share
it, and TextLayoutManager looks FabricUIManager up once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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 Oct 28, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 28, 2026
@javache

javache commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Great find! Let's split the FabricUIManager off from the PreparedTextLayout changes here. I've got some ideas around the FabricUIManager one (it should really be an alias_ref being returned from the context container)

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Visual regression

No visual differences at 6860e2454637.

Reading FabricUIManager from the context container once will be done
separately; this change now only shares one JNI global reference between
copies of a prepared text layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hryhoriiK97 hryhoriiK97 changed the title Create one JNI global reference per prepared text layout on Android Share one JNI global reference between copies of a prepared text layout on Android Oct 29, 2026
@hryhoriiK97

Copy link
Copy Markdown
Contributor Author

Great find! Let's split the FabricUIManager off from the PreparedTextLayout changes here. I've got some ideas around the FabricUIManager one (it should really be an alias_ref being returned from the context container)

@javache thanks! Split done: this PR is now only the PreparedTextLayout change (header-only), and TextLayoutManager reads FabricUIManager from the context container as before, so that one's yours for the alias_ref approach. Re-measured on its own: NewGlobalRef goes from ~10% to ~3% of the JS thread on a screen of 200 updating numbers (the full version reached 2.3%), so most of the win came from sharing the prepared layout's reference. The description has the new numbers.

@hryhoriiK97

Copy link
Copy Markdown
Contributor Author

@javache these iOS diffs aren't from this PR: it only changes an Android-only header, and the Android visual run passed. The scroll-to diffs look like a flaky capture 🤔

@meta-codesync

meta-codesync Bot commented Oct 29, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D128314571.

@meta-codesync
meta-codesync Bot merged commit 9c7b0f5 into react:main Oct 29, 2026
105 of 106 checks passed
@react-native-bot

Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @hryhoriiK97 in 9c7b0f5

When will my fix make it into a release? | How to file a pick request?

@react-native-bot react-native-bot added the Merged This PR has been merged. label Oct 29, 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. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants