feat: Lottie animation renders at composition frame rate, reducing unnecessary load - #420
feat: Lottie animation renders at composition frame rate, reducing unnecessary load#420OuHT wants to merge 2 commits into
Conversation
Signed-off-by: weixin_46147069 <501436674@qq.com>
|
@OuHT Did you try the |
|
@xvrh Yes, I have tried the frameRate property. However, compared to our current driver solution, the power savings were not significant. |
|
Can you explain me what the trick is then? The existing frameRate tries to not schedule a repaint when it's not necessary. Is it the build itself that is eating the CPU? |
|
The existing frameRate tries to not schedule a repaint when it's not necessary. It only skips the repaint, but doesn't skip the entire VSync wake-up mechanism that causes the repaint. It's not the build itself, but being forced to run build 60 times/second that eats the CPU (assuming a 60Hz screen): frameRate=30: VSync wakes up every 16.7ms → build runs 60/s, paint is just skipped at the end
|
Summary
The problem with existing
|
|
Hi @xvrh , sorry for the follow-up. Regarding the implementation details I explained earlier, I was wondering if the explanation was clear enough, or if there's anything I should clarify? I'd be happy to add more test cases or adjust the implementation approach if needed. Looking forward to your feedback. Thank you! |
Signed-off-by: weixin_46147069 <501436674@qq.com>
|
@xvrh I studied your PR, the gate rebuild approach is clever and does reduce unnecessary I tested three approaches with a 30fps composition on a 60Hz screen, measuring CPU cycles:
Your PR successfully reduced Lottie widget's rebuild frequency (from 60/s to ~28/s), but the The gate skips Lottie's Timer drive bypasses No To achieve this, I added External Controller Comparison:
Both controllers support TickerMode and App Lifecycle, just with different drive mechanisms. Perfetto confirms this — your PR keeps AsyncWorker wake-up interval at 16.7ms, while Timer drive changes it to 33.3ms. Would you consider defaulting to Timer drive when no external controller is provided, using Timer drive with |
The #423 gate stopped the Lottie subtree from rebuilding on every vsync, but the auto-animation's ticker still re-armed a frame callback each display frame, so the engine kept running the full pipeline (scheduling, build/paint flush, scene submission, raster) at the display refresh rate. As measured in the #420 discussion, that per-frame overhead dominates: gating rebuilds alone recovered only ~2% CPU. Drive the auto-animation with a throttled Ticker instead: after each tick, re-arming the vsync callback is delayed with a one-shot timer aimed at the last vsync preceding the next composition-frame boundary (tick timestamps are vsync timestamps, so the vsync phase is known). Timers never fire early, so the frame request goes out one vsync ahead and the tick lands on the first vsync at or after the boundary — frames are neither skipped (24fps content on a 60Hz display) nor slipped (60fps content rendering at half rate). Elapsed time still comes from frame timestamps, so a late timer drops frames instead of slowing the animation down. Because the ticker parks on a timer between frames, the whole pipeline now runs at the composition rate. Benchmarked on macOS (profile, 120Hz display, LottieLogo1 30fps in a ListView): 120 -> 30 engine frames/s, 13.8% -> 6.3% process CPU. 24/25/30/60fps compositions all land exactly on their composition rate. Behavior notes: - External AnimationControllers keep the previous vsync-driven path. - TickerMode (including forceFrames) and app lifecycle keep working: muting cancels the pending timer, and in the background the chain parks itself after a single timer fire. - The throttle is inactive under `flutter test` so that tester.pumpAndSettle() keeps observing the animation; debugThrottleAnimationsInTests re-enables it (used by this package's own tests). - FrameRate.max keeps rendering every display frame. Also add FrameRate.resolveFps as the single place that resolves the composition/max sentinels, an interactive test page (example/lib/frame_rate_demo.dart) with live engine-fps metrics, and a headless benchmark entrypoint (example/lib/bench_main.dart). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… ticker) (#426) Follow-up to the discussion in #420 (and #419): as measured there, the rebuild gate from #423 was not enough — the auto-animation's ticker still re-armed a frame callback on every vsync, so the engine kept running the full pipeline (frame scheduling, build/paint flush, scene submission, raster) at the display refresh rate. Gating rebuilds alone recovered only ~2% CPU because that per-frame engine overhead dominates. ## Approach Drive the auto-animation with a **throttled `Ticker`**: after each tick, re-arming the vsync callback is delayed with a one-shot timer aimed at the last vsync preceding the next composition-frame boundary (each tick timestamp *is* a vsync timestamp, so the vsync phase is known). Timers never fire early, so the frame request goes out one vsync ahead and the tick lands on the first vsync at or after the boundary: - no *skipped* frames when the rates don't divide (24/25fps content on a 60Hz display), - no *slipped* frames when they match (60fps content on 60Hz stays at 60, the throttle becomes a no-op), - elapsed time still comes from frame timestamps, so a late timer **drops** frames instead of slowing the animation down. Because the ticker parks on a timer between composition frames, no engine frame is even scheduled in between — the whole pipeline runs at the composition rate, which is where the savings come from. This achieves the gains of the Timer-drive proposal in #420 while keeping `AnimationController` semantics (statuses, wall-clock accuracy, curves), `TickerMode` (including `forceFrames`), and app-lifecycle behavior (in the background the timer chain parks itself after a single fire), with no new public controller API. ## Measured results (macOS profile build, 120Hz display, composition in a ListView) | | Engine frames/s | Process CPU | |---|---|---| | master | 120 | 13.8% | | this PR | 30 (LottieLogo1, 30fps) | 6.3% | Composition-rate accuracy across assets: 24fps → 24 frames/s, 25fps → ~25, 30fps → ~30, 60fps → ~60. ## Behavior notes - **External `AnimationController`s are unaffected** — they keep the vsync-driven path (plus the #423 gate). - **`flutter test` is unaffected**: the throttle is inactive under the test runner so `tester.pumpAndSettle()` keeps observing the animation exactly as before. `debugThrottleAnimationsInTests` re-enables it (used by this package's own throttle tests). - `FrameRate.max` keeps rendering every display frame; `frameRate: FrameRate(x)` throttles to x. - The vsync period is resolved from the widget's own `View` (multi-view safe, 60Hz fallback). ## Extras - `FrameRate.resolveFps` — single resolver for the `composition`/`max` sentinels (previously duplicated in `roundProgress` and the widget). - `example/lib/frame_rate_demo.dart` — interactive test page with live engine-fps/build/raster metrics, asset & frame-rate pickers, TickerMode/lifecycle/external-controller toggles, concurrent staggered animations, and a side-by-side pacing comparison (`flutter run -t lib/frame_rate_demo.dart --profile`). - `example/lib/bench_main.dart` — headless benchmark entrypoint (`--dart-define=ASSET=...`). ## Testing 9 dedicated widget tests in `test/frame_rate_throttle_test.dart` (throttled rebuild rate, no engine frame scheduled between composition frames, no-op at/above display rate, wall-clock correctness, no skipped frames at 25fps-on-60Hz, TickerMode pause/resume, zero frames when mounted under a disabled TickerMode, pumpAndSettle compatibility). Full suite: 1025 tests pass, goldens unchanged.


From issue #419
Lottie animation renders at composition frame rate, reducing unnecessary load
Flutter Ticker forces Lottie animation to synchronize with 60Hz Vsync. Even when the composition's original frame rate (
frfield) is only 15/30fps, the system still executes the full rendering pipeline at 60Hz, causing unnecessary CPU/GPU load.This solution adopts Timer drive by default, rendering at the composition's original frame rate or configured
frameRate, decoupling animation frequency from screen refresh rate to reduce power consumption.Key Changes
LottieController, render via Timer at configured frame rateAnimationController, automatically uses original Ticker driveLottieControllerAnimationController, noTickerProviderStateMixinrequiredframeRatetakes effectFrameRate(30)orFrameRate.compositionrenders at specified frame rateUsage
Measured Benefits (Android)
Test Demo:
Rendering at fr-specified frame rate from JSON:
Average 38.4% reduction in rendering overhead.
Backward Compatibility
Existing
AnimationControllercode works without modification, automatically uses Ticker drive.