Repository navigation
Investigate: measurement/clone perf findings from upstream RN text PRs (#59063–#59065) #996
Description
Three upstream React Native performance PRs touch areas close to ours:
- Keep paragraph content and measured layouts across unchanged clones react/react-native#59063: keep paragraph content and measured layouts across unchanged clones
- Skip unused getDesiredWidth pass in Android text layout react/react-native#59064: skip the unused
getDesiredWidthpass in Android text layout - Emit C++ TraceSection markers through ATrace on Android react/react-native#59065: emit C++
TraceSectionmarkers through ATrace on Android
None of them change our code paths directly, since we have our own shadow nodes and MeasurementStore. Reading them against our code turned up a few things to look into. None of these have been measured or reproduced yet. They come from reading the code.
1. Android shadow nodes don't keep forceHeightRecalculationCounter_ across clones (same class of bug as #59063)
MarkdownTextShadowNode.h, MarkdownContainerShadowNode.h, MarkdownTextInputShadowNode.h
The clone constructor calls dirtyLayoutIfNeeded() but doesn't copy forceHeightRecalculationCounter_ from the source node, so every clone starts at 0. Once Kotlin has sent a counter above 0, every clone calls dirtyLayout(). That includes the clones Yoga makes of unchanged siblings when a container is laid out again.
Each extra measure costs a JNI call to FabricUIManager.measure, toDynamic(props) of the full markdown and style, and computePropsHash in Kotlin. The cache hit stops it before a StaticLayout is built. Likely worst case: a chat with N markdown messages where one is streaming re-measures the other N−1 on every commit.
iOS already copies the counter (EnrichedMarkdownTextShadowNode.mm).
To investigate: count measureContent calls (or add a trace section) in "N messages, stream one", then copy the counter in the Android clone constructors and compare.
2. The input's first height update may be dropped on Android
InputLayoutManager.kt sends forceHeightRecalculationCounter++ (post-increment), so the first update sends 0. That's the same as the C++ default, so it wouldn't dirty the layout. Everywhere else uses ++counter.
3. Code block height measurement does two passes
CodeBlockContainerView.measureCodeBlockNodeHeight calls Layout.getDesiredWidth (a full measuring pass) only to pick the width of a StaticLayout that doesn't wrap, and then uses only its height. With BREAK_STRATEGY_SIMPLE and no wrapping, the height doesn't depend on the width, so a very large width should give the same height in one pass. This is the idea behind #59064.
4. Add trace sections
We have no tracing on any platform. Sections that check isEnabled would make our work visible in Perfetto and Instruments next to the RN sections from #59065:
- Kotlin
Trace:measureAndCache, parse/render,store - C++: the measurement managers
- iOS
os_signpost:ENRMMeasureMarkdownViewFree
This would also give us a way to measure items 1 and 3.
5. Minor (iOS)
On a cache miss, ENRMMeasureMarkdownContent builds MeasurementCacheKey twice. Each build copies the markdown and recomputes computeStyleFingerprint.
Activity