Commit 3fa648b
authored
perf: schedule engine frames at the composition frame rate (throttled 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.1 parent 91ce46f commit 3fa648b
16 files changed
Lines changed: 934 additions & 40 deletions
File tree
- example
- android
- app
- gradle/wrapper
- lib
- lib/src
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
1 | 4 | | |
2 | 5 | | |
3 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
2 | | - | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | 1 | | |
15 | 2 | | |
16 | 3 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
9 | | - | |
| 8 | + | |
10 | 9 | | |
11 | | - | |
| 10 | + | |
12 | 11 | | |
13 | | - | |
14 | | - | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
18 | | - | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
19 | 24 | | |
20 | | - | |
| 25 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
0 commit comments