Skip to content

Accept readable Android event payloads (#59595) - #59595

Open
javache wants to merge 2 commits into
react:mainfrom
javache:export-D128208007
Open

javache wants to merge 2 commits into
react:mainfrom
javache:export-D128208007

Conversation

@javache

@javache javache commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

Widen Android event payload contracts from WritableMap to ReadableMap across the event emitters, UI managers and Fabric dispatch pipeline, including Event.getEventData. Existing event producers can continue returning writable maps. Build native touch payloads as a complete folly::dynamic and return them through ReadableNativeMap::createWithContents without constructing and populating a writable Java map.

Document Fabric’s native-backed, consumed-payload contract, update the touch test shadow to return a read-only native map and regenerate the Android API snapshot.

Changelog:
[Android][Breaking] - Change Android event payload APIs from WritableMap to ReadableMap; update custom emitter and UI manager implementations to match.

Differential Revision: D128208007

Summary:
On Android, every touch event goes through `TouchesHelper.sendTouchEvent`, which builds the payload JS receives as nested `WritableNativeMap`s.

**Current behavior.** For each pointer of the `MotionEvent`, `createPointersArray` creates a native map and puts eight values into it, one JNI call each. Then, for each changed pointer, the payload is assembled from copies of those maps: the event itself is a copy of the changed pointer, and `changedTouches` and `touches` are arrays of further copies (`copy()`, `pushMap()`, `putArray()`, each one or more JNI calls). With one finger moving, that is about 20–25 JNI calls and 8 native-backed objects for every `ACTION_MOVE`.

**With this change.** `TouchesHelper` collects the pointer values once into a `DoubleArray`. For each event it creates an empty `WritableNativeMap` and passes it with the pointer data to a new native method, `TouchesHelper.nativePutTouchPayload`, which fills it in C++ through the map's existing `putDouble`, `putInt` and `putNativeArray`. That is two JNI calls per event (creating the map and filling it) instead of 20–25. `TouchesHelper` still decides which pointers go into `changedTouches` and `touches`, now as bit masks of pointer indices (start: the new pointer / all; end: the lifted pointer / all but it; move: all / all; cancel: all / none). The keys, values and value types of the payload are the same as before, and the events are dispatched exactly as before.

`WritableNativeMap` is unchanged. The native method is registered like `TransformHelper`'s, from `TouchesHelper.cpp` in `reactnativejni_common`, and `TouchesHelper` is `DoNotStrip` because the class is looked up by name when the library loads. `TouchesHelper` is `internal`, so this adds no public Kotlin API; the C++ API snapshots get the new `TouchesHelper` class. The layout of the pointer data (eight values per pointer) is documented on `nativePutTouchPayload`, and the C++ side and the test shadow point to it.

## Changelog:

[ANDROID] [CHANGED] - Build touch event payloads in native code, making each touch event cheaper on the main thread

Pull Request resolved: react#59554

Test Plan:
- `TouchEventDispatchTest` passes. It checks the payloads of the start/move/end, start/move/cancel and multi-pointer start/pointer-up/cancel sequences, now through `ShadowTouchesHelper`, a Kotlin version of `nativePutTouchPayload`, and compares the map contents. With the end case deliberately broken (lifted pointer kept in `touches`), 2 of its 3 tests fail.
- `ktfmt` and the repository's `clang-format` report no changes for the touched files. The C++ API snapshots were regenerated with `scripts/cxx-api`.
- `./gradlew :packages:rn-tester:android:app:assembleRelease -PreactNativeArchitectures=arm64-v8a` succeeds, from `main` at 01d5527.
- **Same payloads.** A local self-test (not part of this PR) built synthetic `MotionEvent`s for 1 to 5 pointers, a pointer going down and up at every index, a move and a cancel, ran them through the previous implementation and through the first version of this PR, and compared the payloads with `toHashMap()`: 40 events, 60 payloads, no differences, on a moto g57 power. The current version builds the payload with the same C++ code; only the way the result is put into the map changed.
- **Release builds of this PR and of `main`.** Two separate RNTester release APKs (this PR, and `main` at 01d5527), RNTester Playground screen with a `PanResponder` drag area, 36 drags of 1.8 s per launch driven by `adb shell input swipe` (about 4,200 touch events per launch), installs alternated, moto g57 power (Android 16). On-CPU time of the main and JS threads during the drags from `/proc`, trace sections from Perfetto:

| | `main` | this PR | change |
|---|--:|--:|--:|
| Main thread, 36 drags | 9.07 / 8.75 / 8.99 s | 8.22 / 8.12 / 8.15 s | −8.7% |
| JS thread, 36 drags | 5.01 / 4.88 / 5.05 s | 4.97 / 4.92 / 5.00 s | no change |
| `TouchesHelper.sentTouchEventModern(topTouchMove)` trace section, ~700 events | 449 µs | 309 µs | −31% |
| Native heap after the drags (`dumpsys meminfo`) | 44.3 / 42.5 / 42.4 MB | 33.2 / 33.3 / 33.1 MB | −10 MB |

  The trace-section times include the tracing overhead. `FabricEventEmitter.receiveEvent('topTouchMove')` is about 11–17 µs slower with this PR (95 → 111 µs), which is small next to the saving in `sentTouchEventModern`. In every run JS received all touch moves, 10 out of 10 taps on `Pressable` rows were handled, and there were no errors in logcat.

- Not run: a C++ unit test of `nativePutTouchPayload` (JVM tests can't load the native library; it is covered on the device by the runs above and, through the shadow, by `TouchEventDispatchTest`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

## Internal Buck validation

- `buck build fbsource//xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/jni/react/jni:jniAndroid fbsource//xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events:events-commonAndroid fbsource//xplat/js/react-native-github/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows:shadowsAndroid` — passed.
- `buck test fbsource//xplat/js/react-native-github/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events:eventsAndroid -- TouchEventDispatchTest` — 4 passed, 0 failed. Test run: https://www.internalfb.com/intern/testinfra/testrun/14355224025898138
- `arc lint -a` — passed.
- `arc lint -e extra --take CLANGTIDY --never-apply-patches xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/jni/react/jni/TouchesHelper.cpp` — no lint issues.
- Repository-managed `clang-format --dry-run --Werror` on `TouchesHelper.cpp` — passed.
- Compared the eight Kotlin/C++ field-index constants and the pointer stride; all match, and the producer, native readers and shadow use named indices.

Reviewed By: christophpurrer

Differential Revision: D128055442

Pulled By: javache
@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 29, 2026
@meta-codesync

meta-codesync Bot commented Oct 29, 2026

Copy link
Copy Markdown

@javache has exported this pull request. If you are a Meta employee, you can view the originating Diff in D128208007.

Summary:
Pull Request resolved: react#59595

Widen Android event payload contracts from `WritableMap` to `ReadableMap` across the event emitters, UI managers and Fabric dispatch pipeline, including `Event.getEventData`. Existing event producers can continue returning writable maps. Build native touch payloads as a complete `folly::dynamic` and return them through `ReadableNativeMap::createWithContents` without constructing and populating a writable Java map.

Document Fabric’s native-backed, consumed-payload contract, update the touch test shadow to return a read-only native map and regenerate the Android API snapshot.

Changelog:
[Android][Breaking] - Change Android event payload APIs from `WritableMap` to `ReadableMap`; update custom emitter and UI manager implementations to match.

Differential Revision: D128208007
@meta-codesync meta-codesync Bot changed the title Accept readable Android event payloads Accept readable Android event payloads (#59595) Oct 29, 2026

This branch has not been deployed

No deployments
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. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants