perf: schedule engine frames at the composition frame rate (throttled ticker) - #426
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #426 +/- ##
==========================================
+ Coverage 83.96% 84.09% +0.13%
==========================================
Files 163 164 +1
Lines 6672 6741 +69
==========================================
+ Hits 5602 5669 +67
- Misses 1070 1072 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ezamagni-tg
pushed a commit
to ezamagni-tg/lottie-flutter
that referenced
this pull request
Aug 6, 2026
perf: schedule engine frames at the composition frame rate (throttled ticker)
Contributor
|
This performance improvements are very much needed! Hope they can be released in an upcoming version soon |
Owner
Author
|
@ezamagni I'm looking for feedback on this work. If you can validate that it works for you I will merge it. |
Contributor
Owner
Author
|
This is an alternative implementation to achieve the same gain as #420 and any feedback on real world usage is welcome. |
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le 8.7, AGP 8.6, declarative plugins) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: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
AnimationControllersemantics (statuses, wall-clock accuracy, curves),TickerMode(includingforceFrames), 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)
Composition-rate accuracy across assets: 24fps → 24 frames/s, 25fps → ~25, 30fps → ~30, 60fps → ~60.
Behavior notes
AnimationControllers are unaffected — they keep the vsync-driven path (plus the perf: throttle auto-animation rebuilds to composition frame rate #423 gate).flutter testis unaffected: the throttle is inactive under the test runner sotester.pumpAndSettle()keeps observing the animation exactly as before.debugThrottleAnimationsInTestsre-enables it (used by this package's own throttle tests).FrameRate.maxkeeps rendering every display frame;frameRate: FrameRate(x)throttles to x.View(multi-view safe, 60Hz fallback).Extras
FrameRate.resolveFps— single resolver for thecomposition/maxsentinels (previously duplicated inroundProgressand 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.cc @OuHT — this should deliver the same savings your Perfetto traces showed for Timer drive; I'd be very interested in your CPU-cycle numbers for this branch with your Android measurement setup.
🤖 Generated with Claude Code