Skip to content

Commit 04c288e

Browse files
hryhoriiK97meta-codesync[bot]
authored andcommitted
Build Android touch event payloads in one JNI call (#59554)
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: #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
1 parent c12d0b3 commit 04c288e

11 files changed

Lines changed: 306 additions & 77 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt‎

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,41 @@
77

88
package com.facebook.react.uimanager.events
99

10-
import com.facebook.react.bridge.Arguments
11-
import com.facebook.react.bridge.WritableArray
12-
import com.facebook.react.bridge.WritableMap
10+
import com.facebook.proguard.annotations.DoNotStrip
11+
import com.facebook.react.bridge.ReactNativeJNISoLoader
12+
import com.facebook.react.bridge.WritableNativeMap
1313
import com.facebook.react.uimanager.PixelUtil.pxToDp
1414
import com.facebook.systrace.Systrace
1515

1616
/**
1717
* Class responsible for generating catalyst touch events based on android
1818
* [android.view.MotionEvent].
1919
*/
20+
@DoNotStrip
2021
internal object TouchesHelper {
21-
@JvmField @Deprecated("Not used in New Architecture") val TARGET_KEY: String = "target"
22-
23-
private const val TARGET_SURFACE_KEY = "targetSurface"
24-
private const val CHANGED_TOUCHES_KEY = "changedTouches"
25-
private const val TOUCHES_KEY = "touches"
26-
private const val PAGE_X_KEY = "pageX"
27-
private const val PAGE_Y_KEY = "pageY"
28-
private const val TIMESTAMP_KEY = "timestamp"
29-
private const val POINTER_IDENTIFIER_KEY = "identifier"
22+
init {
23+
ReactNativeJNISoLoader.staticInit()
24+
}
3025

31-
private const val LOCATION_X_KEY = "locationX"
32-
private const val LOCATION_Y_KEY = "locationY"
26+
// Keep in sync with the field indices in TouchesHelper.cpp.
27+
const val PAGE_X_INDEX = 0
28+
const val PAGE_Y_INDEX = 1
29+
const val LOCATION_X_INDEX = 2
30+
const val LOCATION_Y_INDEX = 3
31+
const val TARGET_SURFACE_INDEX = 4
32+
const val TARGET_INDEX = 5
33+
const val TIMESTAMP_INDEX = 6
34+
const val IDENTIFIER_INDEX = 7
35+
const val POINTER_FIELDS = 8
3336

3437
/**
35-
* Creates catalyst pointers array in format that is expected by RCTEventEmitter JS module from
36-
* given {@param event} instance. This method use {@param reactTarget} parameter to set as a
37-
* target view id associated with current gesture.
38+
* Fills the pointer data of [nativeCreateTouchPayload]: for each pointer of the
39+
* [android.view.MotionEvent], in order, pageX, pageY, locationX, locationY, targetSurface,
40+
* target, timestamp and identifier.
3841
*/
39-
private fun createPointersArray(event: TouchEvent): Array<WritableMap?> {
42+
private fun createPointerData(event: TouchEvent): DoubleArray {
4043
val motionEvent = event.getMotionEvent()
41-
val touches = arrayOfNulls<WritableMap>(motionEvent.pointerCount)
44+
val pointerData = DoubleArray(motionEvent.pointerCount * POINTER_FIELDS)
4245

4346
// Calculate the coordinates for the target view.
4447
// The MotionEvent contains the X,Y of the touch in the coordinate space of the root view
@@ -51,29 +54,27 @@ internal object TouchesHelper {
5154
val targetViewCoordinateY = motionEvent.y - event.viewY
5255

5356
for (index in 0 until motionEvent.pointerCount) {
54-
val touch = Arguments.createMap()
57+
val field = index * POINTER_FIELDS
5558

5659
// pageX,Y values are relative to the RootReactView
5760
// the motionEvent already contains coordinates in that view
58-
touch.putDouble(PAGE_X_KEY, motionEvent.getX(index).pxToDp().toDouble())
59-
touch.putDouble(PAGE_Y_KEY, motionEvent.getY(index).pxToDp().toDouble())
61+
pointerData[field + PAGE_X_INDEX] = motionEvent.getX(index).pxToDp().toDouble()
62+
pointerData[field + PAGE_Y_INDEX] = motionEvent.getY(index).pxToDp().toDouble()
6063

6164
// locationX,Y values are relative to the target view
6265
// To compute the values for the view, we subtract that views location from the event X,Y
6366
val locationX = motionEvent.getX(index) - targetViewCoordinateX
6467
val locationY = motionEvent.getY(index) - targetViewCoordinateY
65-
touch.putDouble(LOCATION_X_KEY, locationX.pxToDp().toDouble())
66-
touch.putDouble(LOCATION_Y_KEY, locationY.pxToDp().toDouble())
68+
pointerData[field + LOCATION_X_INDEX] = locationX.pxToDp().toDouble()
69+
pointerData[field + LOCATION_Y_INDEX] = locationY.pxToDp().toDouble()
6770

68-
touch.putInt(TARGET_SURFACE_KEY, event.surfaceId)
69-
@Suppress("DEPRECATION") touch.putInt(TARGET_KEY, event.viewTag)
70-
touch.putDouble(TIMESTAMP_KEY, event.timestampMs.toDouble())
71-
touch.putDouble(POINTER_IDENTIFIER_KEY, motionEvent.getPointerId(index).toDouble())
72-
73-
touches[index] = touch
71+
pointerData[field + TARGET_SURFACE_INDEX] = event.surfaceId.toDouble()
72+
pointerData[field + TARGET_INDEX] = event.viewTag.toDouble()
73+
pointerData[field + TIMESTAMP_INDEX] = event.timestampMs.toDouble()
74+
pointerData[field + IDENTIFIER_INDEX] = motionEvent.getPointerId(index).toDouble()
7475
}
7576

76-
return touches
77+
return pointerData
7778
}
7879

7980
/**
@@ -82,8 +83,9 @@ internal object TouchesHelper {
8283
* more efficient manner.
8384
*
8485
* Touches have to be dispatched as separate events for each changed pointer to make JS process
85-
* them correctly. To avoid allocations, we preprocess touch events in Java world and then convert
86-
* them to native before dispatch.
86+
* them correctly. The pointers are collected once into a [DoubleArray], and each event's payload
87+
* is built from it in a single JNI call, instead of putting every value into a native map one by
88+
* one.
8789
*
8890
* @param eventEmitter emitter to dispatch event to
8991
* @param event the touch event to extract data from
@@ -95,52 +97,44 @@ internal object TouchesHelper {
9597
"TouchesHelper.sentTouchEventModern(" + event.getEventName() + ")",
9698
)
9799
try {
98-
val type = event.getTouchEventType()
99100
val motionEvent = event.getMotionEvent()
100-
var touches = createPointersArray(event)
101-
val changedTouches: Array<WritableMap?>?
102-
103-
when (type) {
101+
val pointerCount = motionEvent.pointerCount
102+
// Pointers are passed to native as bit masks of their indices.
103+
check(pointerCount < Int.SIZE_BITS) { "Too many pointers: $pointerCount" }
104+
val allPointers = (1 shl pointerCount) - 1
105+
val changedPointers: Int
106+
val touchPointers: Int
107+
108+
when (event.getTouchEventType()) {
104109
TouchEventType.START -> {
105-
val newPointerIndex = motionEvent.actionIndex
106-
107-
changedTouches = arrayOf(touches[newPointerIndex]?.copy())
110+
changedPointers = 1 shl motionEvent.actionIndex
111+
touchPointers = allPointers
108112
}
109113
TouchEventType.END -> {
110-
val finishedPointerIndex = motionEvent.actionIndex
114+
changedPointers = 1 shl motionEvent.actionIndex
111115
/*
112-
* Clear finished pointer index for compatibility with W3C touch "end" events, where the
116+
* Leave out the finished pointer for compatibility with W3C touch "end" events, where the
113117
* active touches don't include the set that has just been "ended".
114118
*/
115-
val finishedPointer = touches[finishedPointerIndex]
116-
touches[finishedPointerIndex] = null
117-
118-
changedTouches = arrayOf(finishedPointer)
119+
touchPointers = allPointers and changedPointers.inv()
119120
}
120121
TouchEventType.MOVE -> {
121-
changedTouches = arrayOfNulls(touches.size)
122-
var i = 0
123-
while (i < touches.size) {
124-
changedTouches[i] = touches[i]?.copy()
125-
i++
126-
}
122+
changedPointers = allPointers
123+
touchPointers = allPointers
127124
}
128125
TouchEventType.CANCEL -> {
129-
changedTouches = touches
130-
touches = arrayOfNulls(0)
126+
changedPointers = allPointers
127+
touchPointers = 0
131128
}
132129
}
133130

134-
for (touchData in changedTouches) {
131+
val pointerData = createPointerData(event)
132+
for (pointer in 0 until pointerCount) {
133+
if (changedPointers and (1 shl pointer) == 0) {
134+
continue
135+
}
135136
val eventData =
136-
touchData?.let { td ->
137-
val ed = td.copy()
138-
val changedTouchesArray = getWritableArray(/* copyObjects */ true, changedTouches)
139-
val touchesArray = getWritableArray(/* copyObjects */ true, touches)
140-
ed.putArray(CHANGED_TOUCHES_KEY, changedTouchesArray)
141-
ed.putArray(TOUCHES_KEY, touchesArray)
142-
ed
143-
}
137+
nativeCreateTouchPayload(pointerData, pointer, changedPointers, touchPointers)
144138

145139
eventEmitter.receiveEvent(
146140
event.surfaceId,
@@ -158,13 +152,20 @@ internal object TouchesHelper {
158152
}
159153
}
160154

161-
private fun getWritableArray(copyObjects: Boolean, objects: Array<WritableMap?>): WritableArray {
162-
val result = Arguments.createArray()
163-
for (obj in objects) {
164-
if (obj != null) {
165-
result.pushMap(if (copyObjects) obj.copy() else obj)
166-
}
167-
}
168-
return result
169-
}
155+
/**
156+
* Creates the payload of a touch event: the fields of pointer [eventPointer] and the
157+
* `changedTouches` and `touches` arrays of the pointers whose bits are set in [changedPointers]
158+
* and [touchPointers].
159+
*
160+
* @param pointerData 8 values for each pointer, in order: pageX, pageY, locationX, locationY,
161+
* targetSurface, target, timestamp and identifier.
162+
*/
163+
@DoNotStrip
164+
@JvmStatic
165+
private external fun nativeCreateTouchPayload(
166+
pointerData: DoubleArray,
167+
eventPointer: Int,
168+
changedPointers: Int,
169+
touchPointers: Int,
170+
): WritableNativeMap
170171
}

‎packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ add_library(
3232
OnLoad-common.cpp
3333
ReadableNativeArray.cpp
3434
ReadableNativeMap.cpp
35+
TouchesHelper.cpp
3536
TransformHelper.cpp
3637
WritableNativeArray.cpp
3738
WritableNativeMap.cpp

‎packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <fbjni/fbjni.h>
1212

1313
#include "InspectorNetworkRequestListener.h"
14+
#include "TouchesHelper.h"
1415
#include "TransformHelper.h"
1516

1617
#ifndef WITH_GLOGINIT
@@ -34,6 +35,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
3435
FLAGS_minloglevel = 0;
3536
#endif
3637
InspectorNetworkRequestListener::registerNatives();
38+
TouchesHelper::registerNatives();
3739
TransformHelper::registerNatives();
3840
});
3941
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "TouchesHelper.h"
9+
10+
#include <stdexcept>
11+
12+
#include "WritableNativeArray.h"
13+
#include "WritableNativeMap.h"
14+
15+
using namespace facebook::jni;
16+
17+
namespace facebook::react {
18+
19+
namespace {
20+
21+
// Keep in sync with the field indices in TouchesHelper.kt.
22+
constexpr jint kPageXIndex = 0;
23+
constexpr jint kPageYIndex = 1;
24+
constexpr jint kLocationXIndex = 2;
25+
constexpr jint kLocationYIndex = 3;
26+
constexpr jint kTargetSurfaceIndex = 4;
27+
constexpr jint kTargetIndex = 5;
28+
constexpr jint kTimestampIndex = 6;
29+
constexpr jint kIdentifierIndex = 7;
30+
constexpr jint kPointerFields = 8;
31+
32+
folly::dynamic createTouch(const double* fields) {
33+
folly::dynamic touch = folly::dynamic::object();
34+
// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic)
35+
touch.insert("pageX", fields[kPageXIndex]);
36+
touch.insert("pageY", fields[kPageYIndex]);
37+
touch.insert("locationX", fields[kLocationXIndex]);
38+
touch.insert("locationY", fields[kLocationYIndex]);
39+
touch.insert("targetSurface", static_cast<int>(fields[kTargetSurfaceIndex]));
40+
touch.insert("target", static_cast<int>(fields[kTargetIndex]));
41+
touch.insert("timestamp", fields[kTimestampIndex]);
42+
touch.insert("identifier", fields[kIdentifierIndex]);
43+
// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic)
44+
return touch;
45+
}
46+
47+
local_ref<WritableNativeMap::jhybridobject> createTouchPayload(
48+
alias_ref<jclass> /*unused*/,
49+
alias_ref<JArrayDouble> pointerData,
50+
jint eventPointer,
51+
jint changedPointers,
52+
jint touchPointers) {
53+
auto length = static_cast<jint>(pointerData->size());
54+
jint pointerCount = length / kPointerFields;
55+
// Pointers are selected by bits of the jint masks.
56+
if (length % kPointerFields != 0 || pointerCount >= 32 || eventPointer < 0 ||
57+
eventPointer >= pointerCount) {
58+
throw std::invalid_argument("Invalid touch pointer data");
59+
}
60+
auto values = pointerData->getRegion(0, length);
61+
62+
folly::dynamic changedTouches = folly::dynamic::array();
63+
folly::dynamic touches = folly::dynamic::array();
64+
for (jint index = 0; index < pointerCount; index++) {
65+
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
66+
const double* fields = values.get() + index * kPointerFields;
67+
if ((changedPointers & (1 << index)) != 0) {
68+
changedTouches.push_back(createTouch(fields));
69+
}
70+
if ((touchPointers & (1 << index)) != 0) {
71+
touches.push_back(createTouch(fields));
72+
}
73+
}
74+
75+
auto jPayload = WritableNativeMap::newObjectJavaArgs();
76+
auto* payload = jPayload->cthis();
77+
// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic)
78+
const double* fields = values.get() + eventPointer * kPointerFields;
79+
payload->putDouble("pageX", fields[kPageXIndex]);
80+
payload->putDouble("pageY", fields[kPageYIndex]);
81+
payload->putDouble("locationX", fields[kLocationXIndex]);
82+
payload->putDouble("locationY", fields[kLocationYIndex]);
83+
payload->putInt(
84+
"targetSurface", static_cast<int>(fields[kTargetSurfaceIndex]));
85+
payload->putInt("target", static_cast<int>(fields[kTargetIndex]));
86+
payload->putDouble("timestamp", fields[kTimestampIndex]);
87+
payload->putDouble("identifier", fields[kIdentifierIndex]);
88+
// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic)
89+
WritableNativeArray changedTouchesArray(std::move(changedTouches));
90+
payload->putNativeArray("changedTouches", &changedTouchesArray);
91+
WritableNativeArray touchesArray(std::move(touches));
92+
payload->putNativeArray("touches", &touchesArray);
93+
return jPayload;
94+
}
95+
96+
} // namespace
97+
98+
void TouchesHelper::registerNatives() {
99+
javaClassLocal()->registerNatives({
100+
makeNativeMethod("nativeCreateTouchPayload", createTouchPayload),
101+
});
102+
}
103+
104+
} // namespace facebook::react
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <fbjni/fbjni.h>
11+
12+
namespace facebook::react {
13+
14+
class TouchesHelper : public jni::JavaClass<TouchesHelper> {
15+
public:
16+
static auto constexpr *kJavaDescriptor = "Lcom/facebook/react/uimanager/events/TouchesHelper;";
17+
18+
static void registerNatives();
19+
};
20+
21+
} // namespace facebook::react

0 commit comments

Comments
 (0)