Repository navigation
Build Android touch event payloads in one JNI call - #59554
hryhoriiK97 wants to merge 4 commits into
Conversation
TouchesHelper built every touch event payload as nested WritableNativeMaps: for each pointer, eight JNI put calls, then several copies of the same map for the event, changedTouches and touches. Collect the pointer values into a DoubleArray instead and let WritableNativeMap.putTouchPayload build the whole payload in native code in a single JNI call. TouchesHelper still decides which pointers are in changedTouches and touches, as bit masks of pointer indices; the keys, values and value types of the payload are the same as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
javache
left a comment
There was a problem hiding this comment.
WritableNativeMap is a generic data structure, it should not be aware of touches.
Instead can we add a native method to TouchesHelper which creates the NativeMap specifically for that use-case?
WritableNativeMap is a generic data structure, so it no longer has a touch-specific putTouchPayload. TouchesHelper gets a static native method instead, registered like TransformHelper, which fills an empty WritableNativeMap through its existing C++ API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@javache I've moved it. I've re-measured on the same device: main thread −8.7% over 36 drags, |
| internal object TouchesHelper { | ||
| @JvmField @Deprecated("Not used in New Architecture") val TARGET_KEY: String = "target" | ||
| init { | ||
| ReactNativeJniCommonSoLoader.staticInit() |
There was a problem hiding this comment.
Should be ReactNativeJNISoLoader
There was a problem hiding this comment.
Done, it now loads ReactNativeJNISoLoader and is registered in OnLoad.cpp next to TransformHelper - 279fc96
| private const val PAGE_Y_KEY = "pageY" | ||
| private const val TIMESTAMP_KEY = "timestamp" | ||
| private const val POINTER_IDENTIFIER_KEY = "identifier" | ||
| @JvmField @Deprecated("Not used in New Architecture") val TARGET_KEY: String = "target" |
There was a problem hiding this comment.
This has been deprecated for long enough - let's remove it.
| val eventData = WritableNativeMap() | ||
| nativePutTouchPayload(eventData, pointerData, pointer, changedPointers, touchPointers) |
There was a problem hiding this comment.
Create the WritableNativeMap directly from C++
| val eventData = WritableNativeMap() | |
| nativePutTouchPayload(eventData, pointerData, pointer, changedPointers, touchPointers) | |
| val eventData = nativeCreateTouchPayload(pointerData, pointer, changedPointers, touchPointers) |
There was a problem hiding this comment.
Done - 279fc96 via newObjectJavaArgs() + cthis(). newObjectCxxArgs aborts here ("Attempt to set C++ native pointer twice"), since the Kotlin constructor already calls initHybrid. Same perf as before.
…ctnativejni Address review: nativeCreateTouchPayload now creates the WritableNativeMap in C++ and returns it, TouchesHelper loads reactnativejni and is registered there like TransformHelper, and the deprecated TARGET_KEY is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D128055442. |
Visual regressionNo visual differences at ios: no capture |
Summary:
On Android, every touch event goes through
TouchesHelper.sendTouchEvent, which builds the payload JS receives as nestedWritableNativeMaps.Current behavior. For each pointer of the
MotionEvent,createPointersArraycreates 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, andchangedTouchesandtouchesare 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 everyACTION_MOVE.With this change.
TouchesHelpercollects the pointer values once into aDoubleArray. For each event it creates an emptyWritableNativeMapand passes it with the pointer data to a new native method,TouchesHelper.nativePutTouchPayload, which fills it in C++ through the map's existingputDouble,putIntandputNativeArray. That is two JNI calls per event (creating the map and filling it) instead of 20–25.TouchesHelperstill decides which pointers go intochangedTouchesandtouches, 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.WritableNativeMapis unchanged. The native method is registered likeTransformHelper's, fromTouchesHelper.cppinreactnativejni_common, andTouchesHelperis@DoNotStripbecause the class is looked up by name when the library loads.TouchesHelperisinternal, so this adds no public Kotlin API; the C++ API snapshots get the newTouchesHelperclass. The layout of the pointer data (eight values per pointer) is documented onnativePutTouchPayload, 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
Test Plan:
TouchEventDispatchTestpasses. It checks the payloads of the start/move/end, start/move/cancel and multi-pointer start/pointer-up/cancel sequences, now throughShadowTouchesHelper, a Kotlin version ofnativePutTouchPayload, and compares the map contents. With the end case deliberately broken (lifted pointer kept intouches), 2 of its 3 tests fail.ktfmtand the repository'sclang-formatreport no changes for the touched files. The C++ API snapshots were regenerated withscripts/cxx-api../gradlew :packages:rn-tester:android:app:assembleRelease -PreactNativeArchitectures=arm64-v8asucceeds, frommainat 01d5527.MotionEvents 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 withtoHashMap(): 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.main. Two separate RNTester release APKs (this PR, andmainat 01d5527), RNTester Playground screen with aPanResponderdrag area, 36 drags of 1.8 s per launch driven byadb 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:mainTouchesHelper.sentTouchEventModern(topTouchMove)trace section, ~700 eventsdumpsys meminfo)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 insentTouchEventModern. In every run JS received all touch moves, 10 out of 10 taps onPressablerows were handled, and there were no errors in logcat.nativePutTouchPayload(JVM tests can't load the native library; it is covered on the device by the runs above and, through the shadow, byTouchEventDispatchTest).🤖 Generated with Claude Code