Skip to content

Commit 9c7b0f5

Browse files
authored
Share one JNI global reference between copies of a prepared text layout on Android
## 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](https://claude.com/claude-code) Differential Revision: D128314571 Pull Request resolved: #59592
1 parent f422ddc commit 9c7b0f5

2 files changed

Lines changed: 32 additions & 2 deletions

File tree

‎packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,31 @@ class TextLayoutManager;
3232
*/
3333
class TextLayoutManager {
3434
public:
35-
using PreparedTextLayout = SafeReleaseJniRef<jni::global_ref<JPreparedLayout>>;
35+
/*
36+
* A prepared layout's Java object. Copies share one JNI global reference, so
37+
* returning a cached layout or storing it in state doesn't create another.
38+
*/
39+
class PreparedTextLayout {
40+
public:
41+
PreparedTextLayout() = default;
42+
explicit PreparedTextLayout(jni::global_ref<JPreparedLayout> ref)
43+
: ref_(std::make_shared<const SafeReleaseJniRef<jni::global_ref<JPreparedLayout>>>(std::move(ref)))
44+
{
45+
}
46+
47+
JPreparedLayout::javaobject get() const
48+
{
49+
return ref_ ? ref_->get() : nullptr;
50+
}
51+
52+
explicit operator bool() const noexcept
53+
{
54+
return ref_ && *ref_;
55+
}
56+
57+
private:
58+
std::shared_ptr<const SafeReleaseJniRef<jni::global_ref<JPreparedLayout>>> ref_;
59+
};
3660

3761
TextLayoutManager(const std::shared_ptr<const ContextContainer> &contextContainer);
3862

‎scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4013,7 +4013,13 @@ class facebook::react::TextLayoutManager {
40134013
public facebook::react::TextMeasurement measure(const facebook::react::AttributedStringBox& attributedStringBox, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const;
40144014
public facebook::react::TextMeasurement measureCachedSpannableById(int64_t cacheId, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const;
40154015
public facebook::react::TextMeasurement measurePreparedLayout(const facebook::react::TextLayoutManager::PreparedTextLayout& layout, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const;
4016-
public using PreparedTextLayout = facebook::react::SafeReleaseJniRef<jni::global_ref<facebook::react::JPreparedLayout>>;
4016+
}
4017+
4018+
class facebook::react::TextLayoutManager::PreparedTextLayout {
4019+
public JPreparedLayout::javaobject get() const;
4020+
public PreparedTextLayout() = default;
4021+
public PreparedTextLayout(jni::global_ref<facebook::react::JPreparedLayout> ref);
4022+
public operator bool() const noexcept;
40174023
}
40184024

40194025
class facebook::react::TextMeasureCacheKey {

0 commit comments

Comments
 (0)