Back out "Animated: Optimize onUserDrivenAnimationEnded Deopt" (#48733)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48733

Original commit changeset: e7d7e486bbfd

Original Phabricator Diff: D67872307

changelog: [internal]

Reviewed By: yungsters

Differential Revision: D68268701

fbshipit-source-id: 1eb0dcb257c2d568cbc02ab2aa4a16b161797b24
This commit is contained in:
Samuel Susla
2025-01-16 10:48:23 -08:00
committed by Facebook GitHub Bot
parent 67fc85615a
commit 6fa4cc6085
5 changed files with 99 additions and 44 deletions
@@ -68,15 +68,22 @@ describe('AnimatedValue', () => {
expect(callback).toBeCalledTimes(1);
});
it('creates a native node on attach', () => {
it('creates a native node when adding a listener', () => {
const node = createNativeAnimatedValue();
node.__attach();
expect(NativeAnimatedHelper.API.createAnimatedNode).not.toBeCalled();
const id = node.addListener(jest.fn());
node.removeListener(id);
expect(NativeAnimatedHelper.API.createAnimatedNode).toBeCalledTimes(1);
});
it('drops a created native node on detach', () => {
const node = createNativeAnimatedValue();
node.__attach();
expect(NativeAnimatedHelper.API.createAnimatedNode).toBeCalledTimes(0);
node.addListener(jest.fn());
expect(NativeAnimatedHelper.API.createAnimatedNode).toBeCalledTimes(1);
expect(NativeAnimatedHelper.API.dropAnimatedNode).toBeCalledTimes(0);
@@ -9,6 +9,7 @@
*/
import type {EventSubscription} from '../../vendor/emitter/EventEmitter';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type Animation, {EndCallback} from '../animations/Animation';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
@@ -84,6 +85,7 @@ function _executeAsAnimatedBatch(id: string, operation: () => void) {
* See https://reactnative.dev/docs/animatedvalue
*/
export default class AnimatedValue extends AnimatedWithChildren {
#listenerCount: number = 0;
#updateSubscription: ?EventSubscription = null;
_value: number;
@@ -105,20 +107,8 @@ export default class AnimatedValue extends AnimatedWithChildren {
}
}
__attach(): void {
__detach() {
if (this.__isNative) {
// NOTE: In theory, we should only need to call this when any listeners
// are added. However, there is a global `onUserDrivenAnimationEnded`
// listener that relies on `onAnimatedValueUpdate` having fired to update
// the values in JavaScript. If that listener is removed, this could be
// re-optimized.
this.#ensureUpdateSubscriptionExists();
}
}
__detach(): void {
if (this.__isNative) {
this.#updateSubscription?.remove();
NativeAnimatedAPI.getValue(this.__getNativeTag(), value => {
this._value = value - this._offset;
});
@@ -131,6 +121,38 @@ export default class AnimatedValue extends AnimatedWithChildren {
return this._value + this._offset;
}
__makeNative(platformConfig: ?PlatformConfig): void {
super.__makeNative(platformConfig);
if (this.#listenerCount > 0) {
this.#ensureUpdateSubscriptionExists();
}
}
addListener(callback: (value: any) => mixed): string {
const id = super.addListener(callback);
this.#listenerCount++;
if (this.__isNative) {
this.#ensureUpdateSubscriptionExists();
}
return id;
}
removeListener(id: string): void {
super.removeListener(id);
this.#listenerCount--;
if (this.__isNative && this.#listenerCount === 0) {
this.#updateSubscription?.remove();
}
}
removeAllListeners(): void {
super.removeAllListeners();
this.#listenerCount = 0;
if (this.__isNative) {
this.#updateSubscription?.remove();
}
}
#ensureUpdateSubscriptionExists(): void {
if (this.#updateSubscription != null) {
return;
@@ -17,7 +17,9 @@ import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNa
import {isPublicInstance as isFabricPublicInstance} from '../ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstanceUtils';
import useRefEffect from '../Utilities/useRefEffect';
import {AnimatedEvent} from './AnimatedEvent';
import AnimatedNode from './nodes/AnimatedNode';
import AnimatedProps from './nodes/AnimatedProps';
import AnimatedValue from './nodes/AnimatedValue';
import {
useCallback,
useEffect,
@@ -37,6 +39,11 @@ type CallbackRef<T> = T => mixed;
type UpdateCallback = () => void;
type AnimatedValueListeners = Array<{
propValue: AnimatedValue,
listenerId: string,
}>;
const useMemoOrAnimatedPropsMemo =
ReactNativeFeatureFlags.enableAnimatedPropsMemo()
? useAnimatedPropsMemo
@@ -162,6 +169,7 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
const target = getEventTarget(instance);
const events = [];
const animatedValueListeners: AnimatedValueListeners = [];
for (const propName in props) {
// $FlowFixMe[invalid-computed-prop]
@@ -169,6 +177,8 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
if (propValue instanceof AnimatedEvent && propValue.__isNative) {
propValue.__attach(target, propName);
events.push([propName, propValue]);
// $FlowFixMe[incompatible-call] - the `addListenersToPropsValue` drills down the propValue.
addListenersToPropsValue(propValue, animatedValueListeners);
}
}
@@ -178,6 +188,10 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
for (const [propName, propValue] of events) {
propValue.__detach(target, propName);
}
for (const {propValue, listenerId} of animatedValueListeners) {
propValue.removeListener(listenerId);
}
};
},
[node, useNativePropsInFabric, props],
@@ -201,6 +215,35 @@ function reduceAnimatedProps<TProps>(
};
}
function addListenersToPropsValue(
propValue: AnimatedValue,
accumulator: AnimatedValueListeners,
) {
// propValue can be a scalar value, an array or an object.
if (propValue instanceof AnimatedValue) {
const listenerId = propValue.addListener(() => {});
accumulator.push({propValue, listenerId});
} else if (Array.isArray(propValue)) {
// An array can be an array of scalar values, arrays of arrays, or arrays of objects
for (const prop of propValue) {
addListenersToPropsValue(prop, accumulator);
}
} else if (propValue instanceof Object) {
addAnimatedValuesListenersToProps(propValue, accumulator);
}
}
function addAnimatedValuesListenersToProps(
props: AnimatedNode,
accumulator: AnimatedValueListeners,
) {
for (const propName in props) {
// $FlowFixMe[prop-missing] - This is an object contained in a prop, but we don't know the exact type.
const propValue = props[propName];
addListenersToPropsValue(propValue, accumulator);
}
}
/**
* Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach`
* and `__detach`. However, this is more complicated because `AnimatedProps`
@@ -1106,9 +1106,12 @@ declare export default class AnimatedValue extends AnimatedWithChildren {
_animation: ?Animation;
_tracking: ?AnimatedTracking;
constructor(value: number, config?: ?AnimatedValueConfig): void;
__attach(): void;
__detach(): void;
__getValue(): number;
__makeNative(platformConfig: ?PlatformConfig): void;
addListener(callback: (value: any) => mixed): string;
removeListener(id: string): void;
removeAllListeners(): void;
setValue(value: number): void;
setOffset(offset: number): void;
flattenOffset(): void;
@@ -208,21 +208,15 @@ describe('Native Animated', () => {
const value1 = new Animated.Value(0);
value1.__makeNative();
const nativeTag = value1.__getNativeTag();
value1.__attach();
const listener = jest.fn();
const id = value1.addListener(listener);
expect(
NativeAnimatedModule.startListeningToAnimatedNodeValue,
).toHaveBeenCalledTimes(1);
expect(
NativeAnimatedModule.startListeningToAnimatedNodeValue,
).toHaveBeenCalledWith(nativeTag);
).toHaveBeenCalledWith(value1.__getNativeTag());
NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', {
value: 42,
tag: nativeTag,
tag: value1.__getNativeTag(),
});
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toBeCalledWith({value: 42});
@@ -230,24 +224,20 @@ describe('Native Animated', () => {
NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', {
value: 7,
tag: nativeTag,
tag: value1.__getNativeTag(),
});
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toBeCalledWith({value: 7});
expect(value1.__getValue()).toBe(7);
value1.removeListener(id);
value1.__detach();
expect(
NativeAnimatedModule.stopListeningToAnimatedNodeValue,
).toHaveBeenCalledTimes(1);
expect(
NativeAnimatedModule.stopListeningToAnimatedNodeValue,
).toHaveBeenCalledWith(nativeTag);
).toHaveBeenCalledWith(value1.__getNativeTag());
NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', {
value: 1492,
tag: nativeTag,
tag: value1.__getNativeTag(),
});
expect(listener).toHaveBeenCalledTimes(2);
expect(value1.__getValue()).toBe(7);
@@ -258,37 +248,27 @@ describe('Native Animated', () => {
const value1 = new Animated.Value(0);
value1.__makeNative();
const nativeTag = value1.__getNativeTag();
value1.__attach();
const listener = jest.fn();
[1, 2, 3, 4].forEach(() => value1.addListener(listener));
expect(
NativeAnimatedModule.startListeningToAnimatedNodeValue,
).toHaveBeenCalledTimes(1);
expect(
NativeAnimatedModule.startListeningToAnimatedNodeValue,
).toHaveBeenCalledWith(nativeTag);
).toHaveBeenCalledWith(value1.__getNativeTag());
NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', {
value: 42,
tag: nativeTag,
tag: value1.__getNativeTag(),
});
expect(listener).toHaveBeenCalledTimes(4);
expect(listener).toBeCalledWith({value: 42});
value1.removeAllListeners();
value1.__detach();
expect(
NativeAnimatedModule.stopListeningToAnimatedNodeValue,
).toHaveBeenCalledTimes(1);
expect(
NativeAnimatedModule.stopListeningToAnimatedNodeValue,
).toHaveBeenCalledWith(nativeTag);
).toHaveBeenCalledWith(value1.__getNativeTag());
NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', {
value: 7,
tag: nativeTag,
tag: value1.__getNativeTag(),
});
expect(listener).toHaveBeenCalledTimes(4);
});