From f503b212039f79f00ea56b65ecf3cd150b82f087 Mon Sep 17 00:00:00 2001 From: almouro Date: Mon, 11 Apr 2022 10:37:33 -0700 Subject: [PATCH] improve interpolation performance with big input range (#33598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: https://github.com/facebook/react-native/pull/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: image However if `__DEV__` mode is deactivated, no error is thrown Reviewed By: yungsters Differential Revision: D35507441 Pulled By: javache fbshipit-source-id: 36ac49422f7a42d247130c42d12248b2be1232c6 --- .../Animated/nodes/AnimatedInterpolation.js | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/Libraries/Animated/nodes/AnimatedInterpolation.js b/Libraries/Animated/nodes/AnimatedInterpolation.js index d6f5deed041f5a..dac38a30a18662 100644 --- a/Libraries/Animated/nodes/AnimatedInterpolation.js +++ b/Libraries/Animated/nodes/AnimatedInterpolation.js @@ -46,20 +46,23 @@ function createInterpolation( } const outputRange: Array = (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; @@ -276,16 +279,10 @@ function findRange(input: number, inputRange: $ReadOnlyArray) { function checkValidInputRange(arr: $ReadOnlyArray) { 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); } }