Skip to content

Commit

Permalink
improve interpolation performance with big input range (#33598)
Browse files Browse the repository at this point in the history
Summary:
This drastically improves `Animated.interpolate` performance when `inputRange` has a considerable amount of elements (~100 in my tests).

For instance in `ActivityIndicator` inside `react-native-paper`, the input has 144 elements https://github.com/callstack/react-native-paper/blob/main/src/components/ActivityIndicator.tsx#L170. `react-native-elements` has 9k stars, so I'm assuming this is widely used.

### Cause

The reason for the performance drop is that if we assume `n` to be the size of the range, calculating `'inputRange must be monotonically non-decreasing ' + arr` essentially calculates `arr.toString()` which has O(n) complexity.
Since it is recalculated in a for loop, we end up with `checkValidInputRange` having a O(n²) complexity. Which means ~10k operations if the array has a size close to 100.

## Changelog

[General] [Fixed] - Fix performance issue on Animated.interpolate with big input range

Pull Request resolved: #33598

Test Plan:
[Here's a repo](https://github.com/Almouro/AnimatedInterpolationRepro) reproducing the issue.
The branch `fix` includes the fix.
Clicking `Interpolate` runs:

```js
new Animated.Value(0).interpolate({
  inputRange: Array(144)
    .fill()
    .map((_, i) => 1 / (i + 1))
    .reverse(),
  outputRange: Array(144)
    .fill()
    .map((_, i) => 1 / (i + 1))
```

Here's a comparison of JS thread perf before the fix and after the fix:
- on a Samsung J3 2017 (lower end)
- using Flipper and https://github.com/bamlab/react-native-performance)
- ` __DEV__` mode deactivated
- clicking the button and waiting 15s

| Before   |      After      |
|----------|:-------------:|
| ![image](https://user-images.githubusercontent.com/4534323/162413692-307c2be1-5c7f-4e7f-ba69-8ba8d7c52bda.png) |  ![image](https://user-images.githubusercontent.com/4534323/162413842-780f12d2-ce8b-457c-b66c-c6d86f14ed28.png)|

The error still throws if `inputRange` is incorrect:

<img width="517" alt="image" src="https://user-images.githubusercontent.com/4534323/162439219-6ce120ae-98e5-496b-899a-492978689d6d.png">

However if `__DEV__` mode is deactivated, no error is thrown

Reviewed By: yungsters

Differential Revision: D35507441

Pulled By: javache

fbshipit-source-id: 36ac49422f7a42d247130c42d12248b2be1232c6
  • Loading branch information
Almouro authored and facebook-github-bot committed Apr 11, 2022
1 parent e147365 commit f503b21
Showing 1 changed file with 17 additions and 20 deletions.
37 changes: 17 additions & 20 deletions Libraries/Animated/nodes/AnimatedInterpolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,23 @@ function createInterpolation(
}

const outputRange: Array<number> = (config.outputRange: any);
checkInfiniteRange('outputRange', outputRange);

const inputRange = config.inputRange;
checkInfiniteRange('inputRange', inputRange);
checkValidInputRange(inputRange);

invariant(
inputRange.length === outputRange.length,
'inputRange (' +
inputRange.length +
') and outputRange (' +
outputRange.length +
') must have the same length',
);
if (__DEV__) {
checkInfiniteRange('outputRange', outputRange);
checkInfiniteRange('inputRange', inputRange);
checkValidInputRange(inputRange);

invariant(
inputRange.length === outputRange.length,
'inputRange (' +
inputRange.length +
') and outputRange (' +
outputRange.length +
') must have the same length',
);
}

const easing = config.easing || linear;

Expand Down Expand Up @@ -276,16 +279,10 @@ function findRange(input: number, inputRange: $ReadOnlyArray<number>) {

function checkValidInputRange(arr: $ReadOnlyArray<number>) {
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
const message =
'inputRange must be monotonically non-decreasing ' + String(arr);
for (let i = 1; i < arr.length; ++i) {
invariant(
arr[i] >= arr[i - 1],
/* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression
* below this comment, one or both of the operands may be something that
* doesn't cleanly convert to a string, like undefined, null, and object,
* etc. If you really mean this implicit string conversion, you can do
* something like String(myThing) */
'inputRange must be monotonically non-decreasing ' + arr,
);
invariant(arr[i] >= arr[i - 1], message);
}
}

Expand Down

0 comments on commit f503b21

Please sign in to comment.