From 6c0f8b636bfbddc3440a7f2180d42e057124b4b1 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 4 Nov 2026 18:57:47 +0000 Subject: [PATCH 1/2] RFC 410.0000: Android Dynamic Performance Framework (ADPF) Integration for Flutter --- rfc/410.0000-android-adpf-integration.md | 86 ++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 rfc/410.0000-android-adpf-integration.md diff --git a/rfc/410.0000-android-adpf-integration.md b/rfc/410.0000-android-adpf-integration.md new file mode 100644 index 0000000..c2e2971 --- /dev/null +++ b/rfc/410.0000-android-adpf-integration.md @@ -0,0 +1,86 @@ +--- +type: rfc +rfc: '410.0000' +title: Android Dynamic Performance Framework (ADPF) Integration for Flutter +description: Integrates Android's APerformanceHintManager (ADPF) NDK API into the Flutter Android engine to report per-frame UI and raster thread work durations and post-idle workload resets to the OS scheduler. +status: draft +created: 2026-11-04T00:00:00Z +updated: 2026-11-04T00:00:00Z +tags: + - 410-android + - 200-engine +authors: + - https://github.com/gmackall +--- + +# RFC 410.0000: Android Dynamic Performance Framework (ADPF) Integration for Flutter + +**Tracking issue:** [flutter/flutter#195992](https://github.com/flutter/flutter/issues/195992) ([flutter/flutter#114835](https://github.com/flutter/flutter/issues/114835)) • **Prototype:** [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/4c15b121486e83f0faecee2edb4e86bd41055e19...gmackall:flutter:adpf-pure-dual) + +--- + +## Background + +By default on Android, CPU frequency scaling only knows how busy a thread has been recently. It does not know when our frame deadline is or that Flutter splits rendering across a UI thread and a raster thread. When an app wakes up from idle (e.g. after a user reads a screen or a network call finishes), hits a heavy frame during a scroll, or continues scrolling after the user lifts their finger (once the OS touch boost ends), CPU clocks often ramp up too late and we miss frames. + +The [Android Dynamic Performance Framework (ADPF)](https://developer.android.com/games/optimize/adpf) [`APerformanceHintManager`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehintmanager) NDK API lets an app register performance-critical thread IDs with a target frame duration and report how long work actually took each frame (using [`AWorkDuration`](https://developer.android.com/ndk/reference/group/a-performance-hint#aworkduration) on Android 15 / API 35+) so the OS can scale CPU clocks to meet the frame budget. Native Android views already use this under the hood, but Flutter currently does not. + +## Proposed Change + +Land [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/4c15b121486e83f0faecee2edb4e86bd41055e19...gmackall:flutter:adpf-pure-dual), which: + +1. **Dynamically loads the [`APerformanceHintManager`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehintmanager) symbols** from `libandroid.so` via `dlsym` (since Flutter's `minSdkVersion` is 24) and gates session creation on Android 15+ (API 35+). +2. **Creates two [`APerformanceHintSession`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehintsession) instances** (via [`APerformanceHint_createSession`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_createsession)) in `AndroidShellHolder`: one for the UI thread (`ui_performance_hint_manager_`, registered with the merged platform/UI thread's `gettid()` by default, or the dedicated UI thread from `ThreadHost` if unmerged) and one for the raster thread (`raster_performance_hint_manager_`, capturing the raster thread's `gettid()` via `ThreadHostConfig`'s `thread_config_setter` callback so `fml::Thread` does not need to be modified). Both managers are shared across `AndroidShellHolder::Spawn` (`FlutterEngineGroup`) and target the display frame period (e.g. 8.33 ms at 120 Hz, updated on display changes via [`APerformanceHint_updateTargetWorkDuration`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_updatetargetworkduration) when the target changes by more than 5%). +3. **Adds an internal `Settings::frame_built_callback`** (fired at the end of [`Animator::EndFrame`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/shell/common/animator.cc#L141-L154); see [this branch](https://github.com/flutter/flutter/compare/4c15b121486e83f0faecee2edb4e86bd41055e19...gmackall:flutter:adpf-pr1-frame-built-callback) for the exact proposed change) so the UI session reports [`[kBuildStart, kBuildFinish]`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/common/settings.h#L29-L30) as soon as the UI thread finishes building rather than waiting for rasterization, while `AndroidShellHolder`'s existing [`frame_rasterized_callback`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/shell/common/rasterizer.cc#L566) reports [`[kRasterStart, kRasterFinish]`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/common/settings.h#L31-L32) to the raster session. Both callbacks report via [`APerformanceHint_reportActualWorkDuration2`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_reportactualworkduration2), passing the phase start timestamp and wall-clock duration for both total and CPU time (per Android's [`WorkDuration`](https://developer.android.com/reference/android/os/WorkDuration) spec). +4. Forwards both hint managers through `PlatformViewAndroid::CreateVSyncWaiter` into `VsyncWaiterAndroid` so [`VsyncWaiterAndroid::AwaitVSync`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/shell/platform/android/vsync_waiter_android.cc#L29-L61) can call **[`APerformanceHint_notifyWorkloadReset`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_notifyworkloadreset) on Android 16+ (API 36+) when a frame is requested more than 100 ms after the session's last notification** (capped at 3 resets between actual work reports), telling the PowerHAL that the workload history from before the idle period is stale (mirroring Android HWUI's [`HintSessionWrapper::sendLoadResetHint`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/renderthread/HintSessionWrapper.cpp)). +5. **Closes both [`APerformanceHintSession`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehintsession) instances** (via [`APerformanceHint_closeSession`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_closesession)) and frees their [`AWorkDuration`](https://developer.android.com/ndk/reference/group/a-performance-hint#aworkduration) structs (via [`AWorkDuration_release`](https://developer.android.com/ndk/reference/group/a-performance-hint#aworkduration_release)) in [`AndroidShellHolder::~AndroidShellHolder`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/shell/platform/android/android_shell_holder.cc#L209-L212) before `thread_host_.reset()` joins and destroys `ThreadHost`'s OS threads, ensuring the sessions are closed while their registered thread IDs are still alive. + +### Note on merged threads for HC + +When `RasterThreadMerger` merges the rasterizer onto the platform/UI thread during legacy Hybrid Composition, both callbacks **skip ADPF reporting** (`frame_built_callback` checks `TaskRunnerChecker::RunsOnTheSameThread` and `frame_rasterized_callback` checks `gettid() != captured_raster_tid`). I tried reporting the combined `[kBuildStart, kBuildFinish]` + `[kRasterStart, kRasterFinish]` duration to the UI session, but because [`external_view_embedder_->EndFrame`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/shell/common/rasterizer.cc#L303-L304) (`FlutterViewEndFrame`) runs after `kRasterFinish`, that under-reported the merged thread's work and caused a +28% build regression on Lenovo Tab. We could probably make this work with additional changes, but I'd prefer to avoid over-engineering a fix just for HC mode. + +### Interaction with the Android Embedder API migration + +Today, Flutter already places Android-specific OS scheduler hints in `flutter/fml/` behind a platform-agnostic interface that is a no-op on other OSes (see [`fml/cpu_affinity.h`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/fml/cpu_affinity.h) and [`fml/platform/android/cpu_affinity.cc`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/fml/platform/android/cpu_affinity.cc)). When `AndroidShellHolder` migrates to the C Embedder API ([flutter/rfc#24](https://github.com/flutter/rfc/pull/24)), we can follow that same pattern so **no `embedder.h` API changes are required**: + +- **Primary path (move to `fml/platform/android/`, zero `embedder.h` changes):** Move `AndroidPerformanceHintManager` (which is pure C/NDK via `dlsym("libandroid.so")` with no JNI dependencies) into `flutter/fml/platform/android/` and drive it directly from `Shell`, `Animator`, and base `VsyncWaiter`. `Shell::CreateShellOnPlatformThread` already runs synchronous setup tasks on the UI and raster task runners (where `gettid()` can be captured), `VsyncWaiter` already gates frame requests and receives `(frame_start_time, frame_target_time)` on every frame, and `EmbedderEngine::CollectShell` destroys `Shell` before `CollectThreadHost` joins the threads. This keeps internal build/raster pipeline hooks out of the public `embedder.h` ABI. +- **Alternative (expose callbacks on `embedder.h`):** If other Embedder API consumers later want per-phase frame timing hooks, we could instead keep `AndroidPerformanceHintManager` in `shell/platform/android/` and expose `frame_built_callback` and `frame_rasterized_callback` on `FlutterProjectArgs` in [`embedder.h`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/engine/src/flutter/shell/platform/embedder/embedder.h). + +## Other Options Considered + +1. **Putting both threads in a single session:** Because UI and raster can run in parallel on pipelined frames, a single session forces us to pick one duration to report for both threads. Reporting `build + raster` over-reports when both stages overlap (e.g. 4.5 ms build + 4.5 ms raster easily hits 120 Hz, but reporting 9.0 ms against an 8.33 ms target causes continuous boosting and used +475 mW in testing). Reporting `max(build, raster)` works, but boosts both threads whenever either one is slow, which used ~145 mW more power on Pixel 8 and Pixel 9 during steady scrolling than keeping the two sessions separate. +2. **Reporting GPU time ([`AWorkDuration_setActualGpuDurationNanos`](https://developer.android.com/ndk/reference/group/a-performance-hint#aworkduration_setactualgpudurationnanos)):** I also tested a branch ([gmackall/flutter@adpf-pure-dual-gpu](https://github.com/gmackall/flutter/compare/adpf-pure-dual...adpf-pure-dual-gpu)) that enables Impeller's GPU timestamp queries in profile/release builds and reports GPU duration on API 35+. I am not proposing this for now because (a) enabling per-frame Vulkan timestamp queries regressed `spiky_scroll` median frame time by +49% (10.19 -> 15.14 ms) on Moto G85 5G, and (b) what seems like an Android framework bug in `performance_hint.cpp` currently drops the hint on Pixel whenever `gpu_ns > cpu_ns`. There may be more to explore here in the future, but I also view it as sort of orthogonal to the cpu change. +3. **Reporting both UI and raster durations from `frame_rasterized_callback` (no `frame_built_callback`):** Because `FrameTiming` already contains `kBuildStart` and `kBuildFinish`, I also tested reporting both the UI and raster durations inside `Settings::frame_rasterized_callback` without adding `frame_built_callback`. That still gets most of the improvement on heavy scrolls (e.g. -59% Build P90 on Pixel 8 `tiles_scroll_perf`), but because the UI session is not notified until Frame N finishes rasterizing (~3–6 ms later, after Frame N+1's UI build may already be underway), reporting UI work immediately at the end of `Animator::EndFrame` via `frame_built_callback` improved Build P90 on pipelined scrolls (e.g. from -2% to -18% on Pixel 9 `complex_layout_scroll_perf`, and from -26% to -34% on Pixel 8). However, we could consider this option if people are opposed to adding `frame_built_callback`. + +## Benchmark Results + +I ran Flutter's existing [`dev/benchmarks/`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks) CI suite via `flutter drive --profile` on physical Pixel 8 (API 35), Pixel 9 (API 36), and Lenovo Tab (API 35) devices (9 interleaved rounds across 3 physical devices per model, n=9, with Pixel 8 and Pixel 9 set to 120 Hz) comparing `master` vs. [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/4c15b121486e83f0faecee2edb4e86bd41055e19...gmackall:flutter:adpf-pure-dual). Note that Flutter's Devicelab CI harness normally enables Android's fixed-performance mode ([`cmd power set-fixed-performance-mode-enabled true`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/devicelab/lib/framework/devices.dart#L618-L624)), which locks CPU/GPU clocks to a fixed floor and bypasses dynamic frequency scaling; for these runs I disabled fixed-performance mode so the benchmarks ran under the normal OS CPU governor used on end-user devices: + +- **Scrolling & heavy workloads (bolded rows):** ADPF boosts when frames approach or exceed the target frame budget (8.33 ms at 120 Hz on Pixel 8/9, 16.67 ms at 60 Hz on Lenovo Tab, where light scroll frames stay ~4x under budget so only heavy workloads like `backdrop_filter_perf` trigger boosting). In the scroll benchmarks on Pixel 8 and Pixel 9, the test pauses 500 ms between 300 ms scrolls. On `master` the governor drops clocks during each pause, whereas ADPF ramps clocks back up on the first frame. Because CPU frequency scales per cluster rather than per thread, boosting the overloaded raster session in e.g. `backdrop_filter_perf` also speeds up the UI thread when both run on the same cluster. +- **Sub-budget workloads (unbolded rows — `cull_opacity_perf`, `animation_with_microtasks_perf`):** Both stay well within budget (0 missed frames against Flutter's 16 ms timeline threshold; P90 <= 4.5 ms), with small or neutral deltas on `cull_opacity_perf` (+1% to -9%). `animation_with_microtasks_perf` is an unusual case: it runs a synchronous Dart busy-loop on the UI thread for ~220 ms between frames (~4.5 FPS), with only ~0.5 ms build and ~2.2 ms raster work per frame (>3x under budget). On Android 16+ (Pixel 9), [`APerformanceHint_notifyWorkloadReset`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_notifyworkloadreset) fires after each >100 ms gap and cuts raster time by ~33%. On Android 15 (Pixel 8 and Lenovo Tab), `notifyWorkloadReset` is not available, so reporting a ~2 ms frame followed by 220 ms of silence keeps the raster thread at lower clocks and includes the Binder IPC inside the trace span (2.17 -> 2.49 ms on Pixel 8, 2.26 -> 2.72 ms on Lenovo Tab, still >3x under the frame budget). + +| **Device** | **Benchmark** | **Build Avg (master -> ADPF)** | **Build P90 (master -> ADPF)** | **Raster Avg (master -> ADPF)** | **Raster P90 (master -> ADPF)** | +| --- | --- | --- | --- | --- | --- | +| Pixel 8 (API 35) | [complex\_layout\_scroll\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart) | **2.25 -> 1.46 ms (-35%)** | **4.24 -> 2.80 ms (-34%)** | **4.18 -> 2.65 ms (-36%)** | **6.02 -> 4.06 ms (-33%)** | +| Pixel 8 | [tiles\_scroll\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart) | **3.40 -> 1.58 ms (-53%)** | **7.91 -> 2.92 ms (-63%)** | **4.62 -> 2.91 ms (-37%)** | **6.52 -> 4.29 ms (-34%)** | +| Pixel 8 | [complex\_layout\_scroll\_perf\_bad](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad_test.dart)\* | **8.01 -> 7.62 ms (-5%)** | **2.65 -> 2.13 ms (-20%)** | **5.32 -> 3.62 ms (-32%)** | **7.44 -> 5.49 ms (-26%)** | +| Pixel 8 | [backdrop\_filter\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/backdrop_filter_perf_test.dart) | **2.35 -> 1.74 ms (-26%)** | **4.10 -> 3.00 ms (-27%)** | **43.98 -> 44.56 ms (+1%)** | **52.25 -> 52.39 ms (0%)** | +| Pixel 8 | [cull\_opacity\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/cull_opacity_perf_test.dart) | 3.48 -> 3.20 ms (-8%) | 4.35 -> 4.07 ms (-6%) | 2.14 -> 1.94 ms (-9%) | 2.92 -> 2.64 ms (-9%) | +| Pixel 8 | [animation\_with\_microtasks\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/animation_with_microtasks_perf_test.dart) | 0.49 -> 0.56 ms (+13%) | 0.57 -> 0.60 ms (+5%) | 2.17 -> 2.49 ms (+15%) | 2.67 -> 3.04 ms (+14%) | +| Pixel 8 | [platform\_views\_scroll\_perf\_hc](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/platform_views_layout_hybrid_composition/test_driver/scroll_perf_test.dart) | **0.80 -> 0.58 ms (-28%)** | **1.10 -> 0.76 ms (-31%)** | **5.73 -> 4.65 ms (-19%)** | **8.45 -> 6.12 ms (-28%)** | +| Pixel 9 (API 36) | [complex\_layout\_scroll\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart) | **1.62 -> 1.24 ms (-23%)** | **2.54 -> 2.07 ms (-18%)** | **4.15 -> 2.59 ms (-38%)** | **5.57 -> 3.56 ms (-36%)** | +| Pixel 9 | [tiles\_scroll\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart) | **2.62 -> 1.42 ms (-46%)** | **6.34 -> 2.77 ms (-56%)** | **4.28 -> 2.42 ms (-44%)** | **5.97 -> 3.17 ms (-47%)** | +| Pixel 9 | [complex\_layout\_scroll\_perf\_bad](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad_test.dart)\* | **7.23 -> 7.04 ms (-3%)** | **2.58 -> 1.58 ms (-39%)** | **4.59 -> 3.20 ms (-30%)** | **6.84 -> 4.08 ms (-40%)** | +| Pixel 9 | [backdrop\_filter\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/backdrop_filter_perf_test.dart) | **0.97 -> 0.78 ms (-19%)** | **1.42 -> 1.19 ms (-16%)** | **33.28 -> 31.95 ms (-4%)** | **40.50 -> 38.75 ms (-4%)** | +| Pixel 9 | [cull\_opacity\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/cull_opacity_perf_test.dart) | 3.28 -> 3.03 ms (-8%) | 4.48 -> 4.07 ms (-9%) | 1.84 -> 1.71 ms (-7%) | 2.80 -> 2.56 ms (-9%) | +| Pixel 9 | [animation\_with\_microtasks\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/animation_with_microtasks_perf_test.dart) | 0.61 -> 0.65 ms (+7%) | 0.66 -> 0.72 ms (+10%) | 2.27 -> 1.52 ms (-33%) | 2.76 -> 1.87 ms (-32%) | +| Pixel 9 | [platform\_views\_scroll\_perf\_hc](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/platform_views_layout_hybrid_composition/test_driver/scroll_perf_test.dart) | **0.68 -> 0.68 ms (0%)** | **0.92 -> 0.93 ms (+1%)** | **4.65 -> 4.52 ms (-3%)** | **6.50 -> 6.04 ms (-7%)** | +| Lenovo Tab (API 35) | [complex\_layout\_scroll\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart) | **1.31 -> 1.34 ms (+2%)** | **1.99 -> 1.81 ms (-9%)** | **4.25 -> 4.07 ms (-4%)** | **5.51 -> 5.49 ms (0%)** | +| Lenovo Tab | [tiles\_scroll\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart) | **1.85 -> 1.83 ms (-1%)** | **4.43 -> 4.09 ms (-8%)** | **4.53 -> 4.29 ms (-5%)** | **5.83 -> 5.79 ms (-1%)** | +| Lenovo Tab | [complex\_layout\_scroll\_perf\_bad](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad_test.dart)\* | **11.56 -> 11.48 ms (-1%)** | **3.42 -> 3.83 ms (+12%)** | **5.41 -> 5.51 ms (+2%)** | **7.94 -> 7.67 ms (-3%)** | +| Lenovo Tab | [backdrop\_filter\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/backdrop_filter_perf_test.dart) | **1.47 -> 1.10 ms (-25%)** | **2.68 -> 1.80 ms (-33%)** | **185.42 -> 160.47 ms (-13%)** | **194.04 -> 168.88 ms (-13%)** | +| Lenovo Tab | [cull\_opacity\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/cull_opacity_perf_test.dart) | 2.81 -> 2.83 ms (+1%) | 3.36 -> 3.35 ms (0%) | 2.66 -> 2.63 ms (-1%) | 3.53 -> 3.45 ms (-2%) | +| Lenovo Tab | [animation\_with\_microtasks\_perf](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/macrobenchmarks/test_driver/animation_with_microtasks_perf_test.dart) | 0.53 -> 0.57 ms (+9%) | 0.59 -> 0.67 ms (+13%) | 2.26 -> 2.72 ms (+20%) | 2.42 -> 3.27 ms (+35%) | +| Lenovo Tab | [platform\_views\_scroll\_perf\_hc](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/platform_views_layout_hybrid_composition/test_driver/scroll_perf_test.dart) | **1.75 -> 1.95 ms (+11%)** | **2.89 -> 3.07 ms (+6%)** | **21.66 -> 21.56 ms (0%)** | **26.74 -> 26.95 ms (+1%)** | + +*\* In `complex_layout_scroll_perf_bad`, Build Avg is higher than Build P90 because [`scroll_perf_bad_test.dart`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad_test.dart#L47) passes `retainPriorEvents: true`, which includes Frame 1's ~1,000 ms synchronous layout spike (from nesting a 500-item `shrinkWrap: true` `ListView`) in the arithmetic mean while sitting above the 90th percentile.* From 1e579720cecd2fa050a159a9e42f649ea832666a Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 11 Nov 2026 20:34:58 +0000 Subject: [PATCH 2/2] Fix prototype compare link and dev/benchmarks link in RFC 410.0000 The adpf-pure-dual branch has had master merged in, so comparing it against the pinned base commit showed hundreds of unrelated commits. Compare against master instead. Also link dev/benchmarks as a tree. --- rfc/410.0000-android-adpf-integration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rfc/410.0000-android-adpf-integration.md b/rfc/410.0000-android-adpf-integration.md index c2e2971..a2cd145 100644 --- a/rfc/410.0000-android-adpf-integration.md +++ b/rfc/410.0000-android-adpf-integration.md @@ -15,7 +15,7 @@ authors: # RFC 410.0000: Android Dynamic Performance Framework (ADPF) Integration for Flutter -**Tracking issue:** [flutter/flutter#195992](https://github.com/flutter/flutter/issues/195992) ([flutter/flutter#114835](https://github.com/flutter/flutter/issues/114835)) • **Prototype:** [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/4c15b121486e83f0faecee2edb4e86bd41055e19...gmackall:flutter:adpf-pure-dual) +**Tracking issue:** [flutter/flutter#195992](https://github.com/flutter/flutter/issues/195992) ([flutter/flutter#114835](https://github.com/flutter/flutter/issues/114835)) • **Prototype:** [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/master...gmackall:flutter:adpf-pure-dual) --- @@ -27,7 +27,7 @@ The [Android Dynamic Performance Framework (ADPF)](https://developer.android.com ## Proposed Change -Land [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/4c15b121486e83f0faecee2edb4e86bd41055e19...gmackall:flutter:adpf-pure-dual), which: +Land [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/master...gmackall:flutter:adpf-pure-dual), which: 1. **Dynamically loads the [`APerformanceHintManager`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehintmanager) symbols** from `libandroid.so` via `dlsym` (since Flutter's `minSdkVersion` is 24) and gates session creation on Android 15+ (API 35+). 2. **Creates two [`APerformanceHintSession`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehintsession) instances** (via [`APerformanceHint_createSession`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_createsession)) in `AndroidShellHolder`: one for the UI thread (`ui_performance_hint_manager_`, registered with the merged platform/UI thread's `gettid()` by default, or the dedicated UI thread from `ThreadHost` if unmerged) and one for the raster thread (`raster_performance_hint_manager_`, capturing the raster thread's `gettid()` via `ThreadHostConfig`'s `thread_config_setter` callback so `fml::Thread` does not need to be modified). Both managers are shared across `AndroidShellHolder::Spawn` (`FlutterEngineGroup`) and target the display frame period (e.g. 8.33 ms at 120 Hz, updated on display changes via [`APerformanceHint_updateTargetWorkDuration`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_updatetargetworkduration) when the target changes by more than 5%). @@ -54,7 +54,7 @@ Today, Flutter already places Android-specific OS scheduler hints in `flutter/fm ## Benchmark Results -I ran Flutter's existing [`dev/benchmarks/`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks) CI suite via `flutter drive --profile` on physical Pixel 8 (API 35), Pixel 9 (API 36), and Lenovo Tab (API 35) devices (9 interleaved rounds across 3 physical devices per model, n=9, with Pixel 8 and Pixel 9 set to 120 Hz) comparing `master` vs. [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/4c15b121486e83f0faecee2edb4e86bd41055e19...gmackall:flutter:adpf-pure-dual). Note that Flutter's Devicelab CI harness normally enables Android's fixed-performance mode ([`cmd power set-fixed-performance-mode-enabled true`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/devicelab/lib/framework/devices.dart#L618-L624)), which locks CPU/GPU clocks to a fixed floor and bypasses dynamic frequency scaling; for these runs I disabled fixed-performance mode so the benchmarks ran under the normal OS CPU governor used on end-user devices: +I ran Flutter's existing [`dev/benchmarks/`](https://github.com/flutter/flutter/tree/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/benchmarks) CI suite via `flutter drive --profile` on physical Pixel 8 (API 35), Pixel 9 (API 36), and Lenovo Tab (API 35) devices (9 interleaved rounds across 3 physical devices per model, n=9, with Pixel 8 and Pixel 9 set to 120 Hz) comparing `master` vs. [gmackall/flutter@adpf-pure-dual](https://github.com/flutter/flutter/compare/master...gmackall:flutter:adpf-pure-dual). Note that Flutter's Devicelab CI harness normally enables Android's fixed-performance mode ([`cmd power set-fixed-performance-mode-enabled true`](https://github.com/flutter/flutter/blob/4c15b121486e83f0faecee2edb4e86bd41055e19/dev/devicelab/lib/framework/devices.dart#L618-L624)), which locks CPU/GPU clocks to a fixed floor and bypasses dynamic frequency scaling; for these runs I disabled fixed-performance mode so the benchmarks ran under the normal OS CPU governor used on end-user devices: - **Scrolling & heavy workloads (bolded rows):** ADPF boosts when frames approach or exceed the target frame budget (8.33 ms at 120 Hz on Pixel 8/9, 16.67 ms at 60 Hz on Lenovo Tab, where light scroll frames stay ~4x under budget so only heavy workloads like `backdrop_filter_perf` trigger boosting). In the scroll benchmarks on Pixel 8 and Pixel 9, the test pauses 500 ms between 300 ms scrolls. On `master` the governor drops clocks during each pause, whereas ADPF ramps clocks back up on the first frame. Because CPU frequency scales per cluster rather than per thread, boosting the overloaded raster session in e.g. `backdrop_filter_perf` also speeds up the UI thread when both run on the same cluster. - **Sub-budget workloads (unbolded rows — `cull_opacity_perf`, `animation_with_microtasks_perf`):** Both stay well within budget (0 missed frames against Flutter's 16 ms timeline threshold; P90 <= 4.5 ms), with small or neutral deltas on `cull_opacity_perf` (+1% to -9%). `animation_with_microtasks_perf` is an unusual case: it runs a synchronous Dart busy-loop on the UI thread for ~220 ms between frames (~4.5 FPS), with only ~0.5 ms build and ~2.2 ms raster work per frame (>3x under budget). On Android 16+ (Pixel 9), [`APerformanceHint_notifyWorkloadReset`](https://developer.android.com/ndk/reference/group/a-performance-hint#aperformancehint_notifyworkloadreset) fires after each >100 ms gap and cuts raster time by ~33%. On Android 15 (Pixel 8 and Lenovo Tab), `notifyWorkloadReset` is not available, so reporting a ~2 ms frame followed by 220 ms of silence keeps the raster thread at lower clocks and includes the Binder IPC inside the trace span (2.17 -> 2.49 ms on Pixel 8, 2.26 -> 2.72 ms on Lenovo Tab, still >3x under the frame budget).