Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 22 additions & 15 deletions packages/react-native/src/private/animated/NativeAnimatedHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {EventSubscription} from '../../../Libraries/vendor/emitter/EventEmi

import NativeAnimatedNonTurboModule from '../../../Libraries/Animated/NativeAnimatedModule';
import NativeAnimatedTurboModule from '../../../Libraries/Animated/NativeAnimatedTurboModule';
import queueMicrotask from '../../../Libraries/Core/Timers/queueMicrotask';
import NativeEventEmitter from '../../../Libraries/EventEmitter/NativeEventEmitter';
import RCTDeviceEventEmitter from '../../../Libraries/EventEmitter/RCTDeviceEventEmitter';
import Platform from '../../../Libraries/Utilities/Platform';
Expand Down Expand Up @@ -57,7 +58,6 @@ const isSingleOpBatching =
Platform.OS === 'android' &&
NativeAnimatedModule?.queueAndExecuteBatchedOperations != null &&
ReactNativeFeatureFlags.animatedShouldUseSingleOp();
let flushQueueImmediate = null;

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

let flushQueueGeneration = 1;
function scheduleQueueFlush(): void {
const generation = ++flushQueueGeneration;
queueMicrotask(() => {
if (generation !== flushQueueGeneration) {
return;
}
API.flushQueue();
});
}
function cancelQueueFlush(): void {
flushQueueGeneration++;
}

function createNativeOperations(): NonNullable<typeof NativeAnimatedModule> {
const methodNames = [
'createAnimatedNode', // 1
Expand Down Expand Up @@ -116,8 +130,7 @@ function createNativeOperations(): NonNullable<typeof NativeAnimatedModule> {
// details, see `NativeAnimatedModule.queueAndExecuteBatchedOperations`.
singleOpQueue.push(operationID, ...args);
if (shouldSignalBatch) {
clearImmediate(flushQueueImmediate);
flushQueueImmediate = setImmediate(API.flushQueue);
scheduleQueueFlush();
}
};
}
Expand All @@ -137,8 +150,7 @@ function createNativeOperations(): NonNullable<typeof NativeAnimatedModule> {
} else if (shouldSignalBatch) {
// $FlowExpectedError[incompatible-call] - Dynamism.
queue.push(() => method(...args));
clearImmediate(flushQueueImmediate);
flushQueueImmediate = setImmediate(API.flushQueue);
scheduleQueueFlush();
} else {
// $FlowExpectedError[incompatible-call] - Dynamism.
method(...args);
Expand Down Expand Up @@ -190,9 +202,7 @@ const API = {
invariant(NativeAnimatedModule, 'Native animated module is not available');

if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) {
const prevImmediate = flushQueueImmediate;
clearImmediate(prevImmediate);
flushQueueImmediate = setImmediate(API.flushQueue);
scheduleQueueFlush();
} else {
API.flushQueue();
}
Expand All @@ -218,7 +228,6 @@ const API = {
NativeAnimatedModule,
'Native animated module is not available',
);
flushQueueImmediate = null;

if (singleOpQueue.length === 0) {
return;
Expand All @@ -239,7 +248,6 @@ const API = {
NativeAnimatedModule,
'Native animated module is not available',
);
flushQueueImmediate = null;

if (queue.length === 0) {
return;
Expand Down Expand Up @@ -299,11 +307,10 @@ const API = {

waitingForQueuedOperations.add(id);
queueOperations = true;
if (
ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush() &&
flushQueueImmediate
) {
clearImmediate(flushQueueImmediate);
// Entering explicit queue mode: drop any flush already scheduled so ops
// accumulate until `disableQueue`.
if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) {
cancelQueueFlush();
}
},
startAnimatingNode: (isSingleOpBatching
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import typeof TNativeAnimatedModule from '../../specs_DEPRECATED/modules/NativeAnimatedModule';

import {create} from '@react-native/jest-preset/jest/renderer';
import * as React from 'react';

// Force the C++ Native Animated backend on so this suite exercises the
// microtask-batched scheduling path regardless of the current flag default.
// `cxxNativeAnimatedEnabled` is a native flag.
jest.mock('../../featureflags/ReactNativeFeatureFlags', () => ({
...jest.requireActual('../../featureflags/ReactNativeFeatureFlags'),
cxxNativeAnimatedEnabled: () => true,
}));

// The C++ backend flushes batched native operations on a microtask
// (`scheduleQueueFlush` -> `queueMicrotask`), so drain it before asserting.
const flushMicrotasks = (): Promise<void> => Promise.resolve();

describe('Native Animated scheduling (cxxNativeAnimatedEnabled)', () => {
let NativeAnimatedModule: Exclude<TNativeAnimatedModule, null | void>;

function importModules() {
return {
// $FlowFixMe[unsafe-getters-setters]
get Animated() {
return require('../../../../Libraries/Animated/Animated').default;
},
// $FlowFixMe[unsafe-getters-setters]
get ReactNativeFeatureFlags() {
return require('../../featureflags/ReactNativeFeatureFlags');
},
};
}

beforeEach(() => {
jest.resetModules();
jest.restoreAllMocks();
jest
.mock('../../../../Libraries/BatchedBridge/NativeModules', () => ({
__esModule: true,
default: {
NativeAnimatedModule: {},
PlatformConstants: {
getConstants() {
return {};
},
},
},
}))
.mock('../../specs_DEPRECATED/modules/NativeAnimatedModule')
.mock('../../../../Libraries/EventEmitter/NativeEventEmitter')
// findNodeHandle is imported from RendererProxy so mock that whole module.
.setMock('../../../../Libraries/ReactNative/RendererProxy', {
findNodeHandle: () => 1,
});

NativeAnimatedModule =
// $FlowFixMe[incompatible-type]
require('../../specs_DEPRECATED/modules/NativeAnimatedModule').default;
// $FlowFixMe[cannot-write]
// $FlowFixMe[incompatible-use]
// $FlowFixMe[unsafe-object-assign]
Object.assign(NativeAnimatedModule, {
getValue: jest.fn(),
addAnimatedEventToView: jest.fn(),
connectAnimatedNodes: jest.fn(),
connectAnimatedNodeToView: jest.fn(),
createAnimatedNode: jest.fn(),
disconnectAnimatedNodeFromView: jest.fn(),
disconnectAnimatedNodes: jest.fn(),
dropAnimatedNode: jest.fn(),
extractAnimatedNodeOffset: jest.fn(),
flattenAnimatedNodeOffset: jest.fn(),
removeAnimatedEventFromView: jest.fn(),
restoreDefaultValues: jest.fn(),
setAnimatedNodeOffset: jest.fn(),
setAnimatedNodeValue: jest.fn(),
startAnimatingNode: jest.fn(),
startListeningToAnimatedNodeValue: jest.fn(),
stopAnimation: jest.fn(),
stopListeningToAnimatedNodeValue: jest.fn(),
});
});

it('runs with cxxNativeAnimatedEnabled forced on', () => {
const {ReactNativeFeatureFlags} = importModules();
expect(ReactNativeFeatureFlags.cxxNativeAnimatedEnabled()).toBe(true);
});

it('batches a synchronous Animated operation and flushes it on a microtask', async () => {
const {Animated} = importModules();

const opacity = new Animated.Value(0);
opacity.__makeNative();
await create(<Animated.View style={{opacity}} />);

// With the C++ backend a synchronous Animated call is batched rather than
// dispatched inline...
opacity.setValue(0.5);
expect(NativeAnimatedModule.setAnimatedNodeValue).not.toHaveBeenCalled();

// ...and reaches the native module once the microtask drains, with the same
// arguments the inline (platform) backend would have sent.
await flushMicrotasks();
expect(NativeAnimatedModule.setAnimatedNodeValue).toHaveBeenCalledWith(
expect.any(Number),
0.5,
);
});

it('batches a native-driven animation start and flushes it on a microtask', async () => {
const {Animated} = importModules();

const opacity = new Animated.Value(0);
// Mount first so the style/props nodes are already created and flushed; this
// isolates the `startAnimatingNode` operation produced by `start()` below
// (otherwise `create()` would drain the flush before we can observe it).
await create(<Animated.View style={{opacity}} />);

// Starting a native-driven animation batches `startAnimatingNode` rather
// than dispatching it inline...
Animated.timing(opacity, {
toValue: 10,
duration: 1000,
useNativeDriver: true,
}).start();
expect(NativeAnimatedModule.startAnimatingNode).not.toHaveBeenCalled();

// ...and it reaches the native module once the microtask drains.
await flushMicrotasks();
expect(NativeAnimatedModule.startAnimatingNode).toHaveBeenCalledWith(
expect.any(Number),
expect.any(Number),
expect.objectContaining({type: 'frames'}),
expect.any(Function),
);
});
});
Loading