Skip to content

Commit 3789181

Browse files
rubennortemeta-codesync[bot]
authored andcommitted
Formalize event timestamps and propagate from host platform to JS
Summary: 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++. Differential Revision: D94669354
1 parent d6f2b34 commit 3789181

33 files changed

Lines changed: 501 additions & 59 deletions

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: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,9 +1099,32 @@ public void receiveEvent(
10991099
boolean canCoalesceEvent,
11001100
@Nullable WritableMap params,
11011101
@EventCategoryDef int eventCategory) {
1102-
receiveEvent(surfaceId, reactTag, eventName, canCoalesceEvent, params, eventCategory, false);
1102+
receiveEvent(
1103+
surfaceId,
1104+
reactTag,
1105+
eventName,
1106+
canCoalesceEvent,
1107+
params,
1108+
eventCategory,
1109+
false,
1110+
SystemClock.uptimeMillis());
11031111
}
11041112

1113+
/**
1114+
* receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals
1115+
* that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before
1116+
* emitting.
1117+
*
1118+
* <p>{@code customCoalesceKey} is currently unused.
1119+
*
1120+
* @param surfaceId
1121+
* @param reactTag
1122+
* @param eventName
1123+
* @param canCoalesceEvent
1124+
* @param params
1125+
* @param eventCategory
1126+
* @param experimentalIsSynchronous
1127+
*/
11051128
@Override
11061129
public void receiveEvent(
11071130
int surfaceId,
@@ -1111,6 +1134,43 @@ public void receiveEvent(
11111134
@Nullable WritableMap params,
11121135
@EventCategoryDef int eventCategory,
11131136
boolean experimentalIsSynchronous) {
1137+
receiveEvent(
1138+
surfaceId,
1139+
reactTag,
1140+
eventName,
1141+
canCoalesceEvent,
1142+
params,
1143+
eventCategory,
1144+
experimentalIsSynchronous,
1145+
SystemClock.uptimeMillis());
1146+
}
1147+
1148+
/**
1149+
* receiveEvent API that emits an event to C++. If {@code canCoalesceEvent} is true, that signals
1150+
* that C++ may coalesce the event optionally. Otherwise, coalescing can happen in Java before
1151+
* emitting.
1152+
*
1153+
* <p>{@code customCoalesceKey} is currently unused.
1154+
*
1155+
* @param surfaceId
1156+
* @param reactTag
1157+
* @param eventName
1158+
* @param canCoalesceEvent
1159+
* @param params
1160+
* @param eventCategory
1161+
* @param experimentalIsSynchronous
1162+
* @param eventTimestamp
1163+
*/
1164+
@Override
1165+
public void receiveEvent(
1166+
int surfaceId,
1167+
int reactTag,
1168+
String eventName,
1169+
boolean canCoalesceEvent,
1170+
@Nullable WritableMap params,
1171+
@EventCategoryDef int eventCategory,
1172+
boolean experimentalIsSynchronous,
1173+
long eventTimestamp) {
11141174

11151175
if (ReactBuildConfig.DEBUG && surfaceId == View.NO_ID) {
11161176
FLog.d(TAG, "Emitted event without surfaceId: [%d] %s", reactTag, eventName);
@@ -1128,7 +1188,13 @@ public void receiveEvent(
11281188
// access to the event emitter later when the view is mounted. For now just save the event
11291189
// in the view state and trigger it later.
11301190
mMountingManager.enqueuePendingEvent(
1131-
surfaceId, reactTag, eventName, canCoalesceEvent, params, eventCategory);
1191+
surfaceId,
1192+
reactTag,
1193+
eventName,
1194+
canCoalesceEvent,
1195+
params,
1196+
eventCategory,
1197+
eventTimestamp);
11321198
} else {
11331199
// This can happen if the view has disappeared from the screen (because of async events)
11341200
FLog.i(TAG, "Unable to invoke event: " + eventName + " for reactTag: " + reactTag);
@@ -1142,13 +1208,13 @@ public void receiveEvent(
11421208
boolean firstEventForFrame =
11431209
mSynchronousEvents.add(new SynchronousEvent(surfaceId, reactTag, eventName));
11441210
if (firstEventForFrame) {
1145-
eventEmitter.dispatchEventSynchronously(eventName, params);
1211+
eventEmitter.dispatchEventSynchronously(eventName, params, eventTimestamp);
11461212
}
11471213
} else {
11481214
if (canCoalesceEvent) {
1149-
eventEmitter.dispatchUnique(eventName, params);
1215+
eventEmitter.dispatchUnique(eventName, params, eventTimestamp);
11501216
} else {
1151-
eventEmitter.dispatch(eventName, params, eventCategory);
1217+
eventEmitter.dispatch(eventName, params, eventCategory, eventTimestamp);
11521218
}
11531219
}
11541220
}

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)