Repository navigation
Share one JNI global reference between copies of a prepared text layout on Android - #59592
Conversation
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>
|
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) |
Visual regressionNo visual differences at |
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>
@javache thanks! Split done: this PR is now only the |
|
@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 🤔 |
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D128314571. |
|
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? |
Summary:
On Android,
TextLayoutManager::PreparedTextLayoutwas an alias ofSafeReleaseJniRef<jni::global_ref<JPreparedLayout>>, so every copy of a prepared layout created a new JNI global reference: the cache keeps one,prepareLayoutreturns a copy of the cached entry, andParagraphShadowNodecopies the measured layout intoParagraphState. 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,NewGlobalRefwas 9–11% of the JS thread on a screen of 200 updating numbers.With this change,
PreparedTextLayoutkeeps itsSafeReleaseJniRefin astd::shared_ptr, so copies share one global reference. It keepsget()andoperator bool, which is what its users call, soParagraphShadowNode,ParagraphStateandTextLayoutManager.cppare unchanged. The JavaPreparedLayoutis treated as immutable (new react tags produce a new one throughreusePreparedLayoutWithNewReactTags), so sharing one reference between the owners is the same as each holding its own; the last owner releases it, still throughSafeReleaseJniRefso 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.
TextLayoutManagerstill readsFabricUIManagerfrom the context container on every call, which copies its global reference as well; that is left for a separate change.TextLayoutManager::PreparedTextLayoutchanges 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 --checkreports no changes for the touched header.ReactAndroidFrameworksCxx.apihas thePreparedTextLayoutclass (regenerated withscripts/cxx-apifor 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-v8asucceeds with this change.prepareTextLayoutcalls;TextLayoutManageridentical tomainat 92629fd), without and with this change, installed alternately on a moto g57 power (Android 16). Playground screens where texts change every 100 ms.art::JNI::NewGlobalRef, share of the JS threadContextContainer::at(theFabricUIManagerreads), share of the JS thread/procover 20 s, divided by the number of text layouts, three alternating rounds: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.
FabricUIManageronce in the constructor, rendered a screen of static rich text (nested bold, colored and underlined<Text onPress>, wrapping) pixel-identically tomain, and tapping its link firedonPress. This version only removes that part.🤖 Generated with Claude Code