Skip to content

[Xamarin.Android.Build.Tasks] mostly R2R the main assembly - #12248

Merged
jonathanpeppers merged 1 commit into
mainfrom
jonathanpeppers-mibc-partial-r2r
Jul 28, 2026
Merged

[Xamarin.Android.Build.Tasks] mostly R2R the main assembly#12248
jonathanpeppers merged 1 commit into
mainfrom
jonathanpeppers-mibc-partial-r2r

Conversation

@jonathanpeppers

@jonathanpeppers jonathanpeppers commented Jul 27, 2026

Copy link
Copy Markdown
Member

crossgen2's --partial only 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 GenerateMibcProfile task that writes a .mibc naming 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:

  • Methods in a generic context are left to the JIT. crossgen2 compiles those as shared code over System.__Canon, which a profile can only name via that implementation detail. So the assembly ends up mostly, not fully, R2R compiled.
  • Output is deterministic, so an unchanged app produces a byte identical .mibc and does not invalidate incremental builds.

Bring your own profile

If you record a real startup profile, for example with the maui profile startup --format mibc skill in dotnet/maui-labs, you have already said exactly what you want compiled, and the whole point of --partial is 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 .mibc to 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: true forces the profile on, false turns 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-arm64 so 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 maui dotnet new maui -sc
methods in profile 87 1,291
profile size 1,829 bytes 14,867 bytes
skipped as generic 0 23
app methods in R2R image 0 -> 87 0 -> 1,289
APK size 20,013,784 -> 20,079,320 23,032,560 -> 23,298,800
APK delta +65,536 (+0.33%) +266,240 (+1.16%)
startup before 826.6 +/- 3.6, 825.2 +/- 2.9 ms 1685.1 +/- 3.8, 1688.3 +/- 4.0 ms
startup after 805.7 +/- 2.7, 806.6 +/- 2.7 ms 1609.0 +/- 2.9, 1613.7 +/- 3.7 ms
startup delta -19.8 ms (-2.4%) -75.4 ms (-4.5%)

Both 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, -sc gains 1,289 for 260 KB.

apkdiff shows all the growth is the composite R2R image; dex and every other entry are byte identical in both templates.

$ apkdiff full-arm64-before.apk full-arm64-after.apk      # dotnet new maui
  +      66,488 lib/arm64-v8a/libassembly-store.so
Summary:
  +           0 Other entries 0.00% (of 3,930,902)
  +           0 Dalvik executables 0.00% (of 16,997,756)
  +      66,488 Shared libraries 0.39% (of 17,016,032)
  +      65,536 Package size difference 0.33% (of 20,013,784)

$ apkdiff sc-arm64-before.apk sc-arm64-after.apk          # dotnet new maui -sc
  +     267,632 lib/arm64-v8a/libassembly-store.so
Summary:
  +           0 Other entries 0.00% (of 7,041,375)
  +           0 Dalvik executables 0.00% (of 17,026,184)
  +     267,632 Shared libraries 1.34% (of 20,013,592)
  +     266,240 Package size difference 1.16% (of 23,032,560)

Build time cost

This never runs in a Debug build. $(PublishReadyToRun) only defaults to true for $(Configuration) of Release, and the target additionally requires crossgen2 to be in partial mode, so the inner-loop build is untouched. It is also CoreCLR only, since Microsoft.Android.Sdk.CoreCLR.targets is imported only for that runtime.

For Release builds, GenerateMibcProfile runs once per RID. Durations pulled from the binlogs:

build methods 1st RID 2nd RID
dotnet new maui 87 33.6 ms 1.0 ms
dotnet new maui -sc 1,291 34.5 ms 1.9 ms
-sc, PublishTrimmed=false 1,291 40.4 ms 1.9 ms

So 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, RunReadyToRunCompiler is 4.0-5.0 s in these same builds, so this is well under 1% of crossgen2's own time.

Tests

GenerateMibcProfileTests for the writer, and InstallAndRunTests.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 profiles obj/.../linked/App.dll, and -p:PublishTrimmed=false profiles obj/.../App.dll.

Copilot AI review requested due to automatic review settings July 27, 2026 21:47
@jonathanpeppers
jonathanpeppers force-pushed the jonathanpeppers-mibc-partial-r2r branch 2 times, most recently from e6e7b8d to efda171 Compare July 27, 2026 21:51
@jonathanpeppers jonathanpeppers changed the title [Xamarin.Android.Build.Tasks] fully R2R the app for partial builds [Xamarin.Android.Build.Tasks] mostly R2R the main assembly Jul 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 GenerateMibcProfile MSBuild task + MibcProfileWriter utility to emit a deterministic .mibc covering all compilable methods in the main app assembly.
  • Wire the task into Microsoft.Android.Sdk.CoreCLR.targets to run between ILLink and CreateReadyToRunImages when 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.

Comment thread src/Xamarin.Android.Build.Tasks/Utilities/MibcProfileWriter.cs Outdated
Comment thread tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs Outdated
@jonathanpeppers
jonathanpeppers force-pushed the jonathanpeppers-mibc-partial-r2r branch 3 times, most recently from 07bb5a7 to 8efe170 Compare July 27, 2026 23:51
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
jonathanpeppers force-pushed the jonathanpeppers-mibc-partial-r2r branch from 8efe170 to eb9fc75 Compare July 27, 2026 23:55
@jonathanpeppers jonathanpeppers added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 28, 2026
@jonathanpeppers
jonathanpeppers merged commit c440654 into main Jul 28, 2026
44 checks passed
@jonathanpeppers
jonathanpeppers deleted the jonathanpeppers-mibc-partial-r2r branch July 28, 2026 19:53
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants