From 1149d6c9b6f60b6db001c9e5233326195a0b874d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Wed, 4 Mar 2026 06:45:46 -0800 Subject: [PATCH] Formalize event timestamps and propagate from host platform to JS (#55878) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55878 Changelog: [General][Fixed] - Fix event timestamp propagation from host platforms to JS This adds event timestamps as a first class concept in Fabric during dispatch, and uses the new methods to dispatch events to pass the values from the host platform (Android and iOS). If the value isn't pass, the current timestamp at the time of dispatch is used (which is close enough). This fixes several problems: 1. Makes event timestamps account for native event dispatch delay. 2. Normalizes event timestamps across APIs (event timestamp property, Event Timing API information, etc.). NOTE: I had to implement clock correction on iOS because the timing we get from UITouch objects doesn't use the same clock as we do in C++. Reviewed By: javache, NickGerleman Differential Revision: D94669354 --- .../React/Fabric/RCTConversions.h | 19 +++++ .../React/Fabric/RCTSurfacePointerHandler.mm | 10 ++- .../React/Fabric/RCTSurfaceTouchHandler.mm | 1 + .../ReactAndroid/api/ReactAndroid.api | 4 + .../react/animated/EventAnimationDriver.kt | 1 + .../react/fabric/FabricUIManager.java | 82 +++++++++++++++++-- .../fabric/events/EventEmitterWrapper.kt | 33 ++++++-- .../react/fabric/events/FabricEventEmitter.kt | 35 +++++++- .../react/fabric/mounting/MountingManager.kt | 10 ++- .../fabric/mounting/SurfaceMountingManager.kt | 28 ++++++- .../uimanager/events/FabricEventDispatcher.kt | 1 + .../react/uimanager/events/PointerEvent.kt | 1 + .../uimanager/events/RCTModernEventEmitter.kt | 30 +++++++ .../events/SynchronousEventReceiver.kt | 28 +++++++ .../react/uimanager/events/TouchesHelper.kt | 1 + .../jni/react/fabric/EventEmitterWrapper.cpp | 32 ++++++-- .../jni/react/fabric/EventEmitterWrapper.h | 6 +- .../fabric/events/TouchEventDispatchTest.kt | 3 + .../renderer/components/view/BaseTouch.cpp | 5 +- .../renderer/components/view/BaseTouch.h | 7 ++ .../renderer/components/view/PointerEvent.cpp | 2 + .../renderer/components/view/PointerEvent.h | 5 ++ .../renderer/components/view/TouchEvent.h | 2 - .../components/view/TouchEventEmitter.cpp | 28 +++++-- .../components/view/TouchEventEmitter.h | 1 + .../react/renderer/core/EventEmitter.cpp | 69 +++++++++++++++- .../react/renderer/core/EventEmitter.h | 25 ++++++ .../react/renderer/core/EventPipe.h | 4 +- .../renderer/core/EventQueueProcessor.cpp | 3 +- .../react/renderer/core/RawEvent.cpp | 6 +- .../react/renderer/core/RawEvent.h | 12 +-- .../core/tests/EventQueueProcessorTest.cpp | 3 +- .../react/renderer/scheduler/Scheduler.cpp | 5 +- .../renderer/uimanager/UIManagerBinding.cpp | 27 ++++-- .../renderer/uimanager/UIManagerBinding.h | 7 +- .../__tests__/EventTimingAPI-itest.js | 38 +++++++++ 36 files changed, 514 insertions(+), 60 deletions(-) diff --git a/packages/react-native/React/Fabric/RCTConversions.h b/packages/react-native/React/Fabric/RCTConversions.h index 582c0f8e8344..cb14f8210fbd 100644 --- a/packages/react-native/React/Fabric/RCTConversions.h +++ b/packages/react-native/React/Fabric/RCTConversions.h @@ -13,9 +13,28 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN +/* + * Converts an iOS timestamp (seconds since boot, NOT including sleep time, from + * NSProcessInfo.processInfo.systemUptime or UITouch.timestamp) to a HighResTimeStamp. + * + * iOS timestamps use mach_absolute_time() which doesn't account for sleep time, + * while std::chrono::steady_clock uses mach_continuous_time() which does. + * To handle this correctly, we compute the relative offset from the current time + * and apply it to HighResTimeStamp::now(). + */ +inline facebook::react::HighResTimeStamp RCTHighResTimeStampFromSeconds(NSTimeInterval seconds) +{ + NSTimeInterval nowSystemUptime = NSProcessInfo.processInfo.systemUptime; + NSTimeInterval delta = nowSystemUptime - seconds; + auto deltaDuration = + std::chrono::duration_cast(std::chrono::duration(delta)); + return facebook::react::HighResTimeStamp::now() - facebook::react::HighResDuration::fromChrono(deltaDuration); +} + inline NSString *RCTNSStringFromString( const std::string &string, const NSStringEncoding &encoding = NSUTF8StringEncoding) diff --git a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm index e0078c094a36..18246804d43b 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm @@ -285,6 +285,7 @@ static PointerEvent CreatePointerEventFromActivePointer( PointerEvent event = {}; event.pointerId = activePointer.identifier; event.pointerType = PointerTypeCStringFromUITouchType(activePointer.touchType); + event.timeStamp = RCTHighResTimeStampFromSeconds(activePointer.timestamp); if (eventType == RCTPointerEventTypeCancel) { event.clientPoint = RCTPointFromCGPoint(CGPointZero); @@ -345,7 +346,8 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData( CGPoint clientLocation, CGPoint screenLocation, CGPoint offsetLocation, - UIKeyModifierFlags modifierFlags) + UIKeyModifierFlags modifierFlags, + HighResTimeStamp timeStamp) { PointerEvent event = {}; event.pointerId = pointerId; @@ -365,6 +367,7 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData( event.tangentialPressure = 0.0; event.twist = 0; event.isPrimary = true; + event.timeStamp = timeStamp; return event; } @@ -760,8 +763,11 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer modifierFlags = recognizer.modifierFlags; + // For hover events, use the current time as we don't have a precise timestamp + HighResTimeStamp eventTimestamp = HighResTimeStamp::now(); + PointerEvent event = CreatePointerEventFromIncompleteHoverData( - pointerId, pointerType, clientLocation, screenLocation, offsetLocation, modifierFlags); + pointerId, pointerType, clientLocation, screenLocation, offsetLocation, modifierFlags, eventTimestamp); SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation); if (eventEmitter != nil) { diff --git a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm index ea2952c23e78..4945d3559f60 100644 --- a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm @@ -67,6 +67,7 @@ static void UpdateActiveTouchWithUITouch( activeTouch.touch.pagePoint = RCTPointFromCGPoint(pagePoint); activeTouch.touch.timestamp = uiTouch.timestamp; + activeTouch.touch.timeStamp = RCTHighResTimeStampFromSeconds(uiTouch.timestamp); if (RCTForceTouchAvailable()) { activeTouch.touch.force = RCTZeroIfNaN(uiTouch.force / uiTouch.maximumPossibleForce); diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 22adca5790a3..4e3687261335 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2261,6 +2261,7 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IZ)V + public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IZJ)V public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V public fun removeUIManagerEventListener (Lcom/facebook/react/bridge/UIManagerListener;)V public fun resolveCustomDirectEventName (Ljava/lang/String;)Ljava/lang/String; @@ -2287,6 +2288,7 @@ public final class com/facebook/react/fabric/mounting/SurfaceMountingManager { public final fun attachRootView (Landroid/view/View;Lcom/facebook/react/uimanager/ThemedReactContext;)V public final fun deleteView (I)V public final fun enqueuePendingEvent (ILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V + public final fun enqueuePendingEvent (ILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IJ)V public final fun getContext ()Lcom/facebook/react/uimanager/ThemedReactContext; public final fun getSurfaceId ()I public final fun getView (I)Landroid/view/View; @@ -4804,11 +4806,13 @@ public final class com/facebook/react/uimanager/events/RCTEventEmitter$DefaultIm public abstract interface class com/facebook/react/uimanager/events/RCTModernEventEmitter : com/facebook/react/uimanager/events/RCTEventEmitter { public abstract fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V public abstract fun receiveEvent (IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;I)V + public abstract fun receiveEvent (IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;IJ)V public abstract fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V } public final class com/facebook/react/uimanager/events/RCTModernEventEmitter$DefaultImpls { public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V + public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;IJ)V public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V public static fun receiveTouches (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;Ljava/lang/String;Lcom/facebook/react/bridge/WritableArray;Lcom/facebook/react/bridge/WritableArray;)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.kt index 6d98e8fd2ae9..b40e19efa966 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/EventAnimationDriver.kt @@ -22,6 +22,7 @@ internal class EventAnimationDriver( private val eventPath: List, @JvmField internal var valueNode: ValueAnimatedNode, ) : RCTModernEventEmitter { + @Deprecated("Use the overload with eventTimestamp parameter instead.") override fun receiveEvent( surfaceId: Int, targetTag: Int, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index c0cacbb45abb..5e3839be80df 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -1067,11 +1067,13 @@ public void updateRootLayoutSpecs( return surfaceManager.getView(reactTag); } + @Deprecated @Override public void receiveEvent(int reactTag, String eventName, @Nullable WritableMap params) { receiveEvent(View.NO_ID, reactTag, eventName, false, params, EventCategoryDef.UNSPECIFIED); } + @Deprecated @Override public void receiveEvent( int surfaceId, int reactTag, String eventName, @Nullable WritableMap params) { @@ -1091,7 +1093,9 @@ public void receiveEvent( * @param canCoalesceEvent * @param params * @param eventCategory + * @deprecated Use the overload with eventTimestamp parameter instead. */ + @Deprecated public void receiveEvent( int surfaceId, int reactTag, @@ -1099,9 +1103,34 @@ public void receiveEvent( boolean canCoalesceEvent, @Nullable WritableMap params, @EventCategoryDef int eventCategory) { - receiveEvent(surfaceId, reactTag, eventName, canCoalesceEvent, params, eventCategory, false); + receiveEvent( + surfaceId, + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + false, + SystemClock.uptimeMillis()); } + /** + * receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals + * that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before + * emitting. + * + *

{@code customCoalesceKey} is currently unused. + * + * @param surfaceId + * @param reactTag + * @param eventName + * @param canCoalesceEvent + * @param params + * @param eventCategory + * @param experimentalIsSynchronous + * @deprecated Use the overload with eventTimestamp parameter instead. + */ + @Deprecated @Override public void receiveEvent( int surfaceId, @@ -1111,6 +1140,43 @@ public void receiveEvent( @Nullable WritableMap params, @EventCategoryDef int eventCategory, boolean experimentalIsSynchronous) { + receiveEvent( + surfaceId, + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + experimentalIsSynchronous, + SystemClock.uptimeMillis()); + } + + /** + * receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals + * that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before + * emitting. + * + *

{@code customCoalesceKey} is currently unused. + * + * @param surfaceId + * @param reactTag + * @param eventName + * @param canCoalesceEvent + * @param params + * @param eventCategory + * @param experimentalIsSynchronous + * @param eventTimestamp + */ + @Override + public void receiveEvent( + int surfaceId, + int reactTag, + String eventName, + boolean canCoalesceEvent, + @Nullable WritableMap params, + @EventCategoryDef int eventCategory, + boolean experimentalIsSynchronous, + long eventTimestamp) { if (ReactBuildConfig.DEBUG && surfaceId == View.NO_ID) { FLog.d(TAG, "Emitted event without surfaceId: [%d] %s", reactTag, eventName); @@ -1128,7 +1194,13 @@ public void receiveEvent( // access to the event emitter later when the view is mounted. For now just save the event // in the view state and trigger it later. mMountingManager.enqueuePendingEvent( - surfaceId, reactTag, eventName, canCoalesceEvent, params, eventCategory); + surfaceId, + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + eventTimestamp); } else { // This can happen if the view has disappeared from the screen (because of async events) FLog.i(TAG, "Unable to invoke event: " + eventName + " for reactTag: " + reactTag); @@ -1142,13 +1214,13 @@ public void receiveEvent( boolean firstEventForFrame = mSynchronousEvents.add(new SynchronousEvent(surfaceId, reactTag, eventName)); if (firstEventForFrame) { - eventEmitter.dispatchEventSynchronously(eventName, params); + eventEmitter.dispatchEventSynchronously(eventName, params, eventTimestamp); } } else { if (canCoalesceEvent) { - eventEmitter.dispatchUnique(eventName, params); + eventEmitter.dispatchUnique(eventName, params, eventTimestamp); } else { - eventEmitter.dispatch(eventName, params, eventCategory); + eventEmitter.dispatch(eventName, params, eventCategory, eventTimestamp); } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt index 1d1a3bcd860f..4a61bf606ad9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt @@ -27,33 +27,49 @@ internal class EventEmitterWrapper private constructor() : HybridClassBase() { eventName: String, params: NativeMap?, @EventCategoryDef category: Int, + eventTimestamp: Long, ) - private external fun dispatchEventSynchronously(eventName: String, params: NativeMap?) + private external fun dispatchEventSynchronously( + eventName: String, + params: NativeMap?, + eventTimestamp: Long, + ) - private external fun dispatchUniqueEvent(eventName: String, params: NativeMap?) + private external fun dispatchUniqueEvent( + eventName: String, + params: NativeMap?, + eventTimestamp: Long, + ) /** * Invokes the execution of the C++ EventEmitter. * * @param eventName [String] name of the event to execute. * @param params [WritableMap] payload of the event + * @param eventCategory event category + * @param eventTimestamp timestamp when the event was triggered (in milliseconds since boot) */ @Synchronized - fun dispatch(eventName: String, params: WritableMap?, @EventCategoryDef eventCategory: Int) { + fun dispatch( + eventName: String, + params: WritableMap?, + @EventCategoryDef eventCategory: Int, + eventTimestamp: Long, + ) { if (!isValid) { return } - dispatchEvent(eventName, params as NativeMap?, eventCategory) + dispatchEvent(eventName, params as NativeMap?, eventCategory, eventTimestamp) } @Synchronized - fun dispatchEventSynchronously(eventName: String, params: WritableMap?) { + fun dispatchEventSynchronously(eventName: String, params: WritableMap?, eventTimestamp: Long) { if (!isValid) { return } UiThreadUtil.assertOnUiThread() - dispatchEventSynchronously(eventName, params as NativeMap?) + dispatchEventSynchronously(eventName, params as NativeMap?, eventTimestamp) } /** @@ -62,13 +78,14 @@ internal class EventEmitterWrapper private constructor() : HybridClassBase() { * * @param eventName [String] name of the event to execute. * @param params [WritableMap] payload of the event + * @param eventTimestamp timestamp when the event was triggered (in milliseconds since boot) */ @Synchronized - fun dispatchUnique(eventName: String, params: WritableMap?) { + fun dispatchUnique(eventName: String, params: WritableMap?, eventTimestamp: Long) { if (!isValid) { return } - dispatchUniqueEvent(eventName, params as NativeMap?) + dispatchUniqueEvent(eventName, params as NativeMap?, eventTimestamp) } @Synchronized diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt index fe7425902046..dad9cd3d5556 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt @@ -7,6 +7,7 @@ package com.facebook.react.fabric.events +import android.os.SystemClock import com.facebook.react.bridge.WritableMap import com.facebook.react.fabric.FabricUIManager import com.facebook.react.uimanager.events.EventCategoryDef @@ -14,6 +15,7 @@ import com.facebook.react.uimanager.events.RCTModernEventEmitter import com.facebook.systrace.Systrace internal class FabricEventEmitter(private val uiManager: FabricUIManager) : RCTModernEventEmitter { + @Deprecated("Use the overload with eventTimestamp parameter instead.") override fun receiveEvent( surfaceId: Int, targetTag: Int, @@ -22,10 +24,41 @@ internal class FabricEventEmitter(private val uiManager: FabricUIManager) : RCTM customCoalesceKey: Int, params: WritableMap?, @EventCategoryDef category: Int, + ) { + receiveEvent( + surfaceId, + targetTag, + eventName, + canCoalesceEvent, + customCoalesceKey, + params, + category, + SystemClock.uptimeMillis(), + ) + } + + override fun receiveEvent( + surfaceId: Int, + targetTag: Int, + eventName: String, + canCoalesceEvent: Boolean, + customCoalesceKey: Int, + params: WritableMap?, + @EventCategoryDef category: Int, + eventTimestamp: Long, ) { Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricEventEmitter.receiveEvent('$eventName')") try { - uiManager.receiveEvent(surfaceId, targetTag, eventName, canCoalesceEvent, params, category) + uiManager.receiveEvent( + surfaceId, + targetTag, + eventName, + canCoalesceEvent, + params, + category, + false, + eventTimestamp, + ) } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt index a6ff8f1af233..aa0365037ec2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt @@ -334,6 +334,7 @@ internal class MountingManager( canCoalesceEvent: Boolean, params: WritableMap?, @EventCategoryDef eventCategory: Int, + eventTimestamp: Long, ) { val smm = getSurfaceMountingManager(surfaceId, reactTag) if (smm == null) { @@ -345,7 +346,14 @@ internal class MountingManager( ) return } - smm.enqueuePendingEvent(reactTag, eventName, canCoalesceEvent, params, eventCategory) + smm.enqueuePendingEvent( + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + eventTimestamp, + ) } private fun getSurfaceMountingManager(surfaceId: Int, reactTag: Int): SurfaceMountingManager? = diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index 9224e19ddf3b..a8ead9ca6be6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -8,6 +8,7 @@ package com.facebook.react.fabric.mounting import android.annotation.SuppressLint +import android.os.SystemClock import android.view.View import android.view.ViewGroup import android.view.ViewParent @@ -1082,6 +1083,25 @@ internal constructor( canCoalesceEvent: Boolean, params: WritableMap?, @EventCategoryDef eventCategory: Int, + ): Unit { + enqueuePendingEvent( + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + SystemClock.uptimeMillis(), + ) + } + + @AnyThread + public fun enqueuePendingEvent( + reactTag: Int, + eventName: String, + canCoalesceEvent: Boolean, + params: WritableMap?, + @EventCategoryDef eventCategory: Int, + eventTimestamp: Long, ): Unit { // When the surface stopped we will reset the view state map. We are not going to enqueue // pending events as they are not expected to be dispatched anyways. @@ -1092,7 +1112,8 @@ internal constructor( return } - val viewEvent = PendingViewEvent(eventName, params, eventCategory, canCoalesceEvent) + val viewEvent = + PendingViewEvent(eventName, params, eventCategory, canCoalesceEvent, eventTimestamp) UiThreadUtil.runOnUiThread { val eventEmitter = viewState.eventEmitter if (eventEmitter != null) { @@ -1145,12 +1166,13 @@ internal constructor( private val params: WritableMap?, @field:EventCategoryDef private val eventCategory: Int, private val canCoalesceEvent: Boolean, + private val eventTimestamp: Long, ) { fun dispatch(eventEmitter: EventEmitterWrapper) { if (canCoalesceEvent) { - eventEmitter.dispatchUnique(eventName, params) + eventEmitter.dispatchUnique(eventName, params, eventTimestamp) } else { - eventEmitter.dispatch(eventName, params, eventCategory) + eventEmitter.dispatch(eventName, params, eventCategory, eventTimestamp) } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.kt index 409d307d26dd..a708ce5904a2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.kt @@ -72,6 +72,7 @@ internal class FabricEventDispatcher( event.internal_getEventData(), event.internal_getEventCategory(), true, + event.timestampMs, ) } else { ReactSoftExceptionLogger.logSoftException( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.kt index 6cd9cb5a83a7..ed14d9925750 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEvent.kt @@ -283,6 +283,7 @@ internal class PointerEvent private constructor() : Event() { coalescingKey.toInt(), eventData, getEventCategory(_eventName), + timestampMs, ) } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTModernEventEmitter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTModernEventEmitter.kt index 70024f55278b..2720516397b7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTModernEventEmitter.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/RCTModernEventEmitter.kt @@ -28,11 +28,13 @@ public interface RCTModernEventEmitter : RCTEventEmitter { receiveEvent(-1, targetTag, eventName, params) } + @Deprecated("Use the overload with eventTimestamp parameter instead.") public fun receiveEvent(surfaceId: Int, targetTag: Int, eventName: String, params: WritableMap?) { // We assume this event can't be coalesced. `customCoalesceKey` has no meaning in Fabric. receiveEvent(surfaceId, targetTag, eventName, false, 0, params, EventCategoryDef.UNSPECIFIED) } + @Deprecated("Use the overload with eventTimestamp parameter instead.") public fun receiveEvent( surfaceId: Int, targetTag: Int, @@ -42,4 +44,32 @@ public interface RCTModernEventEmitter : RCTEventEmitter { params: WritableMap?, @EventCategoryDef category: Int, ) + + /** + * Receives an event with a specific timestamp. The default implementation delegates to the + * non-timestamped version for backward compatibility with existing implementations. + * + * @param eventTimestamp The timestamp when the event was triggered (in milliseconds since boot, + * from SystemClock.uptimeMillis()) + */ + public fun receiveEvent( + surfaceId: Int, + targetTag: Int, + eventName: String, + canCoalesceEvent: Boolean, + customCoalesceKey: Int, + params: WritableMap?, + @EventCategoryDef category: Int, + eventTimestamp: Long, + ) { + receiveEvent( + surfaceId, + targetTag, + eventName, + canCoalesceEvent, + customCoalesceKey, + params, + category, + ) + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SynchronousEventReceiver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SynchronousEventReceiver.kt index a5d35ff46682..7d6cb6c429bf 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SynchronousEventReceiver.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SynchronousEventReceiver.kt @@ -21,4 +21,32 @@ internal interface SynchronousEventReceiver { @EventCategoryDef eventCategory: Int, experimentalIsSynchronous: Boolean, ) + + /** + * Receives an event with a specific timestamp. The default implementation delegates to the + * non-timestamped version for backward compatibility with existing implementations. + * + * @param eventTimestamp timestamp when the event was triggered (in milliseconds since boot, from + * SystemClock.uptimeMillis()) + */ + fun receiveEvent( + surfaceId: Int, + reactTag: Int, + eventName: String, + canCoalesceEvent: Boolean, + params: WritableMap?, + @EventCategoryDef eventCategory: Int, + experimentalIsSynchronous: Boolean, + eventTimestamp: Long, + ) { + receiveEvent( + surfaceId, + reactTag, + eventName, + canCoalesceEvent, + params, + eventCategory, + experimentalIsSynchronous, + ) + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt index d4fb44db9d12..b80ed8229f11 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchesHelper.kt @@ -150,6 +150,7 @@ internal object TouchesHelper { 0, eventData, event.getEventCategory(), + event.timestampMs, ) } } finally { diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.cpp index 68e64c6c4d05..5d76e5e197fe 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.cpp @@ -7,6 +7,7 @@ #include "EventEmitterWrapper.h" #include +#include #include @@ -14,10 +15,24 @@ using namespace facebook::jni; namespace facebook::react { +namespace { + +/* + * Converts a Java timestamp (milliseconds since boot from + * SystemClock.uptimeMillis()) to a HighResTimeStamp. + */ +HighResTimeStamp highResTimeStampFromMillis(jlong millis) { + return HighResTimeStamp::fromChronoSteadyClockTimePoint( + std::chrono::steady_clock::time_point(std::chrono::milliseconds(millis))); +} + +} // namespace + void EventEmitterWrapper::dispatchEvent( std::string eventName, NativeMap* payload, - int category) { + int category, + jlong eventTimestamp) { // It is marginal, but possible for this to be constructed without a valid // EventEmitter. In those cases, make sure we noop/blackhole events instead of // crashing. @@ -25,13 +40,15 @@ void EventEmitterWrapper::dispatchEvent( eventEmitter->dispatchEvent( std::move(eventName), (payload != nullptr) ? payload->consume() : folly::dynamic::object(), - static_cast(category)); + static_cast(category), + highResTimeStampFromMillis(eventTimestamp)); } } void EventEmitterWrapper::dispatchEventSynchronously( std::string eventName, - NativeMap* params) { + NativeMap* params, + jlong eventTimestamp) { // It is marginal, but possible for this to be constructed without a valid // EventEmitter. In those cases, make sure we noop/blackhole events instead of // crashing. @@ -40,21 +57,24 @@ void EventEmitterWrapper::dispatchEventSynchronously( eventEmitter->dispatchEvent( std::move(eventName), (params != nullptr) ? params->consume() : folly::dynamic::object(), - RawEvent::Category::Discrete); + RawEvent::Category::Discrete, + highResTimeStampFromMillis(eventTimestamp)); }); } } void EventEmitterWrapper::dispatchUniqueEvent( std::string eventName, - NativeMap* payload) { + NativeMap* payload, + jlong eventTimestamp) { // It is marginal, but possible for this to be constructed without a valid // EventEmitter. In those cases, make sure we noop/blackhole events instead of // crashing. if (eventEmitter != nullptr) { eventEmitter->dispatchUniqueEvent( std::move(eventName), - (payload != nullptr) ? payload->consume() : folly::dynamic::object()); + (payload != nullptr) ? payload->consume() : folly::dynamic::object(), + highResTimeStampFromMillis(eventTimestamp)); } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.h index 20bed32f575a..50143ad8f6c9 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/EventEmitterWrapper.h @@ -25,9 +25,9 @@ class EventEmitterWrapper : public jni::HybridClass { SharedEventEmitter eventEmitter; - void dispatchEvent(std::string eventName, NativeMap *payload, int category); - void dispatchEventSynchronously(std::string eventName, NativeMap *params); - void dispatchUniqueEvent(std::string eventName, NativeMap *payload); + void dispatchEvent(std::string eventName, NativeMap *payload, int category, jlong eventTimestamp); + void dispatchEventSynchronously(std::string eventName, NativeMap *params, jlong eventTimestamp); + void dispatchUniqueEvent(std::string eventName, NativeMap *payload, jlong eventTimestamp); }; } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt index de461fe0bb7a..ebecb3a9ef97 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/events/TouchEventDispatchTest.kt @@ -546,6 +546,7 @@ class TouchEventDispatchTest { anyInt(), argument.capture(), anyInt(), + anyLong(), ) assertThat(startMoveEndExpectedSequence).isEqualTo(argument.allValues) } @@ -565,6 +566,7 @@ class TouchEventDispatchTest { anyInt(), argument.capture(), anyInt(), + anyLong(), ) assertThat(startMoveCancelExpectedSequence).isEqualTo(argument.allValues) } @@ -584,6 +586,7 @@ class TouchEventDispatchTest { anyInt(), argument.capture(), anyInt(), + anyLong(), ) assertThat(startPointerMoveUpExpectedSequence).isEqualTo(argument.allValues) } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.cpp index 944d290e85e9..d09f0e78f3a1 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.cpp @@ -21,7 +21,8 @@ void setTouchPayloadOnObject( object.setProperty(runtime, "screenY", touch.screenPoint.y); object.setProperty(runtime, "identifier", touch.identifier); object.setProperty(runtime, "target", touch.target); - object.setProperty(runtime, "timestamp", touch.timestamp * 1000); + object.setProperty( + runtime, "timestamp", touch.timeStamp.toDOMHighResTimeStamp()); object.setProperty(runtime, "force", touch.force); } @@ -41,7 +42,7 @@ std::vector getDebugProps( {"identifier", getDebugDescription(touch.identifier, options)}, {"target", getDebugDescription(touch.target, options)}, {"force", getDebugDescription(touch.force, options)}, - {"timestamp", getDebugDescription(touch.timestamp, options)}, + {"timeStamp", getDebugDescription(touch.timeStamp, options)}, }; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.h index a0742b29827e..c8473d65b388 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseTouch.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace facebook::react { @@ -53,9 +54,15 @@ struct BaseTouch { /* * The time in seconds when the touch occurred or when it was last mutated. + * @deprecated Use timeStamp instead. */ Float timestamp{0.0f}; + /* + * The time when the touch occurred. + */ + HighResTimeStamp timeStamp{}; + /* * The particular implementation of `Hasher` and (especially) `Comparator` * make sense only when `Touch` object is used as a *key* in indexed diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/PointerEvent.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/PointerEvent.cpp index 46a3ba86996e..958c3e4f8f89 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/PointerEvent.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/PointerEvent.cpp @@ -98,6 +98,8 @@ std::vector getDebugProps( .value = getDebugDescription(pointerEvent.isPrimary, options)}, {.name = "button", .value = getDebugDescription(pointerEvent.button, options)}, + {.name = "timeStamp", + .value = getDebugDescription(pointerEvent.timeStamp, options)}, }; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/PointerEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/PointerEvent.h index 158d73d20965..218782c5e684 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/PointerEvent.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/PointerEvent.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace facebook::react { @@ -110,6 +111,10 @@ struct PointerEvent : public EventPayload { * was fired. */ int button; + /* + * The time when the event occurred. + */ + HighResTimeStamp timeStamp{}; /* * EventPayload implementations diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEvent.h index 3907869831ee..94819dc3cbf1 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEvent.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEvent.h @@ -9,8 +9,6 @@ #include -#include - #include namespace facebook::react { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp index 6264d5fe50d6..cf003d91820d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp @@ -42,26 +42,38 @@ static jsi::Value touchEventPayload( return object; } +static HighResTimeStamp getTimestampFromTouchEvent(const TouchEvent& event) { + if (!event.changedTouches.empty()) { + const auto& firstChangedTouch = *event.changedTouches.begin(); + return firstChangedTouch.timeStamp; + } + return HighResTimeStamp::now(); +} + void TouchEventEmitter::dispatchTouchEvent( std::string type, TouchEvent event, RawEvent::Category category) const { + auto eventTimestamp = getTimestampFromTouchEvent(event); dispatchEvent( std::move(type), [event = std::move(event)](jsi::Runtime& runtime) { return touchEventPayload(runtime, event); }, - category); + category, + eventTimestamp); } void TouchEventEmitter::dispatchPointerEvent( std::string type, PointerEvent event, RawEvent::Category category) const { + auto eventTimestamp = event.timeStamp; dispatchEvent( std::move(type), std::make_shared(std::move(event)), - category); + category, + eventTimestamp); } void TouchEventEmitter::onTouchStart(TouchEvent event) const { @@ -70,10 +82,13 @@ void TouchEventEmitter::onTouchStart(TouchEvent event) const { } void TouchEventEmitter::onTouchMove(TouchEvent event) const { + auto eventTimestamp = getTimestampFromTouchEvent(event); dispatchUniqueEvent( - "touchMove", [event = std::move(event)](jsi::Runtime& runtime) { + "touchMove", + [event = std::move(event)](jsi::Runtime& runtime) { return touchEventPayload(runtime, event); - }); + }, + eventTimestamp); } void TouchEventEmitter::onTouchEnd(TouchEvent event) const { @@ -101,8 +116,11 @@ void TouchEventEmitter::onPointerDown(PointerEvent event) const { } void TouchEventEmitter::onPointerMove(PointerEvent event) const { + auto eventTimestamp = event.timeStamp; dispatchUniqueEvent( - "pointerMove", std::make_shared(std::move(event))); + "pointerMove", + std::make_shared(std::move(event)), + eventTimestamp); } void TouchEventEmitter::onPointerUp(PointerEvent event) const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.h index 74c200293223..7f8fa707cc9a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace facebook::react { diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp index f162e2ffb615..91ef8e2d9489 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp @@ -63,6 +63,18 @@ void EventEmitter::dispatchEvent( category); } +void EventEmitter::dispatchEvent( + std::string type, + folly::dynamic&& payload, + RawEvent::Category category, + HighResTimeStamp eventTimestamp) const { + dispatchEvent( + std::move(type), + DynamicEventPayload::create(std::move(payload)), + category, + eventTimestamp); +} + void EventEmitter::dispatchUniqueEvent( std::string type, folly::dynamic&& payload) const { @@ -70,6 +82,16 @@ void EventEmitter::dispatchUniqueEvent( std::move(type), DynamicEventPayload::create(std::move(payload))); } +void EventEmitter::dispatchUniqueEvent( + std::string type, + folly::dynamic&& payload, + HighResTimeStamp eventTimestamp) const { + dispatchUniqueEvent( + std::move(type), + DynamicEventPayload::create(std::move(payload)), + eventTimestamp); +} + void EventEmitter::dispatchEvent( std::string type, const ValueFactory& payloadFactory, @@ -80,10 +102,31 @@ void EventEmitter::dispatchEvent( category); } +void EventEmitter::dispatchEvent( + std::string type, + const ValueFactory& payloadFactory, + RawEvent::Category category, + HighResTimeStamp eventTimestamp) const { + dispatchEvent( + std::move(type), + std::make_shared(payloadFactory), + category, + eventTimestamp); +} + void EventEmitter::dispatchEvent( std::string type, SharedEventPayload payload, RawEvent::Category category) const { + dispatchEvent( + std::move(type), std::move(payload), category, HighResTimeStamp::now()); +} + +void EventEmitter::dispatchEvent( + std::string type, + SharedEventPayload payload, + RawEvent::Category category, + HighResTimeStamp eventTimestamp) const { TraceSection s("EventEmitter::dispatchEvent", "type", type); auto eventDispatcher = eventDispatcher_.lock(); @@ -96,7 +139,9 @@ void EventEmitter::dispatchEvent( std::move(payload), eventTarget_, shadowNodeFamily_, - category)); + category, + false, + eventTimestamp)); } void EventEmitter::dispatchUniqueEvent( @@ -107,9 +152,27 @@ void EventEmitter::dispatchUniqueEvent( std::make_shared(payloadFactory)); } +void EventEmitter::dispatchUniqueEvent( + std::string type, + const ValueFactory& payloadFactory, + HighResTimeStamp eventTimestamp) const { + dispatchUniqueEvent( + std::move(type), + std::make_shared(payloadFactory), + eventTimestamp); +} + void EventEmitter::dispatchUniqueEvent( std::string type, SharedEventPayload payload) const { + dispatchUniqueEvent( + std::move(type), std::move(payload), HighResTimeStamp::now()); +} + +void EventEmitter::dispatchUniqueEvent( + std::string type, + SharedEventPayload payload, + HighResTimeStamp eventTimestamp) const { TraceSection s("EventEmitter::dispatchUniqueEvent"); auto eventDispatcher = eventDispatcher_.lock(); @@ -122,7 +185,9 @@ void EventEmitter::dispatchUniqueEvent( std::move(payload), eventTarget_, shadowNodeFamily_, - RawEvent::Category::Continuous)); + RawEvent::Category::Continuous, + true, + eventTimestamp)); } void EventEmitter::setEnabled(bool enabled) { diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h index e89dcf2c18e6..f3e9a4c334e7 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h @@ -16,6 +16,7 @@ #include #include #include +#include namespace facebook::react { @@ -86,6 +87,12 @@ class EventEmitter { const ValueFactory &payloadFactory = EventEmitter::defaultPayloadFactory(), RawEvent::Category category = RawEvent::Category::Unspecified) const; + void dispatchEvent( + std::string type, + const ValueFactory &payloadFactory, + RawEvent::Category category, + HighResTimeStamp eventTimestamp) const; + void dispatchEvent( std::string type, folly::dynamic &&payload, @@ -96,13 +103,31 @@ class EventEmitter { SharedEventPayload payload, RawEvent::Category category = RawEvent::Category::Unspecified) const; + void dispatchEvent( + std::string type, + folly::dynamic &&payload, + RawEvent::Category category, + HighResTimeStamp eventTimestamp) const; + + void dispatchEvent( + std::string type, + SharedEventPayload payload, + RawEvent::Category category, + HighResTimeStamp eventTimestamp) const; + void dispatchUniqueEvent(std::string type, folly::dynamic &&payload) const; void dispatchUniqueEvent(std::string type, const ValueFactory &payloadFactory = EventEmitter::defaultPayloadFactory()) const; + void dispatchUniqueEvent(std::string type, const ValueFactory &payloadFactory, HighResTimeStamp eventTimestamp) const; + void dispatchUniqueEvent(std::string type, SharedEventPayload payload) const; + void dispatchUniqueEvent(std::string type, folly::dynamic &&payload, HighResTimeStamp eventTimestamp) const; + + void dispatchUniqueEvent(std::string type, SharedEventPayload payload, HighResTimeStamp eventTimestamp) const; + private: friend class UIManagerBinding; diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventPipe.h b/packages/react-native/ReactCommon/react/renderer/core/EventPipe.h index 233ae236cab9..a632ffc9e443 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventPipe.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventPipe.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace facebook::react { @@ -23,7 +24,8 @@ using EventPipe = std::function; + const EventPayload &payload, + HighResTimeStamp eventTimestamp)>; using EventPipeConclusion = std::function; diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.cpp index 90cdc8f9888a..f48276d0f112 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventQueueProcessor.cpp @@ -96,7 +96,8 @@ void EventQueueProcessor::flushEvents( event.eventTarget.get(), event.type, reactPriority, - *event.eventPayload); + *event.eventPayload, + event.eventStartTimeStamp); if (eventLogger != nullptr) { eventLogger->onEventProcessingEnd(event.loggingTag); diff --git a/packages/react-native/ReactCommon/react/renderer/core/RawEvent.cpp b/packages/react-native/ReactCommon/react/renderer/core/RawEvent.cpp index 7661b6705548..bced302982b1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/RawEvent.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/RawEvent.cpp @@ -15,12 +15,14 @@ RawEvent::RawEvent( SharedEventTarget eventTarget, std::weak_ptr shadowNodeFamily, Category category, - bool isUnique) + bool isUnique, + HighResTimeStamp eventStartTimeStamp) : type(std::move(type)), eventPayload(std::move(eventPayload)), eventTarget(std::move(eventTarget)), shadowNodeFamily(std::move(shadowNodeFamily)), category(category), - isUnique(isUnique) {} + isUnique(isUnique), + eventStartTimeStamp(eventStartTimeStamp) {} } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/RawEvent.h b/packages/react-native/ReactCommon/react/renderer/core/RawEvent.h index d78cbd118606..3111ee7cddfe 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/RawEvent.h +++ b/packages/react-native/ReactCommon/react/renderer/core/RawEvent.h @@ -8,7 +8,6 @@ #pragma once #include -#include #include #include @@ -74,7 +73,8 @@ struct RawEvent { SharedEventTarget eventTarget, std::weak_ptr shadowNodeFamily, Category category = Category::Unspecified, - bool isUnique = false); + bool isUnique = false, + HighResTimeStamp eventStartTimeStamp = HighResTimeStamp::now()); std::string type; SharedEventPayload eventPayload; @@ -84,10 +84,10 @@ struct RawEvent { EventTag loggingTag{0}; bool isUnique{false}; - // The client may specify a platform-specific timestamp for the event start - // time, for example when MotionEvent was triggered on the Android native - // side. - std::optional eventStartTimeStamp = std::nullopt; + // The timestamp for the event start time. This defaults to the current + // time if not specified by the client (e.g., when MotionEvent was triggered + // on the Android native side). + HighResTimeStamp eventStartTimeStamp; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueProcessorTest.cpp b/packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueProcessorTest.cpp index b6ebd6f49055..28a2d7213065 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueProcessorTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueProcessorTest.cpp @@ -42,7 +42,8 @@ class EventQueueProcessorTest : public testing::Test { const EventTarget* /*eventTarget*/, const std::string& type, ReactEventPriority priority, - const EventPayload& /*payload*/) { + const EventPayload& /*payload*/, + HighResTimeStamp /*eventTimestamp*/) { eventTypes_.push_back(type); eventPriorities_.push_back(priority); }; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index a0cf4e16089e..3a1393ef40b0 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -88,11 +88,12 @@ Scheduler::Scheduler( EventTarget* eventTarget, const std::string& type, ReactEventPriority priority, - const EventPayload& payload) { + const EventPayload& payload, + HighResTimeStamp eventTimestamp) { uiManager->visitBinding( [&](const UIManagerBinding& uiManagerBinding) { uiManagerBinding.dispatchEvent( - runtime, eventTarget, type, priority, payload); + runtime, eventTarget, type, priority, payload, eventTimestamp); }, runtime); }; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index d8b41c200e3d..4b405f2db2bb 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -65,12 +65,13 @@ void UIManagerBinding::dispatchEvent( EventTarget* eventTarget, const std::string& type, ReactEventPriority priority, - const EventPayload& eventPayload) const { + const EventPayload& eventPayload, + HighResTimeStamp eventTimestamp) const { TraceSection s("UIManagerBinding::dispatchEvent", "type", type); if (eventPayload.getType() == EventPayloadType::PointerEvent) { auto pointerEvent = static_cast(eventPayload); - auto dispatchCallback = [this, &runtime]( + auto dispatchCallback = [this, &runtime, eventTimestamp]( const ShadowNode& targetNode, const std::string& type, ReactEventPriority priority, @@ -79,7 +80,12 @@ void UIManagerBinding::dispatchEvent( if (eventTarget != nullptr) { eventTarget->retain(runtime); this->dispatchEventToJS( - runtime, eventTarget.get(), type, priority, eventPayload); + runtime, + eventTarget.get(), + type, + priority, + eventPayload, + eventTimestamp); eventTarget->release(runtime); } }; @@ -95,7 +101,8 @@ void UIManagerBinding::dispatchEvent( *uiManager_); } } else { - dispatchEventToJS(runtime, eventTarget, type, priority, eventPayload); + dispatchEventToJS( + runtime, eventTarget, type, priority, eventPayload, eventTimestamp); } } @@ -104,7 +111,8 @@ void UIManagerBinding::dispatchEventToJS( EventTarget* eventTarget, const std::string& type, ReactEventPriority priority, - const EventPayload& eventPayload) const { + const EventPayload& eventPayload, + HighResTimeStamp eventTimestamp) const { auto payload = eventPayload.asJSIValue(runtime); // If a payload is null, the factory has decided to cancel the event @@ -136,6 +144,15 @@ void UIManagerBinding::dispatchEventToJS( << " will be dropped"; } + // Add timestamp to payload if not already set + if (payload.isObject()) { + auto payloadObject = payload.asObject(runtime); + if (!payloadObject.hasProperty(runtime, "timeStamp")) { + payloadObject.setProperty( + runtime, "timeStamp", eventTimestamp.toDOMHighResTimeStamp()); + } + } + currentEventPriority_ = priority; if (eventHandler_) { eventHandler_->call( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.h index 08cd49005e24..8ed65a645e44 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace facebook::react { @@ -47,7 +48,8 @@ class UIManagerBinding : public jsi::HostObject { EventTarget *eventTarget, const std::string &type, ReactEventPriority priority, - const EventPayload &payload) const; + const EventPayload &payload, + HighResTimeStamp eventTimestamp) const; /* * Invalidates the binding and underlying UIManager. @@ -76,7 +78,8 @@ class UIManagerBinding : public jsi::HostObject { EventTarget *eventTarget, const std::string &type, ReactEventPriority priority, - const EventPayload &payload) const; + const EventPayload &payload, + HighResTimeStamp eventTimestamp) const; std::shared_ptr uiManager_; std::unique_ptr eventHandler_; diff --git a/packages/react-native/src/private/webapis/performance/__tests__/EventTimingAPI-itest.js b/packages/react-native/src/private/webapis/performance/__tests__/EventTimingAPI-itest.js index 18921768dfd7..398ecd712ce7 100644 --- a/packages/react-native/src/private/webapis/performance/__tests__/EventTimingAPI-itest.js +++ b/packages/react-native/src/private/webapis/performance/__tests__/EventTimingAPI-itest.js @@ -184,6 +184,44 @@ describe('Event Timing API', () => { expect(entry.interactionId).toBeGreaterThanOrEqual(0); }); + it('provides the event timeStamp as startTime', () => { + const callback = jest.fn(); + + const observer = new PerformanceObserver(callback); + observer.observe({entryTypes: ['event']}); + + let eventTimeStamp; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + eventTimeStamp = event.timeStamp; + }} + />, + ); + }); + + const element = nullthrows(root.document.documentElement.firstElementChild); + + expect(callback).not.toHaveBeenCalled(); + + Fantom.dispatchNativeEvent(element, 'click'); + + expect(callback).toHaveBeenCalledTimes(1); + + const entryList = callback.mock.lastCall[0] as PerformanceObserverEntryList; + const entries = entryList.getEntries(); + + expect(entries.length).toBe(1); + + const entry = ensurePerformanceEventTiming(entries[0]); + + expect(eventTimeStamp).not.toBeUndefined(); + expect(entry.startTime).toBe(eventTimeStamp); + }); + it('reports number of dispatched events via performance.eventCounts', () => { NativePerformance.clearEventCountsForTesting?.();