Skip to content

Add MIBC profile generator for InitializeComponent methods#34660

Open
jkoritzinsky wants to merge 2 commits intodotnet:mainfrom
jkoritzinsky:feature/mibc-profile-generator
Open

Add MIBC profile generator for InitializeComponent methods#34660
jkoritzinsky wants to merge 2 commits intodotnet:mainfrom
jkoritzinsky:feature/mibc-profile-generator

Conversation

@jkoritzinsky
Copy link
Copy Markdown
Member

@jkoritzinsky jkoritzinsky commented Mar 25, 2026

Summary

Adds a build tool that generates MIBC (Managed Instrumented Binary Code) profile files listing all InitializeComponent* methods from compiled MAUI assemblies. These files do not have any actual profile data, but they can be consumed by crossgen2 for partial compilation. Also add MSBuild targets to enable partial R2R for just XAML-generated code in Debug builds for CoreCLR targets using said tool to provide a faster experience for XAML loading in Debug using said MIBC files.

What's included

MibcProfileGenerator tool (src/Controls/src/Build.Tasks/MibcProfileGenerator/)

A standalone .NET console app that:

  • Reads one or more input assemblies using \System.Reflection.Metadata\
  • Discovers all XAML-generated method variants:
    • InitializeComponent — primary entry point
    • InitializeComponentRuntime — runtime XAML inflation
    • InitializeComponentXamlC — XamlC IL-compiled path
    • InitializeComponentSourceGen — source generator compiled path
  • Emits a valid MIBC PE assembly following the format from dotnet/runtime's MibcEmitter.cs
  • Supports both compressed (.mibc) and uncompressed (.dll) output
  • Handles nested types, multiple assemblies, and proper metadata references
  • Validated with dotnet-pgo dump

MSBuild integration

  • New opt-in target _MauiGenerateMibcProfile in Microsoft.Maui.Controls.targets
  • Runs after XamlC so all IL post-processing is complete
  • Enabled by in Debug CoreCLR builds.
  • Incremental build support via Inputs/Outputs

Packaging

  • Tool is packaged alongside existing build tasks in the NuGet package
  • Local dev support via _CopyToBuildTasksDir target

Usage

Automatic in Debug CoreCLR builds.

Add a build tool that scans compiled MAUI assemblies for all
InitializeComponent* methods (InitializeComponent, InitializeComponentRuntime,
InitializeComponentXamlC, InitializeComponentSourceGen) and produces a MIBC
profile file consumable by crossgen2 and the .NET AOT compiler for PGO.

The tool is integrated into the MSBuild pipeline via the
_MauiGenerateMibcProfile target, which runs after XamlC and is opt-in
via the MauiGenerateMibcProfile property. The generated file is exposed
as the @(MauiMibcProfile) MSBuild item for downstream consumption.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 25, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34660

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34660"

@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Mar 25, 2026
@jkoritzinsky jkoritzinsky marked this pull request as ready for review March 25, 2026 22:52
Copilot AI review requested due to automatic review settings March 25, 2026 22:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new build-time tool and MSBuild integration to generate a MIBC profile containing all InitializeComponent* methods from compiled MAUI assemblies, intended to enable partial R2R for XAML-generated code paths.

Changes:

  • Add MibcProfileGenerator console tool that scans assemblies via System.Reflection.Metadata and emits a MIBC PE (optionally compressed).
  • Add _MauiGenerateMibcProfile MSBuild target to run the tool after XamlC and feed the output into PublishReadyToRunPgoFiles / crossgen2 --partial.
  • Package the tool alongside existing Controls build tasks for transitive consumption.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets Adds opt-in target to generate a .mibc and adjust crossgen2 args for partial compilation.
src/Controls/src/Build.Tasks/MibcProfileGenerator/Program.cs Implements the assembly scanner and MIBC emitter.
src/Controls/src/Build.Tasks/MibcProfileGenerator/MibcProfileGenerator.csproj New net10.0 tool project definition + local copy target.
src/Controls/src/Build.Tasks/Controls.Build.Tasks.csproj Wires tool into build tasks packaging (adds project reference + packed outputs).

- Add explicit ret instructions to group and AssemblyDictionary IL methods
- Fix nested type namespace resolution (walk to outermost declaring type)
- Include deps.json in NuGet package and local dev copy
- Fix comment to match actual property/item names
- Fix indentation to use tabs consistently in targets file

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Runs after XamlC so the IL has been post-processed and method signatures are final.
Produces a $(_MauiMibcProfilePath) file and adds it to the @(PublishReadyToRunPgoFiles) item. -->
<Target Name="_MauiGenerateMibcProfile"
AfterTargets="XamlC"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The XamlC target should not run eventually (hopefully already in .NET 11) and XAML code should be compiled just via the source generator by default. In that case, we should make sure this runs after CoreCompile instead.

Comment on lines +281 to +283
<PropertyGroup>
<PublishReadyToRunCrossgen2ExtraArgs>$(PublishReadyToRunCrossgen2ExtraArgs) --partial</PublishReadyToRunCrossgen2ExtraArgs>
</PropertyGroup>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's a different spot in the MAUI SDK which turns on partial R2R - it's the default whenever R2R is enabled (which is also the default). I don't think we should enforce partial here and allow devs to disable it via $(_MauiPublishReadyToRunPartial)=false. Maybe we could actually use that property to skip this task.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@simonrozsival where is that? I only see that in src/Workload/Microsoft.Maui.Sdk/Sdk/Microsoft.Maui.Sdk.Before.targets which only works for Windows today and doesn't turn on partial R2R.

<_MibcProfileGeneratorPath>$(MSBuildThisFileDirectory)MibcProfileGenerator.dll</_MibcProfileGeneratorPath>
</PropertyGroup>

<Exec Command="dotnet exec &quot;$(_MibcProfileGeneratorPath)&quot; &quot;$(_MauiMibcProfilePath)&quot; &quot;$(IntermediateOutputPath)$(TargetFileName)&quot;" />
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder why we explicitly dotnet exec here instead of turning this into a custom MSBuild task?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

System.Reflection.Metadata is a very finicky dependency to get right for MSBuild. Also having a separate tool enables me (or rather Copilot) to more easily validate the tool with ad-hoc testing.

Comment on lines +33 to +35
"InitializeComponentRuntime",
"InitializeComponentXamlC",
"InitializeComponentSourceGen",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In Release builds, only the InitializeComponent method is relevant. The other 3 are used only for unit testing, when we need to generate all 3 versions at build time and switch between them at runtime.

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.

unit testing, and full page XamlHotReload. But as @simonrozsival said, in Release builds, only InitializeComponent should be present

@simonrozsival
Copy link
Copy Markdown
Member

@jkoritzinsky I really like this. I think this is a great start and once we have this task that generates an app-specific profile based on static analysis, we can experiment with including more than just InitializeComponent methods in the .mibc.

For example, we were thinking about changing the way we generate the "inflator" code to be split between smaller methods (https://github.com/dotnet/maui/pull/31555/changes#diff-d1e4f87785dfbad698ce654733169bf7e6091581ccebdbada7f5e66460b17bfcR166-R224) which allowed us to generate much more efficient code which showed in benchmarks. If we wanted to do that, we should also update MibcProfileGenerator.

One other thing we discussed internally some time ago was that maybe we should simply R2R anything in the app-specific .dlls. They are usually quite small compared to the BCL + MAUI SDK and Android/iOS bindings, so just naively compiling everything should not increase the app bundle too much and it would make sure we don't need to JIT any of the app code at startup.

@vitek-karas
Copy link
Copy Markdown
Member

The description of this PR might be a bit misleading since it says it's for Debug builds. If I read the code correctly it will work for both. In reality this doesn't make much sense for Debug builds - we will not R2R the application in Debug builds because it would hurt built time, and I think the cost of running crossgen is larger than the startup gain it provides. Additionally, we don't have the ability to debug R2R code, so we can't really R2R the app itself anyway.

For Release builds this would make a lot of sense. The prime example of code this can help with is XAML inflation - which is the InitializeComponents. But as Simon mentions above the shape of that code will evolve, we want to make it less of a single large method. Also in .NET 11 the default is source generated, so XamlC should not be used at all.

XAML inflation is responsibility of the XSG (XAML Source Gen), thus that's the component with the best knowledge of what methods would benefit from being R2R. Personally, I think it would be even better if we could introduce some attribute which XSG puts on certain methods and that would tell this new tool to include those methods in the MIBC file. @StephaneDelcroix what do you think?

@StephaneDelcroix
Copy link
Copy Markdown
Contributor

XAML inflation is responsibility of the XSG (XAML Source Gen), thus that's the component with the best knowledge of what methods would benefit from being R2R. Personally, I think it would be even better if we could introduce some attribute which XSG puts on certain methods and that would tell this new tool to include those methods in the MIBC file. @StephaneDelcroix what do you think?

Thanks for this, I love what you're doing there

a few thoughts:

  • it should only be done for Release builds. With Debug builds, we aim for the lowest build time possible, and make the develop as fast possible, so extra analysis, and r2r, doesn't make much sense there
  • whatever the inflator you opt for (it's still an option to ask, even in Release builds, for Runtime, or XamlC inflation), the only method present should be InitializeComponent for Release builds. if it's not, it's a bug that we should fix
  • at this time, and for net11.0, the source generator only generates a single, large, InitializeComponent, but the compiler might generate extra types and methods (for lambda). As @vitek-karas said, it might make sense to provide an attribute, like MethodImplAttribute is doing (but not quite) so we, and eventually the user, have a way to tag those methods.
  • if we do so, we might need diagnostics reports about the consequences of so many R2R methods...

@jkoritzinsky
Copy link
Copy Markdown
Member Author

The idea of this tool is to run only in Debug builds.

@vitek-karas this tool only makes sense for Debug builds, as for Release these methods should already be included in R2R unless MAUI's existing partial R2R support doesn't include the user's app assembly or the XAML code. If the Release configuration currently doesn't include the XAML code and we want it to do so, then this becomes interesting for Release.

@StephaneDelcroix is our goal for the Debug experience lowest build time or lowest F5 (build time + startup time to first page)? This PR is meant to improve the overall F5 time by spending a little more time in build to get a lot faster startup. If that's not what we want to optimize for (or this tool does not achieve it) then it's not worth adding.

I'll update this tool to run after CoreCompile as well to catch the debug scenario.

@vitek-karas
Copy link
Copy Markdown
Member

vitek-karas commented Mar 27, 2026

(I apologize for the long writeup)

I also realized that below is only discussing Android. On iOS the situation is different (I will comment on it separately)

Android specifically:

Debug build

First, it's important to note that the current debug build doesn't reflect what we want to do, we just didn't get to implementing it yet (we're working on the measurements first, to validate everything properly, and we'll need the data anyway).
Unless the numbers tell us otherwise, the current POR is that Android debug build will be very similar to iOS debug build (which is already implemented):

  • First build will run crossgen on "non app code", so BCL, Android, MAUI and hopefully binary nugets in the app
  • The output of this is a single composite R2R image of pretty much everything but the app itself
  • Subsequent incremental builds will NOT rerun R2R, they will just C# compiler the app assembly and using FastDeploy upload that one assembly to the device and run the app
  • At startup, this means that everything but the app's assembly code will be R2R, the only JITing will happen for the app's assembly
  • It is true, that in this case we expect the InitializeComponents to be the most expensive method to JIT and thus it will slow down the startup a bit.

Assuming this change is in place - these changes would be applied to the above

  • Run the new tool - to be 100% correct this would need to run on every build, even incremental rebuilds
  • Run crossgen on the app's assembly on every incremental build - this does run JIT on the InitializeComponentes method, so the time it takes to JIT that method is paid here - on EVERY F5.
  • FastDeploy the slightly larger app's assembly
  • At startup it's the same as above with the addition that InitializeComponents is R2R, so no need to JIT it again. This then saves the time we've spent JITing it.

The outcome is that we still pay the JIT cost of InitializeComponents on every F5, we just moved it from startup to build. And we added quite a bit of overhead on top (running the new tool, running crossgen itself).

I honestly don't see this as possibly beneficial, but I would be happy to measure this and decide based on the data.

Overall, we should optimize for the whole F5 performance, not for any specific parts of it, as they rarely run separately. So, if we can get startup gains by spending more time in build, it in itself is not interesting, it would only help if combined it's faster overall.

Release build

The important thing about Release builds on Android is that we optimize for a compromise between size and speed (unlike iOS which for other reasons pretty much only optimizes for speed). Android will R2R the whole application, but to save on size, we perform a partial composite R2R compilation. The "partial" part is driven by a startup profile, which is collected on a "generic" application. One important aspect of the profile is that it won't have anything from the app's itself (the names would not match anyway).
This does a pretty good job precompiling all of the framework code needed during startup, but it does miss the app's code completely.
As Simon mentioned above, we've been considering precompiling the whole app's assembly as one way to improve this.

Using the new tool introduced in this PR would give us some of the benefits of the app's assembly pre-compilation for speed, but with smaller size. It would precompile the InitializeComponents, which is the most JIT expensive method on startup. (It has also benefits for any page navigation which is another sore spot for performance).

In Release builds the impact on build time is MUCH less important, so it makes total sense to pay small additional price (running the new tool) to get the pre-compilation of big methods for startup.

@vitek-karas
Copy link
Copy Markdown
Member

The iOS side of things:

Debug build

What happens today:

  • We R2R composite everything but the app in the initial build
  • Subsequent incremental builds reuse this R2R image and don't run crossgen
  • At startup we pay the price of interpreting the InitializeComponents method, which is likely non-trivial.

With the change we would probably want to do something like this:

  • Run the new tool on every incremental build
  • Run crossgen (new invocation) on every incremental build, only on the app's assembly and with the generated mibc - this would precompile InitializeComponents
  • Deploy the resulting .dylib to the device
  • At startup we would hopefully get a noticeable improvement by not having to interpret InitializeCompontents

On paper this sounds like it might be beneficial - the question is if the additional time spent running the tools (the new one, crossgen, deploying new file) will be compensated by the reduction of startup. I would not be surprised if for small apps it's not worth it, but it might be for larger ones. We need to measure this.

Trade-off:
With this we would lose the ability to debug the InitializeComponents (or any other method which is precompiled). This might be OK if the performance win is worth it. We would need a way to disable this to enable debugging if necessary.

Risks:

  • We would need to add a second invocation of crossgen into the build - not really a problem, just added complexity
  • Will the runtime be able to load 2 R2R images, the iOS R2R loading is different and new, so it might require fixes to handle this case
  • I know that iOS doesn't like loose .dylibs in the app's package, we might need to wrap it in a framework - but maybe this is not necessary on Debug builds.

Release build

Nothing changes here, we R2R composite the whole application, so it should have all the benefits already.

@jkoritzinsky
Copy link
Copy Markdown
Member Author

@vitek-karas thanks for these writeups! I agree that this doesn't make nearly as much (if any) sense for Android as we do have a functioning JIT there. iOS debug is the only place where this makes sense.

Once I get a basic perf test with Apple Mobile, I'll make the adjustments to make this have an opt-out and make it Apple-mobile only (if the numbers look like it's worth it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants