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?.();