Skip to content

Commit 0d96608

Browse files
zeyapfacebook-github-bot
authored andcommitted
Flush NativeAnimated batch via microtask instead of set/clearImmediate
Summary: We used `setImmediate` to schedule the operations in order to use microtasks queue (given the immediate shim used in RN). But jest tests treat setImmediate callback as macrotask like setTimeout (which is browser-aligned behavior). This makes it difficult to test the change introduced by featureflag `animatedShouldDebounceQueueFlush` and `cxxNativeAnimatedEnabled` Differential Revision: D108640934
1 parent dd5c383 commit 0d96608

2 files changed

Lines changed: 169 additions & 15 deletions

File tree

packages/react-native/src/private/animated/NativeAnimatedHelper.js

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {EventSubscription} from '../../../Libraries/vendor/emitter/EventEmi
2323

2424
import NativeAnimatedNonTurboModule from '../../../Libraries/Animated/NativeAnimatedModule';
2525
import NativeAnimatedTurboModule from '../../../Libraries/Animated/NativeAnimatedTurboModule';
26+
import queueMicrotask from '../../../Libraries/Core/Timers/queueMicrotask';
2627
import NativeEventEmitter from '../../../Libraries/EventEmitter/NativeEventEmitter';
2728
import RCTDeviceEventEmitter from '../../../Libraries/EventEmitter/RCTDeviceEventEmitter';
2829
import Platform from '../../../Libraries/Utilities/Platform';
@@ -57,7 +58,6 @@ const isSingleOpBatching =
5758
Platform.OS === 'android' &&
5859
NativeAnimatedModule?.queueAndExecuteBatchedOperations != null &&
5960
ReactNativeFeatureFlags.animatedShouldUseSingleOp();
60-
let flushQueueImmediate = null;
6161

6262
const eventListenerGetValueCallbacks: {
6363
[number]: (value: number) => void,
@@ -71,6 +71,20 @@ let globalEventEmitterAnimationFinishedListener: ?EventSubscription = null;
7171
const shouldSignalBatch: boolean =
7272
ReactNativeFeatureFlags.cxxNativeAnimatedEnabled();
7373

74+
let flushQueueGeneration = 1;
75+
function scheduleQueueFlush(): void {
76+
const generation = ++flushQueueGeneration;
77+
queueMicrotask(() => {
78+
if (generation !== flushQueueGeneration) {
79+
return;
80+
}
81+
API.flushQueue();
82+
});
83+
}
84+
function cancelQueueFlush(): void {
85+
flushQueueGeneration++;
86+
}
87+
7488
function createNativeOperations(): NonNullable<typeof NativeAnimatedModule> {
7589
const methodNames = [
7690
'createAnimatedNode', // 1
@@ -116,8 +130,7 @@ function createNativeOperations(): NonNullable<typeof NativeAnimatedModule> {
116130
// details, see `NativeAnimatedModule.queueAndExecuteBatchedOperations`.
117131
singleOpQueue.push(operationID, ...args);
118132
if (shouldSignalBatch) {
119-
clearImmediate(flushQueueImmediate);
120-
flushQueueImmediate = setImmediate(API.flushQueue);
133+
scheduleQueueFlush();
121134
}
122135
};
123136
}
@@ -137,8 +150,7 @@ function createNativeOperations(): NonNullable<typeof NativeAnimatedModule> {
137150
} else if (shouldSignalBatch) {
138151
// $FlowExpectedError[incompatible-call] - Dynamism.
139152
queue.push(() => method(...args));
140-
clearImmediate(flushQueueImmediate);
141-
flushQueueImmediate = setImmediate(API.flushQueue);
153+
scheduleQueueFlush();
142154
} else {
143155
// $FlowExpectedError[incompatible-call] - Dynamism.
144156
method(...args);
@@ -190,9 +202,7 @@ const API = {
190202
invariant(NativeAnimatedModule, 'Native animated module is not available');
191203

192204
if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) {
193-
const prevImmediate = flushQueueImmediate;
194-
clearImmediate(prevImmediate);
195-
flushQueueImmediate = setImmediate(API.flushQueue);
205+
scheduleQueueFlush();
196206
} else {
197207
API.flushQueue();
198208
}
@@ -218,7 +228,6 @@ const API = {
218228
NativeAnimatedModule,
219229
'Native animated module is not available',
220230
);
221-
flushQueueImmediate = null;
222231

223232
if (singleOpQueue.length === 0) {
224233
return;
@@ -239,7 +248,6 @@ const API = {
239248
NativeAnimatedModule,
240249
'Native animated module is not available',
241250
);
242-
flushQueueImmediate = null;
243251

244252
if (queue.length === 0) {
245253
return;
@@ -299,11 +307,10 @@ const API = {
299307

300308
waitingForQueuedOperations.add(id);
301309
queueOperations = true;
302-
if (
303-
ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush() &&
304-
flushQueueImmediate
305-
) {
306-
clearImmediate(flushQueueImmediate);
310+
// Entering explicit queue mode: drop any flush already scheduled so ops
311+
// accumulate until `disableQueue`.
312+
if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) {
313+
cancelQueueFlush();
307314
}
308315
},
309316
startAnimatingNode: (isSingleOpBatching
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import typeof TNativeAnimatedModule from '../../specs_DEPRECATED/modules/NativeAnimatedModule';
12+
13+
import {create} from '@react-native/jest-preset/jest/renderer';
14+
import * as React from 'react';
15+
16+
// Force the C++ Native Animated backend on so this suite exercises the
17+
// microtask-batched scheduling path regardless of the current flag default.
18+
// `cxxNativeAnimatedEnabled` is a native flag.
19+
jest.mock('../../featureflags/ReactNativeFeatureFlags', () => ({
20+
...jest.requireActual('../../featureflags/ReactNativeFeatureFlags'),
21+
cxxNativeAnimatedEnabled: () => true,
22+
}));
23+
24+
// The C++ backend flushes batched native operations on a microtask
25+
// (`scheduleQueueFlush` -> `queueMicrotask`), so drain it before asserting.
26+
const flushMicrotasks = (): Promise<void> => Promise.resolve();
27+
28+
describe('Native Animated scheduling (cxxNativeAnimatedEnabled)', () => {
29+
let NativeAnimatedModule: Exclude<TNativeAnimatedModule, null | void>;
30+
31+
function importModules() {
32+
return {
33+
// $FlowFixMe[unsafe-getters-setters]
34+
get Animated() {
35+
return require('../../../../Libraries/Animated/Animated').default;
36+
},
37+
// $FlowFixMe[unsafe-getters-setters]
38+
get ReactNativeFeatureFlags() {
39+
return require('../../featureflags/ReactNativeFeatureFlags');
40+
},
41+
};
42+
}
43+
44+
beforeEach(() => {
45+
jest.resetModules();
46+
jest.restoreAllMocks();
47+
jest
48+
.mock('../../../../Libraries/BatchedBridge/NativeModules', () => ({
49+
__esModule: true,
50+
default: {
51+
NativeAnimatedModule: {},
52+
PlatformConstants: {
53+
getConstants() {
54+
return {};
55+
},
56+
},
57+
},
58+
}))
59+
.mock('../../specs_DEPRECATED/modules/NativeAnimatedModule')
60+
.mock('../../../../Libraries/EventEmitter/NativeEventEmitter')
61+
// findNodeHandle is imported from RendererProxy so mock that whole module.
62+
.setMock('../../../../Libraries/ReactNative/RendererProxy', {
63+
findNodeHandle: () => 1,
64+
});
65+
66+
NativeAnimatedModule =
67+
// $FlowFixMe[incompatible-type]
68+
require('../../specs_DEPRECATED/modules/NativeAnimatedModule').default;
69+
// $FlowFixMe[cannot-write]
70+
// $FlowFixMe[incompatible-use]
71+
// $FlowFixMe[unsafe-object-assign]
72+
Object.assign(NativeAnimatedModule, {
73+
getValue: jest.fn(),
74+
addAnimatedEventToView: jest.fn(),
75+
connectAnimatedNodes: jest.fn(),
76+
connectAnimatedNodeToView: jest.fn(),
77+
createAnimatedNode: jest.fn(),
78+
disconnectAnimatedNodeFromView: jest.fn(),
79+
disconnectAnimatedNodes: jest.fn(),
80+
dropAnimatedNode: jest.fn(),
81+
extractAnimatedNodeOffset: jest.fn(),
82+
flattenAnimatedNodeOffset: jest.fn(),
83+
removeAnimatedEventFromView: jest.fn(),
84+
restoreDefaultValues: jest.fn(),
85+
setAnimatedNodeOffset: jest.fn(),
86+
setAnimatedNodeValue: jest.fn(),
87+
startAnimatingNode: jest.fn(),
88+
startListeningToAnimatedNodeValue: jest.fn(),
89+
stopAnimation: jest.fn(),
90+
stopListeningToAnimatedNodeValue: jest.fn(),
91+
});
92+
});
93+
94+
it('runs with cxxNativeAnimatedEnabled forced on', () => {
95+
const {ReactNativeFeatureFlags} = importModules();
96+
expect(ReactNativeFeatureFlags.cxxNativeAnimatedEnabled()).toBe(true);
97+
});
98+
99+
it('batches a synchronous Animated operation and flushes it on a microtask', async () => {
100+
const {Animated} = importModules();
101+
102+
const opacity = new Animated.Value(0);
103+
opacity.__makeNative();
104+
await create(<Animated.View style={{opacity}} />);
105+
106+
// With the C++ backend a synchronous Animated call is batched rather than
107+
// dispatched inline...
108+
opacity.setValue(0.5);
109+
expect(NativeAnimatedModule.setAnimatedNodeValue).not.toHaveBeenCalled();
110+
111+
// ...and reaches the native module once the microtask drains, with the same
112+
// arguments the inline (platform) backend would have sent.
113+
await flushMicrotasks();
114+
expect(NativeAnimatedModule.setAnimatedNodeValue).toHaveBeenCalledWith(
115+
expect.any(Number),
116+
0.5,
117+
);
118+
});
119+
120+
it('batches a native-driven animation start and flushes it on a microtask', async () => {
121+
const {Animated} = importModules();
122+
123+
const opacity = new Animated.Value(0);
124+
// Mount first so the style/props nodes are already created and flushed; this
125+
// isolates the `startAnimatingNode` operation produced by `start()` below
126+
// (otherwise `create()` would drain the flush before we can observe it).
127+
await create(<Animated.View style={{opacity}} />);
128+
129+
// Starting a native-driven animation batches `startAnimatingNode` rather
130+
// than dispatching it inline...
131+
Animated.timing(opacity, {
132+
toValue: 10,
133+
duration: 1000,
134+
useNativeDriver: true,
135+
}).start();
136+
expect(NativeAnimatedModule.startAnimatingNode).not.toHaveBeenCalled();
137+
138+
// ...and it reaches the native module once the microtask drains.
139+
await flushMicrotasks();
140+
expect(NativeAnimatedModule.startAnimatingNode).toHaveBeenCalledWith(
141+
expect.any(Number),
142+
expect.any(Number),
143+
expect.objectContaining({type: 'frames'}),
144+
expect.any(Function),
145+
);
146+
});
147+
});

0 commit comments

Comments
 (0)