Build Performance (Windows)¶
Android release builds of this app are slow on Windows in a way they are not on macOS. This page records what was measured, what was changed, and what is left on the table.
What the build actually moves¶
Measured on this repo after a release build:
| Path | Files | Size |
|---|---|---|
build/ |
111,043 | 7.35 GB |
.dart_tool/ |
749 | 3.10 GB |
assets/ |
9,836 | 235 MB |
android/models_pack/ |
5 | 76.9 MB |
The shape matters more than the totals. This is a many-small-files workload — over a hundred thousand file creations per build — and that is precisely the workload where Windows is worst relative to macOS:
- Every file write passes through Defender's real-time filter driver.
Real-time protection is enabled on this machine (confirmed via
Get-MpComputerStatus). APFS on macOS has no equivalent per-write scan. - NTFS metadata operations (create/open/close/delete) are substantially more expensive than APFS equivalents, and this build does an enormous number of them.
- The app is unusually asset-heavy — ~259 MB of ONNX models stored
noCompressso they can be memory-mapped — so large files get copied and packaged repeatedly on top of the small-file churn.
Measured results¶
flutter build appbundle --release, same machine (14 cores), timing taken from
Gradle's own bundleRelease figure:
| Config | Defender | bundleRelease |
AAB |
|---|---|---|---|
| AGP 8.11.1, no perf flags | scanning | 761.7 s | 230.7 MB |
| AGP 8.11.1, no perf flags | excluded | 466.6 s | 230.7 MB |
| AGP 9.0.1 + parallel + caching | excluded | 315.3 s | 229.5 MB |
761.7 s → 315.3 s overall, a 2.4× speedup.
The two changes were isolated deliberately: the middle row reverts the toolchain and flags to exactly the baseline, so Defender exclusions alone account for −39%. Adding AGP 9 + parallel + caching took another −32% off what remained.
Read these with three caveats:
- The 761.7 s baseline ran without
flutter clean; both later rows include a clean. The later numbers therefore did strictly more work, so the improvement is understated rather than flattered. org.gradle.caching=truewrites to~/.gradle/caches/build-cache-1, whichflutter cleandoes not delete. The 315.3 s row benefits from cache entries populated by earlier AGP 9 builds. That is representative of everyday repeat builds, but a first-ever build on a fresh machine will be slower.flutter cleanitself went 75 s → 14 s across the two runs, but those deleted different amounts (an accumulated multi-build tree vs. a single build's output), so that pair is not a like-for-like comparison.
Changes made¶
In android/gradle.properties:
The build has 23 Gradle projects (app + models_pack + 21 Flutter plugin
modules). Without org.gradle.parallel they are configured and executed one at
a time, which leaves most cores idle — this machine has 14.
Both were verified with a green flutter build appbundle --release.
What NOT to change¶
kotlin.incremental must stay false¶
It looks like an easy win — it is Kotlin's own default, and it was switched off
in a commit whose message is about app icons, so it reads as accidental. It is
not. Flipping it to true fails the build on Windows:
Execution failed for task ':record_android:compileReleaseKotlin'.
> java.lang.Exception: Could not close incremental caches in
...\build\record_android\kotlin\compileReleaseKotlin\cacheable\caches-jvm\jvm\kotlin:
class-fq-name-to-source.tab, source-to-classes.tab, internal-name-to-source.tab
Every Kotlin module failed the same way. Kotlin's incremental caches are
memory-mapped .tab files, and with 21 plugin modules compiling concurrently
Windows cannot release the handles. This is the same file-locking pressure that
makes the build slow in the first place.
If you hit these errors after experimenting, clear the poisoned caches:
Defender exclusions — the single biggest lever (applied)¶
Worth more than every Gradle flag combined: −39% on its own, with nothing else changed. Requires an elevated PowerShell; the exclusion list cannot even be read without admin.
Currently excluded on this machine:
C:\Users\kahst\.gradle
C:\Users\kahst\AppData\Local\Android\Sdk
C:\Users\kahst\AppData\Local\Google\AndroidStudio2024.3
C:\Users\kahst\AppData\Local\Pub\Cache
D:\Code\birdnet-live-app
D:\flutter\flutter
To reproduce on another machine:
# Run as Administrator
Add-MpPreference -ExclusionPath 'D:\Code\birdnet-live-app'
Add-MpPreference -ExclusionPath "$env:USERPROFILE\.gradle"
Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\Pub\Cache"
Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\Android\Sdk"
Add-MpPreference -ExclusionPath 'D:\flutter\flutter'
build/ and .dart_tool/ need no separate entries — they sit under the project
path. The easy ones to forget are ~\.gradle and the Flutter SDK itself.
Verify with Get-MpPreference in an elevated shell, then time a build:
Note the security trade-off: these paths stop being scanned in real time. That is a normal developer-machine trade, but it is a decision, not a free win.
Other options, not taken¶
org.gradle.configuration-cache=true— potentially a large win on the configuration phase given 23 projects, but Flutter's Gradle plugin has a history of incompatibility with it. Untested here; try it in isolation.- Raising
-Xmx4G— plausible on a machine with headroom, but there was no evidence of heap pressure, so it would be a guess. - Moving the repo/caches to a different volume — only worth investigating if
D:turns out to be slower thanC:; not measured.