[Xamarin.Android.Build.Tasks] mostly R2R the main assembly - #12248
Merged
Conversation
jonathanpeppers
force-pushed
the
jonathanpeppers-mibc-partial-r2r
branch
2 times, most recently
from
July 27, 2026 21:51
e6e7b8d to
efda171
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves partial ReadyToRun (R2R) publishing for CoreCLR by generating a synthetic MIBC profile for the app’s main assembly (post-trim, pre-crossgen2), ensuring the app assembly is fully R2R-compiled even when crossgen2 runs with partial compilation.
Changes:
- Add a new
GenerateMibcProfileMSBuild task +MibcProfileWriterutility to emit a deterministic.mibccovering all compilable methods in the main app assembly. - Wire the task into
Microsoft.Android.Sdk.CoreCLR.targetsto run betweenILLinkandCreateReadyToRunImageswhen partial R2R is enabled. - Add unit + device integration tests validating the generated profile and end-to-end behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | Adds an end-to-end device test for partial R2R ensuring the app assembly is actually R2R-compiled and runs. |
| src/Xamarin.Android.Build.Tasks/Utilities/TypeMapHelper.cs | Refactors UTF-8 encoding helper logic and centralizes the stackalloc threshold constant for reuse. |
| src/Xamarin.Android.Build.Tasks/Utilities/MibcProfileWriter.cs | Introduces a new writer that creates deterministic MIBC profiles using System.Reflection.Metadata. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateMibcProfileTests.cs | Adds unit tests covering zipped managed PE output, determinism, and missing-input behavior. |
| src/Xamarin.Android.Build.Tasks/Tasks/GenerateMibcProfile.cs | Adds the new MSBuild task that invokes MibcProfileWriter for the main app assembly. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets | Wires profile generation into the CoreCLR R2R pipeline (incremental, partial-mode gated). |
| .github/instructions/msbuild.instructions.md | Adds MSBuild authoring conventions documentation for contributors/automation. |
jonathanpeppers
force-pushed
the
jonathanpeppers-mibc-partial-r2r
branch
3 times, most recently
from
July 27, 2026 23:51
07bb5a7 to
8efe170
Compare
crossgen2's `--partial` switch restricts ReadyToRun compilation to methods named in a MIBC profile. That is a good trade for framework assemblies, but it means none of the app's own code gets precompiled unless it happens to appear in one of the shipped profiles. Add a `GenerateMibcProfile` task that runs after `ILLink` and before `CreateReadyToRunImages`, walks the trimmed main app assembly, and writes a `.mibc` naming its methods. The profile is appended to `@(PublishReadyToRunPgoFiles)`, so crossgen2 precompiles the app assembly while the rest of the app stays partially compiled. It is "mostly" and not "fully" because methods in a generic context are skipped: they need instantiations from `System.Private.CoreLib`, and measurements on the MAUI templates found zero such methods in a post-trim app assembly, so the machinery bought nothing. Skipped automatically when the app supplies its own `@(PublishReadyToRunPgoFiles)`, e.g. a profile recorded from a real startup trace. MAUI's framework profiles use the private `@(_ReadyToRunPgoFiles)`, so they do not suppress this. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c8115aec-183e-4d8d-abb9-0d79c8d417d3
jonathanpeppers
force-pushed
the
jonathanpeppers-mibc-partial-r2r
branch
from
July 27, 2026 23:55
8efe170 to
eb9fc75
Compare
jkoritzinsky
approved these changes
Jul 28, 2026
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
crossgen2's
--partialonly precompiles methods named in the profile data passed via--mibc. There is no profile for a new app, so today the main app assembly is effectively not ReadyToRun compiled at all.This adds a
GenerateMibcProfiletask that writes a.mibcnaming the compilable methods of the main app assembly and adds it to@(PublishReadyToRunPgoFiles), so crossgen2 compiles that assembly while everything else stays partial. Android version of dotnet/maui#34837.The target runs
AfterTargets="ILLink" BeforeTargets="CreateReadyToRunImages", so the profile only contains methods that survived trimming, and only when$(PublishReadyToRunCrossgen2ExtraArgs)contains--partial.Two things worth knowing:
System.__Canon, which a profile can only name via that implementation detail. So the assembly ends up mostly, not fully, R2R compiled..mibcand does not invalidate incremental builds.Bring your own profile
If you record a real startup profile, for example with the
maui profile startup --format mibcskill in dotnet/maui-labs, you have already said exactly what you want compiled, and the whole point of--partialis to compile only that. Naming every method of the app assembly on top of it would swamp it.So this turns itself off when
@(PublishReadyToRunPgoFiles)is already non-empty. Just adding your.mibcto that item group is enough, no extra property needed. MAUI's own framework profiles go into the private@(_ReadyToRunPgoFiles), so they do not suppress this.$(_AndroidReadyToRunMainAssembly)overrides the decision:trueforces the profile on,falseturns it off, blank (the default) means "on when crossgen2 is partial and the app brought no profile of its own".Results
Measured on the two MAUI templates, Android-only TFM, CoreCLR, Release, composite,
--partial;--map,-p:RuntimeIdentifier=android-arm64so the APK carries a single ABI and the size numbers are not diluted by a second copy of everything. Each pair is the same project built twice, differing only by$(_AndroidReadyToRunMainAssembly), so the profile is the only variable.Startup is from a physical arm64 device: 25 iterations per run, and the whole thing run twice in opposite order (before/after, then after/before) so any ordering or thermal drift would show up as a gap between the two passes of the same APK. It does not.
dotnet new mauidotnet new maui -scBoth passes of each APK agree to within a couple of ms, and the before/after gap is 5-15x the combined standard error, so the deltas are well clear of the noise.
Essentially every method named in the profile lands in the R2R image, and the trade scales with how much code the app actually has: the blank template gains 87 methods for 64 KB,
-scgains 1,289 for 260 KB.apkdiffshows all the growth is the composite R2R image; dex and every other entry are byte identical in both templates.Build time cost
This never runs in a Debug build.
$(PublishReadyToRun)only defaults totruefor$(Configuration)ofRelease, and the target additionally requires crossgen2 to be in partial mode, so the inner-loop build is untouched. It is also CoreCLR only, sinceMicrosoft.Android.Sdk.CoreCLR.targetsis imported only for that runtime.For Release builds,
GenerateMibcProfileruns once per RID. Durations pulled from the binlogs:dotnet new mauidotnet new maui -sc-sc,PublishTrimmed=falseSo 35-45 ms per build, against roughly 50 s of wall clock and 128-142 s of total task time. Whichever RID runs first pays about 34 ms of task assembly load and JIT; the second pays 1-2 ms, and that marginal cost is the same whether the profile names 87 methods or 1,291. Writing the profile is effectively free, you are just paying to load the task once.
For scale,
RunReadyToRunCompileris 4.0-5.0 s in these same builds, so this is well under 1% of crossgen2's own time.Tests
GenerateMibcProfileTestsfor the writer, andInstallAndRunTests.PublishReadyToRunPartial ([Values] bool isComposite)builds, verifies the app assembly is R2R compiled in the APK, then installs and launches it. Both variants pass on an x86_64 emulator.All four decision paths were verified end to end on a real MAUI app: partial with no profile generates it, a user supplied
@(PublishReadyToRunPgoFiles)suppresses it, and each explicit value of$(_AndroidReadyToRunMainAssembly)overrides both. Both trimming paths were checked too: the default build profilesobj/.../linked/App.dll, and-p:PublishTrimmed=falseprofilesobj/.../App.dll.