You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## 📜 Description
Optimized `TimingAnimation.timingAt` method by `10x`.
## 💡 Motivation and Context
### 1️⃣ Main optimization (x10 boost)
The key optimization here is that instead of `bisection` ->
(`findTForX`/`bezierY`) pipeline we simply use `findTForY`/`bezierX`.
Thus we get rid off outer loop and as a result performance got improved
by 10x: from 2.57s to 0.26s!
> Technically performance was improved from _log(n) ^ log(n)_ to
_log(n)_, but concrete benchmarks shows `x10` boost 🔥
Other performance optimization was made for `valueAt` - I removed
unneeded math operations + casting and it gave a little bit boost (like
5%).
### 2️⃣ Don't loose 30% because of closures
To keep follow DRY principles I created common `findT` function.
Initially I wanted to pass a closure (for `findTForX`/`findTForY`), but
turned out it makes perf worse by ~30%:
```swift
private func findTForX(xTarget: CGFloat, epsilon: CGFloat = 0.0001) -> CGFloat {
findT(
target: xTarget,
valueFunction: { [weak self] t in self?.bezierX(t: t) ?? 0 },
derivativeFunction: { [weak self] t in self?.bezierXDerivative(t: t) ?? 0 },
epsilon: epsilon
)
}
```
To overcome performance degradation I decided to switch to `enum`-based
approach. Good comparison of speed:
<img width="606" alt="image"
src="https://github.com/user-attachments/assets/149d21e7-03fd-4c66-a6d6-a64519e70655"
/>
Explanation where we loose 30%:
<img width="772" alt="image"
src="https://github.com/user-attachments/assets/3f3669eb-eb9e-4443-8486-2d9f63bdb02e"
/>
And low level stuff:
<img width="690" alt="image"
src="https://github.com/user-attachments/assets/097dd481-c246-4264-b583-74286b6057d6"
/>
### 3️⃣ Don't loose 10%
I also noticed that code `max(0, min(t, 1))` - it's a simple clamping.
And I created an `extension` for that, but immediately noticed `10%`
worse performance 🤷♂️ I think it happens because of frequent implicit
casting - so for now i decided to have such statement in two places of
the code for the sake of performance.
### 4️⃣ Another potential 5% performance improvements
We also can combine `derivative` and `value` computation from common
values, i. e.:
```swift
private func calculateComponents(t: CGFloat, component: BezierComponent) -> (value: CGFloat, derivative: CGFloat) {
let u = 1 - t
let tt = t * t
let uu = u * u
let tu = t * u
switch component {
case .x:
let value = 3 * uu * t * p1.x + 3 * u * tt * p2.x + tt * t
let derivative = (3 * uu - 6 * tu) * p1.x + (6 * tu - 3 * tt) * p2.x + 3 * tt
return (value, derivative)
case .y:
let value = 3 * uu * t * p1.y + 3 * u * tt * p2.y + tt * t
let derivative = (3 * uu - 6 * tu) * p1.y + (6 * tu - 3 * tt) * p2.y + 3 * tt
return (value, derivative)
}
}
```
But it gives up to 1-5% of boost, and in my opinion we loose code
attractiveness with such approach, so for now i'll stick with closures
and shared methods (later on I always can revisit the approach).
### 5️⃣ What should we do with default bisect method?
This is the question I don't have the answer at the moment. Just
technically it is used only in `SpringAnimation`, but can be potentially
handy to use it in other animations (`DecayAnimation` if it will be ever
implemented, for example). So for now I think we can use it as a main
method for finding a `t` by `x`, but if we can use more optimized
version (as in `TimingAnimation`), then we can always override it.
The reason why I don't want to use Newton-Rhapson method in
`SpringAnimation` is because it's slower, than plain bisect search.
So, to sum-it-up: bisect for `SpringAnimation` (and by default for all
animations), Newton-Rhapson for `TimingAnimation` (since `valueAt` is
based on it, so better not to increase complexity of the method with
adding `bisect` on top of it and simply re-use Newton-Rhapson just for
different coordinate value).
> [!NOTE]
> Last, but not least, if we increase precision by x10 (i. e. from
`0.0001` to `0.00001` then computation will consume only 5% resources
more vs 10% with previous approach 😎
## 📢 Changelog
<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->
### iOS
- added unit tests for `timingAt` method for `TimingAnimation` class;
- updated performance metrics for `TimingAnimation` tests;
- override `timingAt` method and use Newton Raphson method to find a
given value;
- create common `findT` method;
- create `findTForX`/`findTForY` methods (via `enum`);
- create `calculateComponents` method (using `enum` + `switch`).
## 🤔 How Has This Been Tested?
Tested via performance tests.
## 📸 Screenshots (if appropriate):
|Real device|Simulator (slow animations)|
|-----------|----------------------------|
|<video
src="https://github.com/user-attachments/assets/c4bca15b-30a6-4e32-9bd1-71c00c4dc285">|<video
src="https://github.com/user-attachments/assets/f8db2df0-e35f-460b-8b77-321b3b8faefa">|
## 📝 Checklist
- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
Copy file name to clipboardExpand all lines: ios/KeyboardControllerNative/KeyboardControllerNative.xcodeproj/xcshareddata/xcbaselines/0873ED612BB6B7390004F3A4.xcbaseline/AE1417BE-2A84-4C63-BD95-0B7DE93E2975.plist
0 commit comments