Skip to content

Commit 532f7ce

Browse files
rubennortemeta-codesync[bot]
authored andcommitted
Formalize event timestamps and propagate from host platform to JS (#55878)
Summary: Pull Request resolved: #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
1 parent 7989287 commit 532f7ce

34 files changed

+511
-60
lines changed

packages/react-native/React/Fabric/RCTConversions.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,28 @@
1313
#import <react/renderer/graphics/Color.h>
1414
#import <react/renderer/graphics/RCTPlatformColorUtils.h>
1515
#import <react/renderer/graphics/Transform.h>
16+
#import <react/timing/primitives.h>
1617

1718
NS_ASSUME_NONNULL_BEGIN
1819

20+
/*
21+
* Converts an iOS timestamp (seconds since boot, NOT including sleep time, from
22+
* NSProcessInfo.processInfo.systemUptime or UITouch.timestamp) to a HighResTimeStamp.
23+
*
24+
* iOS timestamps use mach_absolute_time() which doesn't account for sleep time,
25+
* while std::chrono::steady_clock uses mach_continuous_time() which does.
26+
* To handle this correctly, we compute the relative offset from the current time
27+
* and apply it to HighResTimeStamp::now().
28+
*/
29+
inline facebook::react::HighResTimeStamp RCTHighResTimeStampFromSeconds(NSTimeInterval seconds)
30+
{
31+
NSTimeInterval nowSystemUptime = NSProcessInfo.processInfo.systemUptime;
32+
NSTimeInterval delta = nowSystemUptime - seconds;
33+
auto deltaDuration =
34+
std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(delta));
35+
return facebook::react::HighResTimeStamp::now() - facebook::react::HighResDuration::fromChrono(deltaDuration);
36+
}
37+
1938
inline NSString *RCTNSStringFromString(
2039
const std::string &string,
2140
const NSStringEncoding &encoding = NSUTF8StringEncoding)

packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ static PointerEvent CreatePointerEventFromActivePointer(
285285
PointerEvent event = {};
286286
event.pointerId = activePointer.identifier;
287287
event.pointerType = PointerTypeCStringFromUITouchType(activePointer.touchType);
288+
event.timeStamp = RCTHighResTimeStampFromSeconds(activePointer.timestamp);
288289

289290
if (eventType == RCTPointerEventTypeCancel) {
290291
event.clientPoint = RCTPointFromCGPoint(CGPointZero);
@@ -345,7 +346,8 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData(
345346
CGPoint clientLocation,
346347
CGPoint screenLocation,
347348
CGPoint offsetLocation,
348-
UIKeyModifierFlags modifierFlags)
349+
UIKeyModifierFlags modifierFlags,
350+
HighResTimeStamp timeStamp)
349351
{
350352
PointerEvent event = {};
351353
event.pointerId = pointerId;
@@ -365,6 +367,7 @@ static PointerEvent CreatePointerEventFromIncompleteHoverData(
365367
event.tangentialPressure = 0.0;
366368
event.twist = 0;
367369
event.isPrimary = true;
370+
event.timeStamp = timeStamp;
368371

369372
return event;
370373
}
@@ -760,8 +763,11 @@ - (void)hovering:(UIHoverGestureRecognizer *)recognizer
760763

761764
modifierFlags = recognizer.modifierFlags;
762765

766+
// For hover events, use the current time as we don't have a precise timestamp
767+
HighResTimeStamp eventTimestamp = HighResTimeStamp::now();
768+
763769
PointerEvent event = CreatePointerEventFromIncompleteHoverData(
764-
pointerId, pointerType, clientLocation, screenLocation, offsetLocation, modifierFlags);
770+
pointerId, pointerType, clientLocation, screenLocation, offsetLocation, modifierFlags, eventTimestamp);
765771

766772
SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation);
767773
if (eventEmitter != nil) {

packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ static void UpdateActiveTouchWithUITouch(
6767
activeTouch.touch.pagePoint = RCTPointFromCGPoint(pagePoint);
6868

6969
activeTouch.touch.timestamp = uiTouch.timestamp;
70+
activeTouch.touch.timeStamp = RCTHighResTimeStampFromSeconds(uiTouch.timestamp);
7071

7172
if (RCTForceTouchAvailable()) {
7273
activeTouch.touch.force = RCTZeroIfNaN(uiTouch.force / uiTouch.maximumPossibleForce);

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2261,6 +2261,7 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid
22612261
public fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V
22622262
public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V
22632263
public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IZ)V
2264+
public fun receiveEvent (IILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IZJ)V
22642265
public fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V
22652266
public fun removeUIManagerEventListener (Lcom/facebook/react/bridge/UIManagerListener;)V
22662267
public fun resolveCustomDirectEventName (Ljava/lang/String;)Ljava/lang/String;
@@ -2287,6 +2288,7 @@ public final class com/facebook/react/fabric/mounting/SurfaceMountingManager {
22872288
public final fun attachRootView (Landroid/view/View;Lcom/facebook/react/uimanager/ThemedReactContext;)V
22882289
public final fun deleteView (I)V
22892290
public final fun enqueuePendingEvent (ILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;I)V
2291+
public final fun enqueuePendingEvent (ILjava/lang/String;ZLcom/facebook/react/bridge/WritableMap;IJ)V
22902292
public final fun getContext ()Lcom/facebook/react/uimanager/ThemedReactContext;
22912293
public final fun getSurfaceId ()I
22922294
public final fun getView (I)Landroid/view/View;
@@ -4804,11 +4806,13 @@ public final class com/facebook/react/uimanager/events/RCTEventEmitter$DefaultIm
48044806
public abstract interface class com/facebook/react/uimanager/events/RCTModernEventEmitter : com/facebook/react/uimanager/events/RCTEventEmitter {
48054807
public abstract fun receiveEvent (IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V
48064808
public abstract fun receiveEvent (IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;I)V
4809+
public abstract fun receiveEvent (IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;IJ)V
48074810
public abstract fun receiveEvent (ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V
48084811
}
48094812

48104813
public final class com/facebook/react/uimanager/events/RCTModernEventEmitter$DefaultImpls {
48114814
public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;IILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V
4815+
public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;IILjava/lang/String;ZILcom/facebook/react/bridge/WritableMap;IJ)V
48124816
public static fun receiveEvent (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;ILjava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V
48134817
public static fun receiveTouches (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;Ljava/lang/String;Lcom/facebook/react/bridge/WritableArray;Lcom/facebook/react/bridge/WritableArray;)V
48144818
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,11 +1067,13 @@ public void updateRootLayoutSpecs(
10671067
return surfaceManager.getView(reactTag);
10681068
}
10691069

1070+
@Deprecated
10701071
@Override
10711072
public void receiveEvent(int reactTag, String eventName, @Nullable WritableMap params) {
10721073
receiveEvent(View.NO_ID, reactTag, eventName, false, params, EventCategoryDef.UNSPECIFIED);
10731074
}
10741075

1076+
@Deprecated
10751077
@Override
10761078
public void receiveEvent(
10771079
int surfaceId, int reactTag, String eventName, @Nullable WritableMap params) {
@@ -1091,17 +1093,44 @@ public void receiveEvent(
10911093
* @param canCoalesceEvent
10921094
* @param params
10931095
* @param eventCategory
1096+
* @deprecated Use the overload with eventTimestamp parameter instead.
10941097
*/
1098+
@Deprecated
10951099
public void receiveEvent(
10961100
int surfaceId,
10971101
int reactTag,
10981102
String eventName,
10991103
boolean canCoalesceEvent,
11001104
@Nullable WritableMap params,
11011105
@EventCategoryDef int eventCategory) {
1102-
receiveEvent(surfaceId, reactTag, eventName, canCoalesceEvent, params, eventCategory, false);
1106+
receiveEvent(
1107+
surfaceId,
1108+
reactTag,
1109+
eventName,
1110+
canCoalesceEvent,
1111+
params,
1112+
eventCategory,
1113+
false,
1114+
SystemClock.uptimeMillis());
11031115
}
11041116

1117+
/**
1118+
* receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals
1119+
* that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before
1120+
* emitting.
1121+
*
1122+
* <p>{@code customCoalesceKey} is currently unused.
1123+
*
1124+
* @param surfaceId
1125+
* @param reactTag
1126+
* @param eventName
1127+
* @param canCoalesceEvent
1128+
* @param params
1129+
* @param eventCategory
1130+
* @param experimentalIsSynchronous
1131+
* @deprecated Use the overload with eventTimestamp parameter instead.
1132+
*/
1133+
@Deprecated
11051134
@Override
11061135
public void receiveEvent(
11071136
int surfaceId,
@@ -1111,6 +1140,43 @@ public void receiveEvent(
11111140
@Nullable WritableMap params,
11121141
@EventCategoryDef int eventCategory,
11131142
boolean experimentalIsSynchronous) {
1143+
receiveEvent(
1144+
surfaceId,
1145+
reactTag,
1146+
eventName,
1147+
canCoalesceEvent,
1148+
params,
1149+
eventCategory,
1150+
experimentalIsSynchronous,
1151+
SystemClock.uptimeMillis());
1152+
}
1153+
1154+
/**
1155+
* receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals
1156+
* that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before
1157+
* emitting.
1158+
*
1159+
* <p>{@code customCoalesceKey} is currently unused.
1160+
*
1161+
* @param surfaceId
1162+
* @param reactTag
1163+
* @param eventName
1164+
* @param canCoalesceEvent
1165+
* @param params
1166+
* @param eventCategory
1167+
* @param experimentalIsSynchronous
1168+
* @param eventTimestamp
1169+
*/
1170+
@Override
1171+
public void receiveEvent(
1172+
int surfaceId,
1173+
int reactTag,
1174+
String eventName,
1175+
boolean canCoalesceEvent,
1176+
@Nullable WritableMap params,
1177+
@EventCategoryDef int eventCategory,
1178+
boolean experimentalIsSynchronous,
1179+
long eventTimestamp) {
11141180

11151181
if (ReactBuildConfig.DEBUG && surfaceId == View.NO_ID) {
11161182
FLog.d(TAG, "Emitted event without surfaceId: [%d] %s", reactTag, eventName);
@@ -1128,7 +1194,13 @@ public void receiveEvent(
11281194
// access to the event emitter later when the view is mounted. For now just save the event
11291195
// in the view state and trigger it later.
11301196
mMountingManager.enqueuePendingEvent(
1131-
surfaceId, reactTag, eventName, canCoalesceEvent, params, eventCategory);
1197+
surfaceId,
1198+
reactTag,
1199+
eventName,
1200+
canCoalesceEvent,
1201+
params,
1202+
eventCategory,
1203+
eventTimestamp);
11321204
} else {
11331205
// This can happen if the view has disappeared from the screen (because of async events)
11341206
FLog.i(TAG, "Unable to invoke event: " + eventName + " for reactTag: " + reactTag);
@@ -1142,13 +1214,13 @@ public void receiveEvent(
11421214
boolean firstEventForFrame =
11431215
mSynchronousEvents.add(new SynchronousEvent(surfaceId, reactTag, eventName));
11441216
if (firstEventForFrame) {
1145-
eventEmitter.dispatchEventSynchronously(eventName, params);
1217+
eventEmitter.dispatchEventSynchronously(eventName, params, eventTimestamp);
11461218
}
11471219
} else {
11481220
if (canCoalesceEvent) {
1149-
eventEmitter.dispatchUnique(eventName, params);
1221+
eventEmitter.dispatchUnique(eventName, params, eventTimestamp);
11501222
} else {
1151-
eventEmitter.dispatch(eventName, params, eventCategory);
1223+
eventEmitter.dispatch(eventName, params, eventCategory, eventTimestamp);
11521224
}
11531225
}
11541226
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/EventEmitterWrapper.kt

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,49 @@ internal class EventEmitterWrapper private constructor() : HybridClassBase() {
2727
eventName: String,
2828
params: NativeMap?,
2929
@EventCategoryDef category: Int,
30+
eventTimestamp: Long,
3031
)
3132

32-
private external fun dispatchEventSynchronously(eventName: String, params: NativeMap?)
33+
private external fun dispatchEventSynchronously(
34+
eventName: String,
35+
params: NativeMap?,
36+
eventTimestamp: Long,
37+
)
3338

34-
private external fun dispatchUniqueEvent(eventName: String, params: NativeMap?)
39+
private external fun dispatchUniqueEvent(
40+
eventName: String,
41+
params: NativeMap?,
42+
eventTimestamp: Long,
43+
)
3544

3645
/**
3746
* Invokes the execution of the C++ EventEmitter.
3847
*
3948
* @param eventName [String] name of the event to execute.
4049
* @param params [WritableMap] payload of the event
50+
* @param eventCategory event category
51+
* @param eventTimestamp timestamp when the event was triggered (in milliseconds since boot)
4152
*/
4253
@Synchronized
43-
fun dispatch(eventName: String, params: WritableMap?, @EventCategoryDef eventCategory: Int) {
54+
fun dispatch(
55+
eventName: String,
56+
params: WritableMap?,
57+
@EventCategoryDef eventCategory: Int,
58+
eventTimestamp: Long,
59+
) {
4460
if (!isValid) {
4561
return
4662
}
47-
dispatchEvent(eventName, params as NativeMap?, eventCategory)
63+
dispatchEvent(eventName, params as NativeMap?, eventCategory, eventTimestamp)
4864
}
4965

5066
@Synchronized
51-
fun dispatchEventSynchronously(eventName: String, params: WritableMap?) {
67+
fun dispatchEventSynchronously(eventName: String, params: WritableMap?, eventTimestamp: Long) {
5268
if (!isValid) {
5369
return
5470
}
5571
UiThreadUtil.assertOnUiThread()
56-
dispatchEventSynchronously(eventName, params as NativeMap?)
72+
dispatchEventSynchronously(eventName, params as NativeMap?, eventTimestamp)
5773
}
5874

5975
/**
@@ -62,13 +78,14 @@ internal class EventEmitterWrapper private constructor() : HybridClassBase() {
6278
*
6379
* @param eventName [String] name of the event to execute.
6480
* @param params [WritableMap] payload of the event
81+
* @param eventTimestamp timestamp when the event was triggered (in milliseconds since boot)
6582
*/
6683
@Synchronized
67-
fun dispatchUnique(eventName: String, params: WritableMap?) {
84+
fun dispatchUnique(eventName: String, params: WritableMap?, eventTimestamp: Long) {
6885
if (!isValid) {
6986
return
7087
}
71-
dispatchUniqueEvent(eventName, params as NativeMap?)
88+
dispatchUniqueEvent(eventName, params as NativeMap?, eventTimestamp)
7289
}
7390

7491
@Synchronized

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/events/FabricEventEmitter.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

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

10+
import android.os.SystemClock
1011
import com.facebook.react.bridge.WritableMap
1112
import com.facebook.react.fabric.FabricUIManager
1213
import com.facebook.react.uimanager.events.EventCategoryDef
@@ -22,10 +23,41 @@ internal class FabricEventEmitter(private val uiManager: FabricUIManager) : RCTM
2223
customCoalesceKey: Int,
2324
params: WritableMap?,
2425
@EventCategoryDef category: Int,
26+
) {
27+
receiveEvent(
28+
surfaceId,
29+
targetTag,
30+
eventName,
31+
canCoalesceEvent,
32+
customCoalesceKey,
33+
params,
34+
category,
35+
SystemClock.uptimeMillis(),
36+
)
37+
}
38+
39+
override fun receiveEvent(
40+
surfaceId: Int,
41+
targetTag: Int,
42+
eventName: String,
43+
canCoalesceEvent: Boolean,
44+
customCoalesceKey: Int,
45+
params: WritableMap?,
46+
@EventCategoryDef category: Int,
47+
eventTimestamp: Long,
2548
) {
2649
Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricEventEmitter.receiveEvent('$eventName')")
2750
try {
28-
uiManager.receiveEvent(surfaceId, targetTag, eventName, canCoalesceEvent, params, category)
51+
uiManager.receiveEvent(
52+
surfaceId,
53+
targetTag,
54+
eventName,
55+
canCoalesceEvent,
56+
params,
57+
category,
58+
false,
59+
eventTimestamp,
60+
)
2961
} finally {
3062
Systrace.endSection(Systrace.TRACE_TAG_REACT)
3163
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ internal class MountingManager(
334334
canCoalesceEvent: Boolean,
335335
params: WritableMap?,
336336
@EventCategoryDef eventCategory: Int,
337+
eventTimestamp: Long,
337338
) {
338339
val smm = getSurfaceMountingManager(surfaceId, reactTag)
339340
if (smm == null) {
@@ -345,7 +346,14 @@ internal class MountingManager(
345346
)
346347
return
347348
}
348-
smm.enqueuePendingEvent(reactTag, eventName, canCoalesceEvent, params, eventCategory)
349+
smm.enqueuePendingEvent(
350+
reactTag,
351+
eventName,
352+
canCoalesceEvent,
353+
params,
354+
eventCategory,
355+
eventTimestamp,
356+
)
349357
}
350358

351359
private fun getSurfaceMountingManager(surfaceId: Int, reactTag: Int): SurfaceMountingManager? =

0 commit comments

Comments
 (0)