Flutter Fixes CPU Affinity Bug That Cost Raster 20% on Snapdragon 8 Elite
Recently Flutter merged PR #195071. The bug is quite amusing, because the Android Engine has been using an ancient CPU affinity strategy:
On chips like Snapdragon 8 Elite that have no traditional little cores, Flutter treats the six performance cores as efficiency cores, and ultimately only allows the Raster thread to run on the two super-large cores.
The developer then tested on a real Snapdragon 8 Elite Gen 5 (SM8850) device by modifying thread affinity from the app side. After freeing up the available cores, the Raster frame time median dropped by 16%–21%, and the Raster thread's cumulative CPU execution time decreased by 16%–18%. At first glance this is counterintuitive: the two super-large cores have a higher max frequency, so why does it run faster when placed on ordinary performance cores?
This strategy dates back to 2023's Engine #45673, when Flutter wanted to keep key threads like UI and Raster away from slow cores, so it read each core's max frequency from /sys/devices/system/cpu/cpuN/cpufreq/cpuinfo_max_freq. The group with the highest frequencies is called kPerformance, and the group with the lowest is called kEfficiency.
But that approach introduced another problem:
- Some Android phones have only one core with the highest frequency, e.g., the Pixel 8 in issue #153690, where UI, Raster, and some Dart workers might be squeezed onto the same core.
- So later Engine #54616 relaxed the policy for UI and Raster to
kNotEfficiency, meaning all cores except the lowest-frequency group could be used.
This approach is reasonable on phones with traditional big.LITTLE cores, but the Snapdragon 8 Elite / 8 Elite Gen 5 changed to two Prime super-large cores plus six Oryon performance cores, with no traditional little cores. For example, on the SM8850 used in the PR's testing, CPU 0–5 have a max frequency of 3.63 GHz, and CPU 6–7 have 4.61 GHz. When Flutter compares them, it still identifies the first six as kEfficiency, so kNotEfficiency ends up being only CPU 6–7:
So the real problem is treating "lowest max frequency" as "little core". The PR doesn't need to rewrite the scheduling framework; it only needs to additionally read /sys/devices/system/cpu/cpuN/cpu_capacity, which provides the relative compute capacity of a CPU at max frequency.
Here, the newly added HasNoLittleCores() requires that every core has capacity data and that they are not all identical, then checks:
return min_capacity != max_capacity &&
min_capacity * 2 >= max_capacity;
That is, the weakest core must be at least 50% of the strongest. On the SM8850, the six performance cores have a capacity of 762, the two super-large cores 1024, a ratio of 74.4%, so Flutter concludes that the chip has no obvious little-core group, and changes the original kNotEfficiency to kNotLittle, expanding the available cores for UI and Raster to CPU 0–7.
On traditional big.LITTLE, a Cortex-A520 relative to an X4 has a capacity of 37%, so this relaxation would not be triggered.
Of course, there are two conservative safeguards:
If any core's capacity cannot be read, or all cores report the same value, Flutter continues to use the old frequency-based grouping logic.
And the kEfficiency grouping logic is unchanged: on SM8850 it still refers to the six Oryon performance cores with lower max frequency, and the background thread strategy was not entirely redone.
There is another question: the two super-large cores are obviously faster, so why is Raster slower?
The PR used Perfetto to trace the execution. Because Raster has a workload that is busy for a while when a frame arrives, then waits for the next frame, even though the two super-large cores have a 4.61 GHz ceiling, on this SM8850, after Raster is restricted to CPU 6–7, the thread often starts at 883 MHz, and 97%–100% of its CPU execution time is below 1 GHz.
In other words, the headline max frequency looks great, but a frame's work might be done before the frequency has time to ramp up.
It's like starting a bike in a high gear but only giving yourself 3 seconds; before you gain speed, you stop.
So when the app uses sched_setaffinity at startup to change Raster's allowed range to CPU 0–7, Perfetto shows the thread running more on the six ordinary performance cores, with an actual average execution frequency of 1.25–1.4 GHz, which is actually higher than when it was confined to the super-large cores.
Isn't that interesting? Similar to what we discussed before on iOS, where the system thinks you don't need Power cores because optimization is too good, and moves you to little cores—this is just as abstract. For example, after relaxing the restriction, opening and scrolling the detail page in the PR shows:
- Raster median frame time drops from 1.48 ms to 1.24 ms, and frames over 12 ms go from 7 to 2.
- Under a 120 Hz budget of 8.3 ms per frame, this change mainly shows up in page transitions, cold starts, and a few heavy frames.
- The medians for the other two scenarios also drop from 1.25 to 1.00 ms and 1.74 to 1.38 ms.
In effect, this method of partitioning cores by CPU frequency and then manually specifying affinity makes too many assumptions about the real chip topology and scheduling behavior. It isn't robust in the first place, but choosing this direction as a temporary solution is a compromise.
In theory, Android's official Performance Hint documentation already recommends that apps should not bind CPU cores themselves, because which group of cores is more suitable changes with SoC architecture, temperature, power limits, and system load. Generally, the more reasonable approach is to hand the target frame duration and actual work duration to ADPF, letting Android decide the frequency and core assignment.
Flutter also has a related draft PR #192582 that attempts to use ADPF Hint Sessions on Android 15+.
Overall, this problem is quite interesting. Flutter originally restricted threads' available CPUs to pursue stable performance, but the new hardware broke that assumption and turned it into negative optimization. It goes to show, this is the charm of "hardcoding".
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Reverted again.