Repository navigation
Commit 9c7b0f5
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: #595921 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
- scripts/cxx-api/api-snapshots
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
36 | 60 | | |
37 | 61 | | |
38 | 62 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4013 | 4013 | | |
4014 | 4014 | | |
4015 | 4015 | | |
4016 | | - | |
| 4016 | + | |
| 4017 | + | |
| 4018 | + | |
| 4019 | + | |
| 4020 | + | |
| 4021 | + | |
| 4022 | + | |
4017 | 4023 | | |
4018 | 4024 | | |
4019 | 4025 | | |
| |||
0 commit comments