Skip to content

Commit 5fb9999

Browse files
authored
[General] Batch native operations (#3831)
## Description This takes things a step further than #3830. By wrapping every call to the native module (from V3) with `NativeProxy`, we have a centralized place that communication with the module always goes through. Using that, most needed operations are batched. The exceptions are `updateGestureHandlerConfig` (see comment) and `createGestureHandler`, which needs to run synchronously. This should reduce the amount of unnecessary native calls. On web, "everything is native" so no batching there. We may investigate the potential gains in the future, but due to differences in scheduling the same approach as on Android/iOS won't work. I've also changed `createGestureHandler` on iOS to create the `UIGestureRecognizer` immediately. This solved a weird "double free" crash I've been seeing. ## Test plan Tested on the same code as #3830 I saw improvements from ~600ms to ~595ms
1 parent a4f1074 commit 5fb9999

File tree

9 files changed

+122
-28
lines changed

9 files changed

+122
-28
lines changed

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,16 @@ class RNGestureHandlerModule(reactContext: ReactApplicationContext?) :
6161
}
6262

6363
@ReactMethod
64-
override fun createGestureHandler(handlerName: String, handlerTagDouble: Double, config: ReadableMap) {
64+
override fun createGestureHandler(handlerName: String, handlerTagDouble: Double, config: ReadableMap): Boolean {
6565
if (ReanimatedProxy.REANIMATED_INSTALLED && !uiRuntimeDecorated) {
6666
uiRuntimeDecorated = decorateUIRuntime()
6767
}
6868

6969
val handlerTag = handlerTagDouble.toInt()
7070

7171
createGestureHandlerHelper<GestureHandler>(handlerName, handlerTag, config)
72+
73+
return true
7274
}
7375

7476
@ReactMethod

packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ - (bool)installUIRuntimeBindings
119119
});
120120
}
121121

122-
- (void)createGestureHandler:(NSString *)handlerName handlerTag:(double)handlerTag config:(NSDictionary *)config
122+
- (NSNumber *)createGestureHandler:(NSString *)handlerName handlerTag:(double)handlerTag config:(NSDictionary *)config
123123
{
124124
if (!_checkedIfReanimatedIsAvailable) {
125125
_isReanimatedAvailable = [self.moduleRegistry moduleForName:"ReanimatedModule"] != nil;
@@ -129,9 +129,10 @@ - (void)createGestureHandler:(NSString *)handlerName handlerTag:(double)handlerT
129129
_uiRuntimeDecorated = [self installUIRuntimeBindings];
130130
}
131131

132-
[self addOperationBlock:^(RNGestureHandlerManager *manager) {
133-
[manager createGestureHandler:handlerName tag:[NSNumber numberWithDouble:handlerTag] config:config];
134-
}];
132+
RNGestureHandlerManager *manager = [RNGestureHandlerModule handlerManagerForModuleId:_moduleId];
133+
[manager createGestureHandler:handlerName tag:[NSNumber numberWithDouble:handlerTag] config:config];
134+
135+
return @1;
135136
}
136137

137138
- (void)attachGestureHandler:(double)handlerTag newView:(double)viewTag actionType:(double)actionType

packages/react-native-gesture-handler/src/handlers/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,26 @@ export function findNodeHandle(
6262
}
6363
return findNodeHandleRN(node) ?? null;
6464
}
65+
66+
let scheduledOperations: (() => void)[] = [];
6567
let flushOperationsScheduled = false;
6668

6769
export function scheduleFlushOperations() {
6870
if (!flushOperationsScheduled) {
6971
flushOperationsScheduled = true;
7072
ghQueueMicrotask(() => {
73+
for (const operation of scheduledOperations) {
74+
operation();
75+
}
76+
scheduledOperations = [];
7177
RNGestureHandlerModule.flushOperations();
7278

7379
flushOperationsScheduled = false;
7480
});
7581
}
7682
}
83+
84+
export function scheduleOperationToBeFlushed(operation: () => void) {
85+
scheduledOperations.push(operation);
86+
scheduleFlushOperations();
87+
}

packages/react-native-gesture-handler/src/specs/NativeRNGestureHandlerModule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface Spec extends TurboModule {
1010
// Record<> is not supported by codegen
1111
// eslint-disable-next-line @typescript-eslint/ban-types
1212
config: Object
13-
) => void;
13+
) => boolean;
1414
attachGestureHandler: (
1515
handlerTag: Double,
1616
newView: Double,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { scheduleOperationToBeFlushed } from '../handlers/utils';
2+
import RNGestureHandlerModule from '../RNGestureHandlerModule';
3+
import {
4+
BaseGestureConfig,
5+
GestureRelations,
6+
SingleGestureName,
7+
} from './types';
8+
9+
// Destructure functions that can be called on the UI thread to have
10+
// a raw HostFunction reference
11+
const { flushOperations, updateGestureHandlerConfig } = RNGestureHandlerModule;
12+
13+
export const NativeProxy = {
14+
createGestureHandler: <T extends Record<string, unknown>>(
15+
handlerName: SingleGestureName,
16+
handlerTag: number,
17+
config?: T
18+
) => {
19+
RNGestureHandlerModule.createGestureHandler(
20+
handlerName,
21+
handlerTag,
22+
config || {}
23+
);
24+
},
25+
setGestureHandlerConfig: <THandlerData, TConfig>(
26+
handlerTag: number,
27+
newConfig: BaseGestureConfig<THandlerData, TConfig>
28+
) => {
29+
scheduleOperationToBeFlushed(() => {
30+
RNGestureHandlerModule.setGestureHandlerConfig(handlerTag, newConfig);
31+
});
32+
},
33+
// updateGestureHandlerConfig can be called on the UI thread when using
34+
// SharedValue binding. Therefore, it needs to be a worklet and we flush
35+
// immediately since we're likely already on the UI thread.
36+
updateGestureHandlerConfig: <THandlerData, TConfig>(
37+
handlerTag: number,
38+
newConfig: BaseGestureConfig<THandlerData, TConfig>
39+
) => {
40+
'worklet';
41+
updateGestureHandlerConfig(handlerTag, newConfig);
42+
flushOperations();
43+
},
44+
dropGestureHandler: (handlerTag: number) => {
45+
scheduleOperationToBeFlushed(() => {
46+
RNGestureHandlerModule.dropGestureHandler(handlerTag);
47+
});
48+
},
49+
configureRelations: (handlerTag: number, relations: GestureRelations) => {
50+
scheduleOperationToBeFlushed(() => {
51+
RNGestureHandlerModule.configureRelations(handlerTag, relations);
52+
});
53+
},
54+
} as const;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import RNGestureHandlerModule from '../RNGestureHandlerModule';
2+
import {
3+
BaseGestureConfig,
4+
GestureRelations,
5+
SingleGestureName,
6+
} from './types';
7+
8+
export const NativeProxy = {
9+
createGestureHandler: <T extends Record<string, unknown>>(
10+
handlerName: SingleGestureName,
11+
handlerTag: number,
12+
config?: T
13+
) => {
14+
RNGestureHandlerModule.createGestureHandler(
15+
handlerName,
16+
handlerTag,
17+
config || {}
18+
);
19+
},
20+
setGestureHandlerConfig: <THandlerData, TConfig>(
21+
handlerTag: number,
22+
newConfig: BaseGestureConfig<THandlerData, TConfig>
23+
) => {
24+
RNGestureHandlerModule.setGestureHandlerConfig(handlerTag, newConfig);
25+
},
26+
updateGestureHandlerConfig: <THandlerData, TConfig>(
27+
handlerTag: number,
28+
newConfig: BaseGestureConfig<THandlerData, TConfig>
29+
) => {
30+
RNGestureHandlerModule.updateGestureHandlerConfig(handlerTag, newConfig);
31+
},
32+
dropGestureHandler: (handlerTag: number) => {
33+
RNGestureHandlerModule.dropGestureHandler(handlerTag);
34+
},
35+
configureRelations: (handlerTag: number, relations: GestureRelations) => {
36+
RNGestureHandlerModule.configureRelations(handlerTag, relations);
37+
},
38+
} as const;

packages/react-native-gesture-handler/src/v3/detectors/utils.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
// For `waitFor` we need array as order of the gestures matters.
55
// For `simultaneousHandlers` we use Set as the order doesn't matter.
66

7-
import { scheduleFlushOperations } from '../../handlers/utils';
8-
import RNGestureHandlerModule from '../../RNGestureHandlerModule';
97
import { tagMessage } from '../../utils';
108
import {
119
isComposedGesture,
1210
prepareRelations,
1311
} from '../hooks/utils/relationUtils';
12+
import { NativeProxy } from '../NativeProxy';
1413
import { ComposedGestureName, Gesture } from '../types';
1514

1615
// The tree consists of ComposedGestures and NativeGestures. NativeGestures are always leaf nodes.
@@ -27,7 +26,7 @@ export const traverseAndConfigureRelations = (
2726
node.gestureRelations.simultaneousHandlers.push(...simultaneousHandlers);
2827
node.gestureRelations.waitFor.push(...waitFor);
2928

30-
RNGestureHandlerModule.configureRelations(node.tag, {
29+
NativeProxy.configureRelations(node.tag, {
3130
waitFor: node.gestureRelations.waitFor,
3231
simultaneousHandlers: node.gestureRelations.simultaneousHandlers,
3332
blocksHandlers: node.gestureRelations.blocksHandlers,
@@ -143,13 +142,8 @@ export function configureRelations<THandlerData, TConfig>(
143142

144143
traverseAndConfigureRelations(gesture, simultaneousHandlers);
145144
} else {
146-
RNGestureHandlerModule.configureRelations(
147-
gesture.tag,
148-
gesture.gestureRelations
149-
);
145+
NativeProxy.configureRelations(gesture.tag, gesture.gestureRelations);
150146
}
151-
152-
scheduleFlushOperations();
153147
}
154148

155149
export function ensureNativeDetectorComponent(

packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useEffect, useMemo, useRef } from 'react';
22
import { getNextHandlerTag } from '../../handlers/getNextHandlerTag';
3-
import RNGestureHandlerModule from '../../RNGestureHandlerModule';
43
import { useGestureCallbacks } from './useGestureCallbacks';
54
import {
65
prepareConfig,
@@ -11,7 +10,7 @@ import {
1110
} from './utils';
1211
import { tagMessage } from '../../utils';
1312
import { BaseGestureConfig, SingleGesture, SingleGestureName } from '../types';
14-
import { scheduleFlushOperations } from '../../handlers/utils';
13+
import { NativeProxy } from '../NativeProxy';
1514

1615
export function useGesture<THandlerData, TConfig>(
1716
type: SingleGestureName,
@@ -81,22 +80,18 @@ export function useGesture<THandlerData, TConfig>(
8180
currentGestureRef.current.type !== (type as string)
8281
) {
8382
currentGestureRef.current = { type, tag };
84-
RNGestureHandlerModule.createGestureHandler(type, tag, {});
85-
// It's possible that this can cause errors about handler not being created when attempting to mount NativeDetector
86-
scheduleFlushOperations();
83+
NativeProxy.createGestureHandler(type, tag, {});
8784
}
8885

8986
useEffect(() => {
9087
return () => {
91-
RNGestureHandlerModule.dropGestureHandler(tag);
92-
scheduleFlushOperations();
88+
NativeProxy.dropGestureHandler(tag);
9389
};
9490
}, [type, tag]);
9591

9692
useEffect(() => {
9793
const preparedConfig = prepareConfigForNativeSide(type, config);
98-
RNGestureHandlerModule.setGestureHandlerConfig(tag, preparedConfig);
99-
scheduleFlushOperations();
94+
NativeProxy.setGestureHandlerConfig(tag, preparedConfig);
10095

10196
bindSharedValues(config, tag);
10297

packages/react-native-gesture-handler/src/v3/hooks/utils/reanimatedUtils.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import RNGestureHandlerModule from '../../../RNGestureHandlerModule';
1+
import { NativeProxy } from '../../NativeProxy';
22
import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper';
33
import {
44
BaseGestureConfig,
@@ -23,8 +23,8 @@ function hash(str: string) {
2323

2424
const SHARED_VALUE_OFFSET = 1.618;
2525

26-
// This is used to obtain HostFunction that can be executed on the UI thread
27-
const { updateGestureHandlerConfig, flushOperations } = RNGestureHandlerModule;
26+
// Don't transfer entire NativeProxy to the UI thread
27+
const { updateGestureHandlerConfig } = NativeProxy;
2828

2929
export function bindSharedValues<THandlerData, TConfig>(
3030
config: BaseGestureConfig<THandlerData, TConfig>,
@@ -52,7 +52,6 @@ export function bindSharedValues<THandlerData, TConfig>(
5252
} else {
5353
updateGestureHandlerConfig(handlerTag, { [configKey]: value });
5454
}
55-
flushOperations();
5655
});
5756
};
5857

0 commit comments

Comments
 (0)