{"id":42998,"date":"2022-11-03T10:01:00","date_gmt":"2022-11-03T17:01:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=42998"},"modified":"2022-11-03T19:34:14","modified_gmt":"2022-11-04T02:34:14","slug":"dotnet-7-performance-improvements-in-dotnet-maui","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-7-performance-improvements-in-dotnet-maui\/","title":{"rendered":".NET 7 Performance Improvements in .NET MAUI"},"content":{"rendered":"<p>When .NET MAUI reached GA, we had goals of improving upon\nXamarin.Forms in startup time and application size. See last release&#8217;s\n<a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-dotnet-maui\/\">Performance Improvements in .NET MAUI<\/a>, to learn about the\nperformance benefits of migrating from Xamarin.Android, Xamarin.iOS,\nor Xamarin.Forms to .NET 6+ and .NET MAUI.<\/p>\n<p>We continued with these themes in .NET 7, building upon the excellent\n<a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance_improvements_in_net_7\/\">.NET performance work described by Stephen Toub<\/a>. General\nimprovements in the .NET runtime or base class libraries (BCL) lay the\nfoundation for the performance we are able to achieve in .NET MAUI.<\/p>\n<p>For Android, our focus remains on startup performance, since\napplication size is in a good place. For iOS, we have the opposite\ngoal: we continued to focus on application size, since startup\nperformance on iOS is in good shape.<\/p>\n<p>From customer feedback we found the need to go beyond just startup\nperformance, also focusing a bit on general UI performance, layout,\nscrolling, etc. We also improved startup time and general performance\nfor desktop platforms. We hope to continue making improvements in\nthese areas in future .NET releases.<\/p>\n<p>For an overall comparison of startup time between .NET 6 and .NET 7,\napps in <code>Release<\/code> mode on a Pixel 5:<\/p>\n<table>\n<thead>\n<tr>\n<th>Application<\/th>\n<th>Version<\/th>\n<th style=\"text-align: right\">Startup Time(ms)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>dotnet new maui<\/td>\n<td>.NET 6<\/td>\n<td style=\"text-align: right\">568.1<\/td>\n<\/tr>\n<tr>\n<td>dotnet new maui<\/td>\n<td>.NET 7<\/td>\n<td style=\"text-align: right\">545.4<\/td>\n<\/tr>\n<tr>\n<td>.NET Podcast<\/td>\n<td>.NET 6<\/td>\n<td style=\"text-align: right\">814.2<\/td>\n<\/tr>\n<tr>\n<td>.NET Podcast<\/td>\n<td>.NET 7<\/td>\n<td style=\"text-align: right\">759.7<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In .NET 7, .NET MAUI applications should see an improvement to startup\ntime as well as smoothing scrolling, navigation, and general UI\nperformance.<\/p>\n<p>Likewise, iOS applications have continued to get smaller in .NET 7:<\/p>\n<table>\n<thead>\n<tr>\n<th>Application<\/th>\n<th>Version<\/th>\n<th style=\"text-align: right\"><code>.ipa<\/code> Size (MB)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>dotnet new maui<\/td>\n<td>.NET 6<\/td>\n<td style=\"text-align: right\">12.64<\/td>\n<\/tr>\n<tr>\n<td>dotnet new maui<\/td>\n<td>.NET 7<\/td>\n<td style=\"text-align: right\">12.48<\/td>\n<\/tr>\n<tr>\n<td>.NET Podcast<\/td>\n<td>.NET 6<\/td>\n<td style=\"text-align: right\">25.08<\/td>\n<\/tr>\n<tr>\n<td>.NET Podcast<\/td>\n<td>.NET 7<\/td>\n<td style=\"text-align: right\">23.91<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Testing a <a href=\"https:\/\/github.com\/Kalyxt\/Test_CollectionView\"><code>CollectionView<\/code> sample<\/a> on a modest\nAndroid device (Pixel 4a). We can see a clear improvement with\n<a href=\"https:\/\/developer.android.com\/topic\/performance\/rendering\/inspect-gpu-rendering\">Android&#8217;s visual GPU profiler<\/a>, a screenshot taken\nduring some &#8220;brisk&#8221; scrolling:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/ScrollingNET6v7.png\" alt=\"Android Visual GPU Profiler .NET 6 vs 7\" \/><\/p>\n<p>This sample is a <code>CollectionView<\/code> with 10,000 rows of two data-bound\nlabels.<\/p>\n<p>Simply updating to .NET 7 should result in smaller\/faster .NET MAUI\napplications. Details below!<\/p>\n<h2>Table Of Contents<\/h2>\n<p>Scrolling and Layout Performance Improvements<\/p>\n<ul>\n<li><a href=\"#lols-per-second\">LOLs per second<\/a><\/li>\n<li><a href=\"#avoid-repeated-viewcontext-calls\">Avoid Repeated <code>View.Context<\/code> Calls<\/a><\/li>\n<li><a href=\"#avoid-viewcontext-calls-in-collectionview\">Avoid <code>View.Context<\/code> Calls in <code>CollectionView<\/code><\/a><\/li>\n<li><a href=\"#reduce-jni-calls-during-layout\">Reduce JNI Calls During Layout<\/a><\/li>\n<li><a href=\"#cache-values-for-rtl-and-dark-mode\">Cache Values for RTL and Dark Mode<\/a><\/li>\n<li><a href=\"#avoid-creating-iview-during-layout\">Avoid Creating <code>IView[]<\/code> During Layout<\/a><\/li>\n<li><a href=\"#defer-rtl-layout-calculations-to-platform\">Defer RTL Layout Calculations to Platform<\/a><\/li>\n<li><a href=\"#further-notes-on-collectionview\">Further Notes on <code>CollectionView<\/code><\/a><\/li>\n<\/ul>\n<p>Startup Performance Improvements<\/p>\n<ul>\n<li><a href=\"#android-ndk-compiler-flags\">Android NDK Compiler Flags<\/a><\/li>\n<li><a href=\"#datetimeoffsetnow\"><code>DateTimeOffset.Now<\/code><\/a><\/li>\n<li><a href=\"#avoid-colorstatelistintint\">Avoid <code>ColorStateList(int[][],int[])<\/code><\/a><\/li>\n<li><a href=\"#improvements-to-net-mauis-aot-profile\">Improvements to .NET MAUI&#8217;s AOT Profile<\/a><\/li>\n<li><a href=\"#better-string-comparisons-in-java-interop\">Better String Comparisons in Java Interop<\/a><\/li>\n<li><a href=\"#xaml-compilation-improvements\">XAML Compilation Improvements<\/a><\/li>\n<li><a href=\"#readytorun-by-default-on-windows\"><code>ReadyToRun<\/code> by Default on Windows<\/a><\/li>\n<li><a href=\"#dual-architectures-by-default-on-macos\">Dual Architectures by Default on macOS<\/a><\/li>\n<li><a href=\"#notes-about-regexoptionscompiled\">Notes about <code>RegexOptions.Compiled<\/code><\/a><\/li>\n<li><a href=\"#improvements-to-monos-interpreter\">Improvements to Mono&#8217;s Interpreter<\/a><\/li>\n<\/ul>\n<p>App Size Improvements<\/p>\n<ul>\n<li><a href=\"#fix-debuggersupport-trimmer-value-for-android\">Fix <code>DebuggerSupport<\/code> Trimmer Value for Android<\/a><\/li>\n<li><a href=\"#r8-java-code-shrinker-improvements\">R8 Java Code Shrinker Improvements<\/a><\/li>\n<li><a href=\"#feature-to-exclude-kotlin-related-files\">Feature to Exclude Kotlin-related Files<\/a><\/li>\n<li><a href=\"#improve-aot-output-for-generics\">Improve AOT Output for Generics<\/a><\/li>\n<\/ul>\n<p>Tooling and Documentation<\/p>\n<ul>\n<li><a href=\"#profiling-net-maui-apps\">Profiling .NET MAUI Apps<\/a><\/li>\n<li><a href=\"#measuring-startup-time\">Measuring Startup Time<\/a><\/li>\n<li><a href=\"#app-size-reporting-tools\">App Size Reporting Tools<\/a><\/li>\n<li><a href=\"#experimental-or-advanced-options\">Experimental or Advanced Options<\/a><\/li>\n<\/ul>\n<h2>Scrolling and Layout Performance Improvements<\/h2>\n<h3>LOLs per Second<\/h3>\n<p>We have seen benchmarks of different UI frameworks that follow a\npattern, such as:<\/p>\n<ol>\n<li>\n<p>Put text on the screen with a random rotation and color<\/p>\n<\/li>\n<li>\n<p>Report how many times a second you can do this<\/p>\n<\/li>\n<\/ol>\n<p>The existing samples were OK, but I found starting from scratch I\ncould avoid minor performance issues and focus on the raw numbers .NET\nMAUI could achieve. This also gave me the opportunity to use the word\n&#8220;LOL&#8221; in the text onscreen, aptly timing the number of &#8220;LOLs per\nsecond&#8221; we can achieve in .NET MAUI. LOL?<\/p>\n<p>Additionally, we timed different types of apps:<\/p>\n<ul>\n<li>A Xamarin.Forms application (thanks <a href=\"https:\/\/github.com\/roubachof\">@roubachof<\/a> for the contribution!)<\/li>\n<li>A .NET MAUI application<\/li>\n<li>A .NET 6 Android application (using our C# bindings for Android APIs)<\/li>\n<li>An Android application written in Java<\/li>\n<\/ul>\n<p>The above apps would all use the same underlying\n<code>Android.Widget.TextView<\/code>. Each app should progressively be able to\nachieve better results: Xamarin.Forms to MAUI to C# <code>TextView<\/code> to Java\n<code>TextView<\/code> (no C# to Java interop):<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/LOLsNET6Chart.png\" alt=\"Chart of LOLs per second\" \/><\/p>\n<p>Overall, this concept (although quite fun!) isn&#8217;t an exact science. My\nversion of the sample required some duration of <code>Thread.Sleep()<\/code>,\nwhich will certainly vary depending on the device you are running on.\nThe sample needs to sleep long enough for the UI thread to be able to\nkeep up with a background thread. Just from visually watching the app,\nI tried to pick a <code>Thread.Sleep()<\/code> time where we got a good number of\nLOLs per second and the UI appeared to be updating quickly.<\/p>\n<p>However, the sample in itself is still useful:<\/p>\n<ol>\n<li>\n<p>We have a fun, visual comparison between .NET MAUI in .NET 6 vs .NET 7<\/p>\n<\/li>\n<li>\n<p>It is a literal gold mine for finding performance issues!<\/p>\n<\/li>\n<\/ol>\n<p>Simply reviewing <code>dotnet-trace<\/code> output of these apps, led to changes\nto improve the performance in .NET MAUI of:<\/p>\n<ul>\n<li>\n<p>Each <code>View<\/code> added to the screen<\/p>\n<\/li>\n<li>\n<p>Layout performance<\/p>\n<\/li>\n<li>\n<p>Scrolling performance<\/p>\n<\/li>\n<li>\n<p>Navigation between pages<\/p>\n<\/li>\n<\/ul>\n<p>In total, the LOLs per second on a Pixel 5 went from ~327 per second\nto ~493 per second moving from .NET 6 to .NET 7:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/MAUILolsNET6vs7.png\" alt=\"LOLs per second screenshot\" \/><\/p>\n<p>For full details and source code for this sample\/benchmark, see the\n<a href=\"https:\/\/github.com\/jonathanpeppers\/lols\">jonathanpeppers\/lols<\/a> repository on GitHub.<\/p>\n<h3>Avoid Repeated <code>View.Context<\/code> Calls<\/h3>\n<p>When reviewing <code>dotnet-trace<\/code> output of the LOLs\/second sample, we\nnoticed:<\/p>\n<pre><code class=\"language-csharp\">7.60s (14%) mono.android!Android.Views.View.get_Context()<\/code><\/pre>\n<p>There is an interesting Java to C# interop implication in this call.\nSince the beginning of Xamarin.Android we&#8217;ve had the behavior:<\/p>\n<ul>\n<li>\n<p>C# calls a Java method<\/p>\n<\/li>\n<li>\n<p>A Java object is returned to C#<\/p>\n<\/li>\n<li>\n<p>Using the <code>Handle<\/code> of the Java object, find out if we have a C#\nobject alive with the same <code>Handle<\/code>.<\/p>\n<\/li>\n<li>\n<p>If not, create a new C# instance to wrap this object for use in\nmanaged code.<\/p>\n<\/li>\n<\/ul>\n<p>So you could see how repeated calls to <code>.Context<\/code> would be a\nperformance concern, after realizing the work happening behind the\nscenes.<\/p>\n<p>We found layout-related code in .NET MAUI, such as:<\/p>\n<pre><code class=\"language-csharp\">var deviceIndependentWidth = widthMeasureSpec.ToDouble(Context);\r\nvar deviceIndependentHeight = heightMeasureSpec.ToDouble(Context);\r\n\/\/...\r\nvar platformWidth = Context.ToPixels(width);\r\nvar platformHeight = Context.ToPixels(height);<\/code><\/pre>\n<p>Where the four calls here could be replaced by a local variable, or\neven a member field for the lifetime of this object. This simple\nchange directly translated to more <code>Label<\/code>&#8216;s per second as well as\nfaster startup and scrolling in .NET MAUI.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8001\">dotnet\/maui#8001<\/a> for details about this improvement.<\/p>\n<h3>Avoid <code>View.Context<\/code> Calls in <code>CollectionView<\/code><\/h3>\n<p>In .NET 6, we had some reports of poor CollectionView performance\nwhile scrolling on older Android devices.<\/p>\n<p>Reviewing <code>dotnet-trace<\/code> output, I did find some issues similar to\n<a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8001\">dotnet\/maui#8001<\/a>:<\/p>\n<pre><code class=\"language-csharp\">317.42ms (1.1%) mono.android!Android.Views.View.get_Context()<\/code><\/pre>\n<p>1% of the time was spent in repeated calls to <code>View.Context<\/code> inside the\n<code>ItemContentView<\/code> class, such as:<\/p>\n<pre><code class=\"language-csharp\">if (pixelWidth == 0)\r\n{\r\n    pixelWidth = (int)Context.ToPixels(measure.Width);\r\n}\r\nif (pixelHeight == 0)\r\n{\r\n    pixelHeight = (int)Context.ToPixels(measure.Height);\r\n}\r\n\/\/...\r\nvar x = (int)Context.ToPixels(mauiControlsView.X);\r\nvar y = (int)Context.ToPixels(mauiControlsView.Y);\r\nvar width = Math.Max(0, (int)Context.ToPixels(mauiControlsView.Width));\r\nvar height = Math.Max(0, (int)Context.ToPixels(mauiControlsView.Height));<\/code><\/pre>\n<p>Making a new overload for <code>ContextExtensions.FromPixel()<\/code>, we were\nable to remove all of these <code>View.Context<\/code> calls.<\/p>\n<p>After these changes, we see a huge difference in the time spent in\nthis method:<\/p>\n<pre><code class=\"language-csharp\">1.30ms (0.01%) mono.android!Android.Views.View.get_Context()<\/code><\/pre>\n<p>We also tested these changes with the &#8220;janky frames&#8221; profiler in\nAndroid Studio, which gives information like this on Android 12+\ndevices (such as a Pixel 4a). We could see several dropped frames when\nscrolling a <code>CollectionView<\/code>:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/JankyFramesBefore.png\" alt=\"Android Studio Janky Frames Before\" \/><\/p>\n<p>On the same Pixel 4a afterward, we only saw a single dropped frame:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/JankyFramesAfter.png\" alt=\"Android Studio Janky Frames After\" \/><\/p>\n<p>We could also test these changes with <a href=\"https:\/\/developer.android.com\/topic\/performance\/rendering\/inspect-gpu-rendering\">Android&#8217;s visual GPU\nprofiler<\/a>, seeing a visual difference in before:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/VisualProfilerBefore.png\" alt=\"Android Visual GPU Profiler Before\" \/><\/p>\n<p>Versus afterward:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/VisualProfilerAfter.png\" alt=\"Android Visual GPU Profiler After\" \/><\/p>\n<p>The larger bars represents time spent in code paths that attribute to\na poor framerate. These changes should improve the performance of\nscrolling for any .NET MAUI <code>CollectionView<\/code> on Android.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8243\">dotnet\/maui#8243<\/a> for details about this improvement.<\/p>\n<h3>Reduce JNI Calls During Layout<\/h3>\n<p>While profiling customer sample similar to the &#8220;LOLs per second&#8221; app,\nwe noticed a decent chunk of time spent measuring Android <code>View<\/code>&#8216;s:<\/p>\n<pre><code class=\"language-csharp\">932.51ms (7.4%)  mono.android!Android.Views.View.Measure(int,int)\r\n115.53ms (0.91%) mono.android!Android.Views.View.get_MeasuredWidth()\r\n 96.97ms (0.77%) mono.android!Android.Views.View.get_MeasuredHeight()<\/code><\/pre>\n<p>This led us to write a Java method that calls all three Java APIs and\nreturn <code>MeasuredWidth<\/code> and <code>MeasuredHeight<\/code>. After a little research,\nit seemed the best approach here was to &#8220;pack&#8221; two integers into a\n<code>long<\/code>. If we tried to return some Java object instead, then we&#8217;d have\nthe same number of JNI calls to get the integers out.<\/p>\n<p>The Java side implementation arrives at:<\/p>\n<pre><code class=\"language-java\">public static long measureAndGetWidthAndHeight(View view, int widthMeasureSpec, int heightMeasureSpec) {\r\n    view.measure(widthMeasureSpec, heightMeasureSpec);\r\n    int width = view.getMeasuredWidth();\r\n    int height = view.getMeasuredHeight();\r\n    return ((long)width &lt;&lt; 32) | (height &amp; 0xffffffffL);\r\n}<\/code><\/pre>\n<p>Which is unpacked in C# via:<\/p>\n<pre><code class=\"language-csharp\">var packed = PlatformInterop.MeasureAndGetWidthAndHeight(platformView, widthSpec, heightSpec);\r\nvar measuredWidth = (int)(packed &gt;&gt; 32);\r\nvar measuredHeight = (int)(packed &amp; 0xffffffffL);<\/code><\/pre>\n<p>So instead of three transitions from C# to Java, we now have one. It\nappears the performance of the additional math is negligible compared\nto reducing the amount of interop overhead in this example.<\/p>\n<p>Measuring again <em>after<\/em> the change, we were able to see an obvious\nsavings in this method:<\/p>\n<pre><code class=\"language-diff\">--783.92ms (6.2%) microsoft.maui!Microsoft.Maui.ViewHandlerExtensions.GetDesiredSizeFromHandler\r\n++528.96ms (4.5%) microsoft.maui!Microsoft.Maui.ViewHandlerExtensions.GetDesiredSizeFromHandler<\/code><\/pre>\n<p>Which also translated to more <code>Label<\/code>&#8216;s per second in the sample app.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8034\">dotnet\/maui#8034<\/a> for details about this improvement.<\/p>\n<h3>Cache Values for RTL and Dark Mode<\/h3>\n<p>While profiling customer sample similar to the &#8220;LOLs per second&#8221; app,\nwe noticed time spent calculating if the OS is in Dark Mode or a\nRight-to-Left language:<\/p>\n<pre><code class=\"language-csharp\">1.06s (6.3%) Microsoft.Maui.Essentials!Microsoft.Maui.ApplicationModel.AppInfoImplementation.get_RequestedLayoutDirection()\r\n4.46ms (0.03%) Microsoft.Maui.Essentials!Microsoft.Maui.ApplicationModel.AppInfoImplementation.get_RequestedTheme()<\/code><\/pre>\n<p>This appears to happen for every .NET MAUI <code>View<\/code> on Android, so this\nimpacts startup time, scrolling, etc.<\/p>\n<p>Reviewing the <code>AppInfoImplementation<\/code> class on Android, we could\nsimply use <code>Lazy&lt;T&gt;<\/code> for every value that queries\n<code>Application.Context<\/code>. These values cannot change after the app\nlaunches, so they don&#8217;t need to be computed on every call.<\/p>\n<p>This should also improve subsequent calls to various Maui.Essentials\nAPIs on Android.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/7996\">dotnet\/maui#7996<\/a> for details about this improvement.<\/p>\n<h3>Avoid Creating <code>IView[]<\/code> During Layout<\/h3>\n<p>While profiling customer sample similar to the &#8220;LOLs per second&#8221; app,\ntime was spent ordering views by their <code>ZIndex<\/code> such as:<\/p>\n<pre><code class=\"language-csharp\">3.42s (16%)  microsoft.maui!Microsoft.Maui.Handlers.LayoutHandler.Add(Microsoft.Maui.IView)\r\n1.52s (7.3%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.GetLayoutHandlerIndex()\r\n1.50s (7.2%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.OrderByZIndex(Microsoft.Maui.ILayout)<\/code><\/pre>\n<p>Reviewing the code in <code>OrderByZIndex()<\/code>, it did the following:<\/p>\n<ul>\n<li>Make a <code>record ViewAndIndex(IView View, int Index)<\/code> for each child<\/li>\n<li>Make a <code>ViewAndIndex[]<\/code> the size of the number of children<\/li>\n<li>Call <code>Array.Sort()<\/code><\/li>\n<li>Make a <code>IView[]<\/code> the size of the number of children<\/li>\n<li>Iterate twice over the arrays in the process<\/li>\n<\/ul>\n<p>Then the <code>GetLayoutHandlerIndex()<\/code> method, did all the same work to\ncreate an <code>IView[]<\/code>, get the index, then throws the array away.<\/p>\n<p>To improve this process, we made the following changes:<\/p>\n<ul>\n<li>Removed much of the above code and used a System.Linq <code>OrderBy()<\/code>\nwith &#8220;fast paths&#8221; for 0 or 1 children.<\/li>\n<li>Made an <code>internal<\/code> <code>EnumerateByZIndex()<\/code> method for use by <code>foreach<\/code>\nloops. This avoids creating arrays in those cases.<\/li>\n<\/ul>\n<p>After these changes, the time dropped substantially in <code>dotnet-trace<\/code>\noutput:<\/p>\n<pre><code class=\"language-csharp\">  1.84s (13%)    microsoft.maui!Microsoft.Maui.Handlers.LayoutHandler.Add(Microsoft.Maui.IView)\r\n352.24ms (2.50%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.GetLayoutHandlerIndex()\r\n181.27ms (1.30%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.&lt;&gt;c.&lt;EnumerateByZIndex&gt;b__0_0(Microsoft.Maui.IView)\r\n  2.78ms (0.02%) microsoft.maui!Microsoft.Maui.Handlers.LayoutExtensions.EnumerateByZIndex(Microsoft.Maui.ILayout)<\/code><\/pre>\n<p>We also saw a significant increase in the number of <code>Label<\/code>&#8216;s a second\nin the customer sample. <code>System.Linq<\/code> was used in the improvement,\nwhich goes a little against traditional guidance to avoid it. In this\ncase, the code was implementing its own version of <code>OrderBy()<\/code>, where\nwe could just use <code>OrderBy()<\/code> instead. &#8220;Rolling your own&#8221;\n<code>System.Linq<\/code> is generally not a great idea, unless you have\nbenchmarks showing your implementation is better.<\/p>\n<p>This change should improve layout performance of all .NET MAUI\napplications on any platform.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8250\">dotnet\/maui#8250<\/a> for details about this improvement.<\/p>\n<h3>Defer RTL Layout Calculations to Platform<\/h3>\n<p>When profiling our <a href=\"https:\/\/github.com\/jonathanpeppers\/lols\">LOLs per second<\/a> sample app, a percentage of\ntime is spent calculating if each view is setup to support &#8220;Right to\nLeft&#8221; languages or not:<\/p>\n<pre><code class=\"language-csharp\">290.86ms (1.4%)    microsoft.maui!Microsoft.Maui.Layouts.LayoutExtensions.ShouldArrangeLeftToRight(Microsoft.Maui.IView)<\/code><\/pre>\n<p>In this sample app, we have a nested layout, 5 views deep, with ~100\n<code>Label<\/code>&#8216;s on the screen. Each <code>Label<\/code> we ended up querying the same\nviews for flow direction 500 times.<\/p>\n<p>To solve this issue and many others, the cross-platform implementation\nof RTL was removed from .NET MAUI in favor of native APIs on each\nplatform. This should improve performance and correctness in this\narea.<\/p>\n<p><code>LayoutExtensions.ShouldArrangeLeftToRight()<\/code> is completely removed\nfrom .NET MAUI in .NET 7. This should improve layout performance for\n.NET MAUI on all platforms.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/9558\">dotnet\/maui#9558<\/a> for details about this improvement.<\/p>\n<h3>Further Notes on <code>CollectionView<\/code><\/h3>\n<p>As seen in issue <a href=\"https:\/\/github.com\/dotnet\/maui\/issues\/6317\">dotnet\/maui#6317<\/a>, a <code>CollectionView<\/code>\nwith 10,000 rows contributed to poor startup time and slow scrolling\nperformance.<\/p>\n<p>In this case, the sample app on a Pixel 5 would start in:<\/p>\n<ul>\n<li>.NET 6: 16s412ms<\/li>\n<li>.NET 7: 14s642ms<\/li>\n<\/ul>\n<p>The root cause here appears to be the specific <code>.xaml<\/code> layout:<\/p>\n<pre><code class=\"language-xml\">&lt;ContentPage ...&gt;\r\n    &lt;ScrollView&gt;\r\n        &lt;VerticalStackLayout&gt;\r\n            &lt;CollectionView \r\n                ItemsSource=\"{Binding Bots}\"\r\n                HorizontalOptions=\"FillAndExpand\"\r\n                VerticalOptions=\"FillAndExpand\"&gt;<\/code><\/pre>\n<p>The <code>CollectionView<\/code> is placed inside a <code>VerticalStackLayout<\/code> that\nwill happily expand to an infinite height. Then subsequently placed\ninside a <code>ScrollView<\/code> (which isn&#8217;t necessary because <code>CollectionView<\/code>\nsupports scrolling). This effectively causes the <code>CollectionView<\/code> to\n&#8220;realize&#8221; all 10,000 rows, and voids any virtualization of the\ncontrol.<\/p>\n<p>To solve this issue, we could simply remove the outer views:<\/p>\n<pre><code class=\"language-xml\">&lt;ScrollView&gt;\r\n    &lt;VerticalStackLayout&gt;<\/code><\/pre>\n<p>Resulting with an identical look and feel on screen, and a drastically\nimproved ~822ms (94% shorter!) startup time on a Pixel 5.<\/p>\n<p>If you&#8217;re getting similar behavior in a <code>CollectionView<\/code>, verify that\nthe rows are actually virtualized and not wrapped in unnecessary\nviews. In future .NET releases, we are working on ways to prevent\nsomeone falling into this trap accidentally.<\/p>\n<h2>Startup Performance Improvements<\/h2>\n<h3>Android NDK Compiler Flags<\/h3>\n<p>When we first got .NET MAUI running on early previews of .NET 7, we\nnoticed a regression to both startup &amp; app size:<\/p>\n<ul>\n<li>A ~1.5MB increase in <code>libmonosgen-2.0.so<\/code>, the Android native\nlibrary for the Mono runtime.<\/li>\n<li>A ~250ms increase in startup time<\/li>\n<\/ul>\n<p>In NDK23b, Google decided to no longer pass the <code>-O2<\/code> compiler\noptimization flag for arm64 as part of the Android toolchain. Instead\nthe flag is decided by upstream CMake behavior, see\n<a href=\"https:\/\/github.com\/android\/ndk\/issues\/1536\">android\/ndk#1536<\/a>. Thus we needed to specify the\noptimization flag going forward for the Mono runtime.<\/p>\n<p>We tested three flags to compile <code>libmonosgen-2.0.so<\/code> in a <code>dotnet new maui<\/code> application:<\/p>\n<ul>\n<li><code>-O3<\/code>: 893.7ms<\/li>\n<li><code>-O2<\/code>: 600.2ms<\/li>\n<li><code>-Oz<\/code>: 649.1ms<\/li>\n<\/ul>\n<p>Interestingly, <code>-03<\/code> (the new default!) produced a larger <em>and<\/em> slower\n<code>libmonosgen-2.0.so<\/code>. We settled on <code>-O2<\/code>, which gave us the same\nperformance and size we were getting in .NET 6.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/68354\">dotnet\/runtime#68354<\/a> for details about this improvement.<\/p>\n<h3>DateTimeOffset.Now<\/h3>\n<p>Reviewing a customer&#8217;s .NET 6 Android application, we found a\nsignificant amount of time spent in the very first\n<code>DateTimeOffset.Now<\/code> call. In this case it was accidental (<code>UtcNow<\/code>\ncould simply be used instead), but nevertheless is a common API that\n.NET developers are going to use.<\/p>\n<p>Reviewing <code>dotnet-trace<\/code> output, the first call to\n<code>DateTimeOffset.Now<\/code> took around 277ms on a Pixel 5 device:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/DateTimeOffsetJIT.png\" alt=\"Speedscope of DateTimeOffset with JIT\" \/><\/p>\n<p>Some of the time here was spent in the JIT, so our first idea was to\nrecord a custom AOT profile (using our experimental\n<a href=\"https:\/\/github.com\/jonathanpeppers\/Mono.Profiler.Android#usage-of-the-aot-profiler\">Mono.AotProfiler.Android<\/a> package), and see if this\nwas a good candidate for our standard profile. This got a decent\nimprovement (~161ms), but the number still seemed like it could be\nimproved:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/DateTimeOffsetAOT.png\" alt=\"Speedscope of DateTimeOffset with AOT\" \/><\/p>\n<p>It turns out, the bulk of the time here was spent loading timezone\ndata &#8212; and then calculating the current time from the result.<\/p>\n<p>Instead, we could:<\/p>\n<ol>\n<li>\n<p>Call an Android Java API for the current time offset. Our startup\ncode in the Android workload <em>is<\/em> Java, so it can easily retrieve\nthe value for use by the BCL.<\/p>\n<\/li>\n<li>\n<p>Load timezone data on a background thread. This results in the\noriginal BCL behavior when complete, while using the offset from\nJava in the meantime.<\/p>\n<\/li>\n<\/ol>\n<p>This greatly improved <code>DateTimeOffset.Now<\/code> to a mere ~17.67ms &#8212; even\nwithout adding the code path to an AOT profile.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/74459\">dotnet\/runtime#74459<\/a> and\n<a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7331\">xamarin-android#7331<\/a> for details about this improvement.<\/p>\n<h3>Avoid <code>ColorStateList(int[][],int[])<\/code><\/h3>\n<p><code>dotnet-trace<\/code> output of a .NET 6 <code>dotnet new maui<\/code> application shows\ntime spent creating Android <code>ColorStateList<\/code> objects:<\/p>\n<pre><code class=\"language-csharp\">16.63ms microsoft.maui!Microsoft.Maui.Platform.ColorStateListExtensions.CreateDefault(int)\r\n16.63ms mono.android!Android.Content.Res.ColorStateList..ctor(int[][],int[])\r\n11.38ms mono.android!Android.Runtime.JNIEnv.NewArray<\/code><\/pre>\n<p>Reviewing the C# binding for this type, reveals part of the\nperformance impact of this constructor:<\/p>\n<pre><code class=\"language-csharp\">public unsafe ColorStateList (int[][]? states, int[]? colors)\r\n    : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)\r\n{\r\n    \/\/...\r\n    IntPtr native_states = JNIEnv.NewArray (states);<\/code><\/pre>\n<p>There is a general performance problem in passing C# arrays\n(especially multidimensional ones) into Java. We have to create a Java\narray and copy each array element over to the Java array. We slightly\nimproved upon this scenario in <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6870\">xamarin-android#6870<\/a>, but\nthe array duplication remains a problem.<\/p>\n<p>To solve this, we can design some internal Java APIs for .NET MAUI like:<\/p>\n<pre><code class=\"language-java\">@NonNull\r\npublic static ColorStateList getDefaultColorStateList(int color)\r\n{\r\n    return new ColorStateList(ColorStates.DEFAULT, new int[] { color });\r\n}\r\n\r\n@NonNull\r\npublic static ColorStateList getEditTextColorStateList(int enabled, int disabled)\r\n{\r\n    return new ColorStateList(ColorStates.getEditTextState(), new int[] { enabled, disabled });\r\n}<\/code><\/pre>\n<p>And move any C# <code>const int<\/code> values to be completely in Java:<\/p>\n<pre><code class=\"language-java\">private static class ColorStates\r\n{\r\n    static final int[] EMPTY = new int[] { };\r\n    static final int[][] DEFAULT = new int[][] { EMPTY };\r\n\r\n    private static int[][] editTextState;\r\n\r\n    static int[][] getEditTextState()\r\n    {\r\n        if (editTextState == null) {\r\n            editTextState = new int[][] {\r\n                new int[] {  android.R.attr.state_enabled },\r\n                new int[] { -android.R.attr.state_enabled },\r\n            };\r\n        }\r\n        return editTextState;\r\n    }\r\n}<\/code><\/pre>\n<p>Usage in C# then could look like:<\/p>\n<pre><code class=\"language-csharp\">ColorStateList defaultColors = PlatformInterop.GetDefaultColorStateList(color);\r\nColorStateList editTextColors = PlatformInterop.GetEditTextColorStateList(enabled, disabled);<\/code><\/pre>\n<p>This removes the need for marshaling various <code>int[]<\/code> values to Java,\nand we only pass in a single integer parameter for each state the\nAndroid control supports.<\/p>\n<p>Measuring startup after these changes, we instead get:<\/p>\n<pre><code class=\"language-csharp\">2.44ms microsoft.maui!Microsoft.Maui.Platform.ColorStateListExtensions.CreateDefault(int)<\/code><\/pre>\n<p>Which appears to save ~14ms of startup time on a Pixel 5 in the\n<code>dotnet new maui<\/code> template. Other apps that use <code>Entry<\/code>, <code>CheckBox<\/code>,\n<code>Switch<\/code>, etc. will also see an improvement to startup time.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5654\">dotnet\/maui#5654<\/a> for details about this improvement.<\/p>\n<h3>Improvements to .NET MAUI&#8217;s AOT Profile<\/h3>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/xamarin\/faster-startup-times-with-startup-tracing-on-android\/\">Startup tracing or Profiled AOT<\/a> is a feature of\nXamarin.Android we brought forward to .NET 6+. This is a mechanism for\nAOT&#8217;ing the startup path of applications, which improves launch times\nsignificantly with only a modest app size increase.<\/p>\n<p>In .NET 6, we had already recorded a pretty decent AOT profile for .NET\nMAUI. However, we added a couple more scenarios to the recorded app to\nimprove things further.<\/p>\n<p>First, we added flyout content to the profile, such as:<\/p>\n<pre><code class=\"language-xml\">&lt;Shell ...&gt;\r\n    &lt;FlyoutItem Title=\"Flyout Item\"&gt;\r\n        &lt;ShellContent\r\n            Title=\"Page 1\"\r\n            ContentTemplate=\"{DataTemplate local:MainPage}\" \/&gt;\r\n        &lt;ShellContent\r\n            Title=\"Page 2\"\r\n            ContentTemplate=\"{DataTemplate local:MainPage}\" \/&gt;\r\n    &lt;\/FlyoutItem&gt;\r\n&lt;\/Shell&gt;<\/code><\/pre>\n<p>This simple change improved the startup of the <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast sample\napp<\/a> by around 25ms.<\/p>\n<p>Secondly, we added a scenario of a non-Shell .NET MAUI app using\n<code>FlyoutPage<\/code>. Shell is the default navigation pattern in .NET MAUI,\nbut developers familiar with Xamarin.Forms may want to use APIs such\nas <code>FlyoutPage<\/code>, <code>NavigationPage<\/code>, and <code>TabPage<\/code>.<\/p>\n<p>So for example, <code>AppFlyoutPage.xaml<\/code>:<\/p>\n<pre><code class=\"language-xml\">&lt;FlyoutPage ...&gt;\r\n    &lt;FlyoutPage.Detail&gt;\r\n        &lt;local:Tabs Title=\"Detail\"&gt;&lt;\/local:Tabs&gt;\r\n    &lt;\/FlyoutPage.Detail&gt;\r\n    &lt;FlyoutPage.Flyout&gt;\r\n        &lt;ContentPage Title=\"Flyout\"&gt;\r\n            &lt;VerticalStackLayout&gt;\r\n                &lt;Label Text=\"Flyout Item 1\"&gt;&lt;\/Label&gt;\r\n                &lt;Label Text=\"Flyout Item 2\"&gt;&lt;\/Label&gt;\r\n                &lt;Label Text=\"Flyout Item 3\"&gt;&lt;\/Label&gt;\r\n                &lt;Label Text=\"Flyout Item 4\"&gt;&lt;\/Label&gt;\r\n            &lt;\/VerticalStackLayout&gt;\r\n        &lt;\/ContentPage&gt;\r\n    &lt;\/FlyoutPage.Flyout&gt;\r\n&lt;\/FlyoutPage&gt;<\/code><\/pre>\n<p>Then we can simply replace <code>MainPage<\/code> during the AOT profile recording:<\/p>\n<pre><code class=\"language-csharp\">App.Current.MainPage = new AppFlyoutPage();<\/code><\/pre>\n<p>This improved startup of a template project using <code>FlyoutPage<\/code> by\nabout 12ms. To go even further with your .NET MAUI application, it is\npossible to record a completely custom AOT profile for your app. See\nour experimental <a href=\"https:\/\/github.com\/jonathanpeppers\/Mono.Profiler.Android#usage-of-the-aot-profiler\">Mono.AotProfiler.Android<\/a> package\nfor details.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8939\">dotnet\/maui#8939<\/a> and <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8941\">dotnet\/maui#8941<\/a>\nfor details about these improvements.<\/p>\n<h3>Better String Comparisons in Java Interop<\/h3>\n<p><code>dotnet-trace<\/code> output of a .NET MAUI application revealed:<\/p>\n<pre><code class=\"language-csharp\">21.28ms (0.45%) java.interop!Java.Interop.JniRuntime.JniTypeManager.AssertSimpleReference(string,string)<\/code><\/pre>\n<p><em>Note that changes to <a href=\"#android-ndk-compiler-flags\">Android NDK Compiler Flags<\/a> may have also contributed to the above timing.<\/em><\/p>\n<p>Reviewing the method, it is mostly assertions around the string syntax\nof <code>jniSimpleReference<\/code>:<\/p>\n<pre><code class=\"language-csharp\">internal static void AssertSimpleReference (string jniSimpleReference, string argumentName = \"jniSimpleReference\")\r\n{\r\n    if (jniSimpleReference == null)\r\n        throw new ArgumentNullException (argumentName);\r\n    if (jniSimpleReference != null &amp;&amp; jniSimpleReference.IndexOf (\".\", StringComparison.Ordinal) &gt;= 0)\r\n        throw new ArgumentException (\"JNI type names do not contain '.', they use '\/'. Are you sure you're using a JNI type name?\", argumentName);\r\n    if (jniSimpleReference != null &amp;&amp; jniSimpleReference.StartsWith (\"[\", StringComparison.Ordinal))\r\n        throw new ArgumentException (\"Arrays cannot be present in simplified type references.\", argumentName);\r\n    if (jniSimpleReference != null &amp;&amp; jniSimpleReference.StartsWith (\"L\", StringComparison.Ordinal) &amp;&amp; jniSimpleReference.EndsWith (\";\", StringComparison.Ordinal))\r\n        throw new ArgumentException (\"JNI type references are not supported.\", argumentName);\r\n}<\/code><\/pre>\n<p>These assertions are needed, as there is now a way to &#8220;remap&#8221;\nunderlying Java type names in .NET 7 as implemented in\n<a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/936\">xamarin\/java.interop#936<\/a>. This was also compounded by how\nmany times this method is called in a typical .NET MAUI application on\nAndroid.<\/p>\n<p>However, we can simply improve the method by using the <code>char<\/code> overload\nof <code>string.IndexOf()<\/code> and the indexer instead of <code>string.StartsWith()<\/code>:<\/p>\n<pre><code class=\"language-csharp\">internal static void AssertSimpleReference (string jniSimpleReference, string argumentName = \"jniSimpleReference\")\r\n{\r\n    if (string.IsNullOrEmpty (jniSimpleReference))\r\n        throw new ArgumentNullException (argumentName);\r\n    if (jniSimpleReference.IndexOf ('.') &gt;= 0)\r\n        throw new ArgumentException (\"JNI type names do not contain '.', they use '\/'. Are you sure you're using a JNI type name?\", argumentName);\r\n    switch (jniSimpleReference [0]) {\r\n        case '[':\r\n            throw new ArgumentException (\"Arrays cannot be present in simplified type references.\", argumentName);\r\n        case 'L':\r\n            if (jniSimpleReference [jniSimpleReference.Length - 1] == ';')\r\n                throw new ArgumentException (\"JNI type references are not supported.\", argumentName);\r\n            break;\r\n        default:\r\n            break;\r\n    }\r\n}<\/code><\/pre>\n<p>This resulted in a much smaller time when reviewing these code changes\nin <code>dotnet-trace<\/code>:<\/p>\n<pre><code class=\"language-csharp\">1.21ms java.interop!Java.Interop.JniRuntime.JniTypeManager.AssertSimpleReference(string,string)<\/code><\/pre>\n<p>We found other code in <code>Java.Interop.JniTypeSignature<\/code>&#8216;s constructor\nthat validates the string syntax of Java type names, so we made\nsimilar changes in both places.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1001\">xamarin\/java.interop#1001<\/a> and\n<a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1002\">xamarin\/java.interop#1002<\/a> for details about these\nimprovements.<\/p>\n<h3>XAML Compilation Improvements<\/h3>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/maui\/xaml\/xamlc\">XAML Compilation<\/a> (or XamlC) in .NET MAUI is on by default,\ntransforming your <code>.xaml<\/code> files directly to IL at build time.<\/p>\n<p>To illustrate this, take a couple <code>&lt;Image\/&gt;<\/code> elements:<\/p>\n<pre><code class=\"language-xml\">&lt;Image Source=\"https:\/\/picsum.photos\/200\/300\" \/&gt;\r\n&lt;Image Source=\"foo.png\" \/&gt;<\/code><\/pre>\n<p>In a <code>Debug<\/code> build, the <code>.xaml<\/code> is validated to give good error\nmessages about mistakes, but not written to disk. In a <code>Release<\/code>\nbuild, however, this <code>.xaml<\/code> is emitted directly into your .NET\nassembly as IL.<\/p>\n<p>In .NET 6, the above would <code>.xaml<\/code> would compile to the equivalent of:<\/p>\n<pre><code class=\"language-csharp\">var image1 = new Image();\r\nimage1.Source = new ImageSourceConverter().ConvertFromInvariantString(\"foo.png\");\r\nvar image2 = new Image();\r\nimage2.Source = new ImageSourceConverter().ConvertFromInvariantString(\"https:\/\/picsum.photos\/200\/300\");<\/code><\/pre>\n<p>.NET MAUI&#8217;s type converters take <code>string<\/code> values at runtime and\nconvert them into the appropriate type. These are not as performant as\nif you created plain C# objects directly. Even the <code>dotnet new maui<\/code>\ntemplate with a single <code>&lt;Image\/&gt;<\/code> shows an impact of the above code:<\/p>\n<pre><code class=\"language-csharp\">1.17ms (&lt;0.01%) microsoft.maui.controls!Microsoft.Maui.Controls.ImageSourceConverter.ConvertFrom()<\/code><\/pre>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/maui\/xaml\/xamlc\">XamlC<\/a> has a concept of <em>compiled<\/em> type converters, when\nimplemented for <code>ImageSource<\/code> generates better code:<\/p>\n<pre><code class=\"language-csharp\">var image1 = new Image();\r\nimage1.Source = ImageSource.FromUri(new Uri(\"foo.png\", UriKind.Absolute));\r\nvar image2 = new Image();\r\nimage2.Source = ImageSource.FromFile(\"https:\/\/picsum.photos\/200\/300\");<\/code><\/pre>\n<p>This treatment was applied for every type converter currently used at\nruntime in the <code>dotnet new maui<\/code> template:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8843\"><code>ImageSource<\/code><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8849\"><code>StrokeShape<\/code><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8567\"><code>Brush<\/code><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/8599\"><code>Point<\/code><\/a><\/li>\n<\/ul>\n<p>This results in better\/faster generated IL from <code>.xaml<\/code> files on all\nplatforms .NET MAUI supports.<\/p>\n<h3><code>ReadyToRun<\/code> by Default on Windows<\/h3>\n<p>When the .NET Fundamentals team implemented .NET MAUI startup tracking\nfor Windows, we found a huge &#8220;easy win&#8221; to improve startup.\n<a href=\"https:\/\/docs.microsoft.com\/dotnet\/core\/deploying\/ready-to-run\"><code>ReadyToRun<\/code><\/a> (or R2R) is not enabled by default in .NET\n6 MAUI applications on Windows. R2R is form of ahead-of-time (AOT)\ncompilation that can be used on platforms running the CoreCLR runtime\n(such as Windows).<\/p>\n<p>Simply adding <code>-p:PublishReadyToRun=true<\/code> resulted in a huge\nimprovement in our graphs:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/WindowsReadyToRun.png\" alt=\"Windows .NET MAUI Startup with ReadyToRun\" \/><\/p>\n<p><em>Note that this graph is running the .NET MAUI application on an Azure DevOps hosted pool, so the times here are a bit worse than what we see on desktop PCs or laptops.<\/em><\/p>\n<p>This made us consider making <code>PublishReadyToRun<\/code> default for <code>Release<\/code>\nbuilds on Windows. .NET MAUI is built on top of <a href=\"https:\/\/learn.microsoft.com\/windows\/apps\/winui\/winui3\/\"><code>WinUI3<\/code> and the\nWindows App SDK<\/a> to leverage the latest UI framework and APIs\nfor Windows. <code>WinUI3<\/code> templates already set <code>PublishReadyToRun<\/code> by\ndefault for <code>Release<\/code> mode, so this was a &#8220;no-brainer&#8221; to bring to .NET\nMAUI by default.<\/p>\n<p>Note that <code>PublishReadyToRun<\/code> does increase application size, so if\nthis is undesirable, you can simply set <code>PublishReadyToRun<\/code> to <code>false<\/code>\nin your project file.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/9357\">dotnet\/maui#9357<\/a> for details about this improvement.<\/p>\n<h3>Dual Architectures by Default on macOS<\/h3>\n<p>A customer reported this behavior in their .NET MAUI application\nrunning on macOS:<\/p>\n<ol>\n<li>Launching the app after first install &#8212; over four seconds.<\/li>\n<li>Subsequent launches are under one second.<\/li>\n<li>A reboot causes the next launch of the application to be slow again.<\/li>\n<\/ol>\n<p>It turns out, the application was built for <code>x86_64<\/code> and running on an\nM1 (<code>arm64<\/code>) MacBook. This behavior is what you would see when\n<a href=\"https:\/\/developer.apple.com\/documentation\/apple-silicon\/about-the-rosetta-translation-environment\"><code>Rosetta<\/code><\/a>, Apple&#8217;s translation technology for Apple silicon\nprocessors, is in effect:<\/p>\n<blockquote>\n<p>To the user, Rosetta is mostly transparent. If an executable\ncontains only Intel instructions, macOS automatically launches\nRosetta and begins the translation process. When translation\nfinishes, the system launches the translated executable in place of\nthe original. However, the translation process takes time, so users\nmight perceive that translated apps launch or run more slowly at\ntimes.<\/p>\n<\/blockquote>\n<p>We could avoid <a href=\"https:\/\/developer.apple.com\/documentation\/apple-silicon\/about-the-rosetta-translation-environment\"><code>Rosetta<\/code><\/a> translation behavior by building\nthe application for multiple architectures:<\/p>\n<pre><code class=\"language-xml\">&lt;RuntimeIdentifiers Condition=\" $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'MacCatalyst' \"&gt;maccatalyst-x64;maccatalyst-arm64&lt;\/RuntimeIdentifiers&gt;<\/code><\/pre>\n<p>The <code>MacCatalyst<\/code> workload, in this case, builds for both\narchitectures, merging both binaries into the final app.<\/p>\n<p>This led us to default to building two architectures in <code>Release<\/code> mode\nfor both <code>net7.0-maccatalyst<\/code> and <code>net7.0-macos<\/code> applications. In most\ncases, developers will want a dual-architecture application for the\nApp Store. If this is undesirable, you can define a single\n<code>$(RuntimeIdentifier)<\/code> in your project file to opt out.<\/p>\n<p>To verify what architecture your .NET MAUI application is built for,\nyou can run:<\/p>\n<pre><code class=\"language-bash\">file \/Applications\/YourApp.app\/Contents\/MacOS\/YourApp\r\n\/Applications\/YourApp.app\/Contents\/MacOS\/YourApp: Mach-O 64-bit executable x86_64<\/code><\/pre>\n<p>Where a dual-architecture binary would result with:<\/p>\n<pre><code class=\"language-bash\">file \/Applications\/YourApp.app\/Contents\/MacOS\/YourApp\r\n\/Applications\/YourApp.app\/Contents\/MacOS\/YourApp: Mach-O universal binary with 2 architectures:\r\n- Mach-O 64-bit executable x86_64\r\n- Mach-O 64-bit executable arm64\r\n\/Applications\/YourApp.app\/Contents\/MacOS\/YourApp (for architecture x86_64):    Mach-O 64-bit executable x86_64\r\n\/Applications\/YourApp.app\/Contents\/MacOS\/YourApp (for architecture arm64):    Mach-O 64-bit executable arm64<\/code><\/pre>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/15769\">xamarin-macios#15769<\/a> for details about this improvement.<\/p>\n<h3>Notes about <code>RegexOptions.Compiled<\/code><\/h3>\n<p>As seen in issue <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/71007\">dotnet\/runtime#71007<\/a>, a customer&#8217;s app\nwas spending ~258ms creating a <code>Regex<\/code> object in the .NET 6 version of\ntheir app, that was significantly higher than the previous\nXamarin.Android counterpart:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/RegexOptionsCompiled.png\" alt=\"dotnet-trace output for RegexOptions.Compiled\" \/><\/p>\n<p>This showed up as a regression from Xamarin.Android to .NET 6, because\n<code>RegexOptions.Compiled<\/code> was not implemented at all in the BCL from\nmono\/mono. While <code>RegexOptions.Compiled<\/code> <em>is<\/em> implemented in the BCL\nfrom .NET Core (and .NET 6+). The new behavior is that time is spent\ncalling <code>System.Reflection.Emit<\/code> to generate code at runtime to make\nfuture <code>Regex<\/code> calls faster.<\/p>\n<p>In this example, the <code>Regex<\/code> was only used once, so the setting was\nnot actually needed at all. Avoid using <code>RegexOptions.Compiled<\/code>,\nunless you really <em>are<\/em> using the <code>Regex<\/code> many times, and are willing\nto pay the startup cost for improved throughput.<\/p>\n<p>However! We have an <em>even better<\/em> solution in .NET 7, as described in\n<a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/regular-expression-improvements-in-dotnet-7\/\">Regular Expression Improvements in .NET 7<\/a>.<\/p>\n<p>So for example, take the following usage of <code>RegexOptions.Compiled<\/code>:<\/p>\n<pre><code class=\"language-csharp\">private static readonly Regex s_myCoolRegex = new Regex(\"abc|def\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n...\r\nif (s_myCoolRegex.IsMatch(text)) { ... }<\/code><\/pre>\n<p>We can rewrite this in .NET 7 using <code>[RegexGenerator]<\/code>, such as:<\/p>\n<pre><code class=\"language-csharp\">[RegexGenerator(\"abc|def\", RegexOptions.IgnoreCase)]\r\nprivate static partial Regex MyCoolRegex();\r\n...\r\nif (MyCoolRegex().IsMatch(text)) { ... }<\/code><\/pre>\n<p>Testing the new <code>Regex<\/code> implementation in a <a href=\"https:\/\/github.com\/jonathanpeppers\/AndroidRegex\">sample Android\napp<\/a>, shows a noticeable improvement to startup time:<\/p>\n<pre><code class=\"language-diff\">--Average(ms): 307.9\r\n--Std Err(ms): 2.38723456930585\r\n--Std Dev(ms): 7.54909854809757\r\n++Average(ms): 214.4\r\n++Std Err(ms): 3.06666666666667\r\n++Std Dev(ms): 9.69765149118303<\/code><\/pre>\n<p>An average of 10 runs on a Pixel 5, shows a total ~93.5ms savings to\nstartup just by using the new <code>Regex<\/code> APIs.<\/p>\n<p>Diving deeper into <code>dotnet-trace<\/code> output, we can see <code>RegexOptions.Compiled<\/code> taking:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/RegexCCtorBefore.png\" alt=\"RegexOptions.Compiled Static Constructor\" \/>\n<img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/RegexIsMatchBefore.png\" alt=\"RegexOptions.Compiled IsMatch\" \/><\/p>\n<p><code>[RegexGenerator]<\/code> is now so fast that the static constructor of\n<code>MainActivity<\/code> completely disappears from the trace. We are merely\nleft with:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/RegexIsMatchAfter.png\" alt=\"RegexGenerator IsMatch\" \/><\/p>\n<p>There is even a built-in refactoring inside of Visual Studio to\nquickly make this change in your own applications:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/RegexRefactoring.png\" alt=\"Regex Refactoring\" \/><\/p>\n<h3>Improvements to Mono&#8217;s Interpreter<\/h3>\n<p>In .NET MAUI, the Mono runtime&#8217;s interpreter is used in two main scenarios:<\/p>\n<ul>\n<li>\n<p>C# Hot Reload: when debugging (or <code>Debug<\/code> configurations), the Mono\ninterpreter is used by default.<\/p>\n<\/li>\n<li>\n<p>iOS (and other Apple platforms) where JIT is restricted: the\ninterpreter enables APIs like <code>System.Reflection.Emit<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>In .NET 7, Mono&#8217;s interpreter has gained &#8220;tiered compilation&#8221;. Where\nstartup is greatly improved in methods that are only called once (or\nvery few times). When a certain threshold of calls occurs, an\noptimization pass takes place on the intermediate representation (IR)\nof the code. This lets the interpreter achieve fast performance in\nfrequently called code-paths in your applications.<\/p>\n<p>We can see the direct result of these changes in a Blazor WASM\napplication:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/10\/MonoInterpreter.png\" alt=\"Performance of Interpreter Changes\" \/><\/p>\n<p>However, we are also able to see a decent improvement in .NET MAUI\nscenarios. For example, take the following .NET MAUI applications\nrunning on a Pixel 5 device with solely the interpreter enabled:<\/p>\n<table>\n<thead>\n<tr>\n<th>Application<\/th>\n<th>Version<\/th>\n<th style=\"text-align: right\">Startup Time(ms)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>dotnet new maui<\/td>\n<td>.NET 6<\/td>\n<td style=\"text-align: right\">978.0<\/td>\n<\/tr>\n<tr>\n<td>dotnet new maui<\/td>\n<td>.NET 7<\/td>\n<td style=\"text-align: right\">775.3<\/td>\n<\/tr>\n<tr>\n<td>.NET Podcast<\/td>\n<td>.NET 6<\/td>\n<td style=\"text-align: right\">1171.1<\/td>\n<\/tr>\n<tr>\n<td>.NET Podcast<\/td>\n<td>.NET 7<\/td>\n<td style=\"text-align: right\">972.9<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>With these changes, you can expect .NET MAUI applications on iOS &amp; Mac\nto have improved startup performance for the interpreter. We can also\nexpect &#8220;inner development loop&#8221; improvements for Debugging and Hot\nReload scenarios.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/68823\">dotnet\/runtime#68823<\/a> for details about this\nimprovement.<\/p>\n<h2>App Size Improvements<\/h2>\n<h3>Fix <code>DebuggerSupport<\/code> Trimmer Value for Android<\/h3>\n<p>Reviewing .NET trimmer output for .NET 6 Android applications, we\nfound methods decorated with attributes such as:<\/p>\n<ul>\n<li><code>System.Diagnostics.DebuggableAttribute<\/code><\/li>\n<li><code>System.Diagnostics.DebuggerBrowsableAttribute<\/code><\/li>\n<li><code>System.Diagnostics.DebuggerDisplayAttribute<\/code><\/li>\n<li><code>System.Diagnostics.DebuggerHiddenAttribute<\/code><\/li>\n<li><code>System.Diagnostics.DebuggerStepThroughAttribute<\/code><\/li>\n<li><code>System.Diagnostics.DebuggerVisualizerAttribute<\/code><\/li>\n<\/ul>\n<p>Were not being <em>removed<\/em> in <code>Release<\/code> builds, attributing to larger\napplication sizes.<\/p>\n<p>We discovered the underlying cause was an incorrect value for the\n<code>$(DebuggerSupport)<\/code> trimmer flag, after fixing this, a &#8220;Hello World&#8221;\nAndroid application saw the following improvements:<\/p>\n<pre><code class=\"language-diff\">--Java.Interop.dll             59419 bytes\r\n++Java.Interop.dll             58642 bytes\r\n--Mono.Android.dll             89327 bytes\r\n++Mono.Android.dll             87877 bytes\r\n--System.Private.CoreLib.dll  532428 bytes\r\n++System.Private.CoreLib.dll  472318 bytes\r\n--Overall package:           2738143 bytes\r\n++Overall package:           2676703 bytes<\/code><\/pre>\n<p>This change will improve application size of all .NET 7 Android\napplications.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7176\">xamarin-android#7176<\/a> for details about this\nimprovement.<\/p>\n<h3>R8 Java Code Shrinker Improvements<\/h3>\n<p><a href=\"https:\/\/r8.googlesource.com\/r8\/\">R8<\/a> is whole-program optimization,\nshrinking and minification tool that converts java byte code to\noptimized Android dex code. <code>R8<\/code> can be too aggressive, and remove\nsomething that is called by Java reflection, etc. We don&#8217;t yet have a\ngood approach for making this the default across all .NET Android\napplications.<\/p>\n<p>However, in .NET 7, we now pass any included <code>proguard.txt<\/code> files from\nAndroid <code>.aar<\/code> libraries to <code>R8<\/code>. AndroidX libraries that are\nimplicitly used by .NET MAUI will use the ProGuard rules provided by\nGoogle. This should result in less problems when enabling R8 in .NET 7\nAndroid applications.<\/p>\n<p>To opt into using <code>R8<\/code> for <code>Release<\/code> builds, add the following to your\n<code>.csproj<\/code>:<\/p>\n<pre><code class=\"language-xml\">&lt;!-- NOTE: not recommended for Debug builds! --&gt;\r\n&lt;AndroidLinkTool Condition=\"'$(Configuration)' == 'Release'\"&gt;r8&lt;\/AndroidLinkTool&gt;<\/code><\/pre>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/5310\">xamarin-android#5310<\/a> for details about this\nimprovement.<\/p>\n<h3>Feature to Exclude Kotlin-related Files<\/h3>\n<p>Recent changes to Google&#8217;s AndroidX libraries are bringing more and\nmore APIs implemented in Kotlin instead of Java. One result of this\nchange, is additional &#8220;stuff&#8221; added to Android <code>.apk<\/code> files.<\/p>\n<p>In the case of a <code>dotnet new maui<\/code> app:<\/p>\n<ul>\n<li><code>kotlin<\/code> directory: 386,289 bytes uncompressed, 154,519 bytes compressed<\/li>\n<li><code>DebugProbesKt.bin<\/code>:  1,719 bytes uncompressed,     777 bytes compressed<\/li>\n<\/ul>\n<p>These files are general metadata from Kotlin, and are not used or\nneeded in .NET Android applications. The <code>kotlin<\/code> folder has files\nwith names such as <code>kotlin\/Error.kotlin_metadata<\/code>.<\/p>\n<p>We added a new MSBuild item group to exclude custom files from the\nfinal Android application. By default we include the following exclusions:<\/p>\n<pre><code class=\"language-xml\">&lt;ItemGroup&gt;\r\n  &lt;AndroidPackagingOptionsExclude Include=\"DebugProbesKt.bin\" \/&gt;\r\n  &lt;AndroidPackagingOptionsExclude Include=\"$([MSBuild]::Escape('*.kotlin_*')\" \/&gt;\r\n&lt;\/ItemGroup&gt;<\/code><\/pre>\n<p>This removes the extra files from any .NET 7+ Android application. It\nalso gives developers the flexibility to remove other files in the\nfuture if needed.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7356\">xamarin-android#7356<\/a> for details about this improvement.<\/p>\n<h3>Improve AOT Output for Generics<\/h3>\n<p>To improve the generated AOT code size of .NET 7 iOS applications, the\nfollowing cases are now implemented:<\/p>\n<ul>\n<li>\n<p>If a generic method or a method of a generic type has all type\nparameters constrained to value types &#8211; methods for reference types\nwill not be generated.<\/p>\n<\/li>\n<li>\n<p>If a generic method or a method of a generic type has all type\nparameters constrained to reference types &#8211; methods for value types\nwill not be generated.<\/p>\n<\/li>\n<\/ul>\n<p>In both cases, we were generating AOT code for methods that were not\npossible to be called. This is impactful for a large assembly like\n<code>System.Private.CoreLib.dll<\/code>.<\/p>\n<p>The results of this change in a &#8220;hello world&#8221; iOS application:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\">File<\/th>\n<th style=\"text-align: right\">Before<\/th>\n<th style=\"text-align: right\">After<\/th>\n<th style=\"text-align: right\">diff<\/th>\n<th style=\"text-align: right\">%<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">HelloiOS<\/td>\n<td style=\"text-align: right\">19539824<\/td>\n<td style=\"text-align: right\">19158672<\/td>\n<td style=\"text-align: right\">-381152<\/td>\n<td style=\"text-align: right\">-1,95%<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">System.Private.CoreLib.dll-llvm.o<\/td>\n<td style=\"text-align: right\">6870572<\/td>\n<td style=\"text-align: right\">6734416<\/td>\n<td style=\"text-align: right\">-136156<\/td>\n<td style=\"text-align: right\">-1,98%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/70838\">dotnet\/runtime#70838<\/a> for details about this\nimprovement.<\/p>\n<h2>Tooling and Documentation<\/h2>\n<h3>Profiling .NET MAUI Apps<\/h3>\n<p>The way to profile your .NET MAUI application varies depending on the\nplatform. Mobile platforms leverage <code>dotnet-trace<\/code> and\n<code>dotnet-dsrouter<\/code>, while Windows desktop applications have access to\n<a href=\"https:\/\/github.com\/microsoft\/perfview\">PerfView<\/a>.<\/p>\n<p>To gather instructions for various platforms, we&#8217;ve aggregated docs for:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/wiki\/Profiling-.NET-MAUI-Apps\">.NET MAUI, in general<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/blob\/main\/Documentation\/guides\/tracing.md\">Android<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/wiki\/Profiling\">iOS, macOS, and MacCatalyst<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/wiki\/Profiling-.NET-MAUI-Apps#windows--perfview\">Windows<\/a><\/li>\n<\/ul>\n<p>This should assist in profiling your own code &#8212; the same way we would\nprofile to optimize .NET MAUI itself.<\/p>\n<h3>Measuring Startup Time<\/h3>\n<p>Just like profiling, the way to measure a .NET MAUI application&#8217;s\nstartup time is drastically different on each platform.<\/p>\n<p>We&#8217;ve collected guidance such as:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/wiki\/Profiling-.NET-MAUI-Apps#measuring-startup-times\">.NET MAUI, in general<\/a><\/li>\n<li><a href=\"https:\/\/developer.android.com\/topic\/performance\/vitals\/launch-time\">Google&#8217;s Android Documentation<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/blob\/main\/Documentation\/guides\/profiling.md#overall-startup\">.NET Android Startup Documentation<\/a><\/li>\n<li><a href=\"https:\/\/developer.apple.com\/documentation\/xcode\/reducing-your-app-s-launch-time\">Apple&#8217;s iOS Documentation<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/wiki\/Profiling-App-Launch\">.NET iOS Startup Documentation<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/wiki\/Profiling-.NET-MAUI-Apps#windows\">Windows\/.NET MAUI<\/a><\/li>\n<\/ul>\n<p>Additionally, we have a <a href=\"https:\/\/github.com\/jonathanpeppers\/measure-startup\"><code>measure-startup<\/code><\/a> tool for\nmeasuring startup for desktop .NET MAUI applications on Windows or macOS.<\/p>\n<p>This tool can be built from source such as:<\/p>\n<pre><code class=\"language-bash\">git clone https:\/\/github.com\/jonathanpeppers\/measure-startup.git\r\ncd measure-startup\r\ndotnet run -- dotnet help<\/code><\/pre>\n<p>Everything after <code>--<\/code> for <code>dotnet run<\/code> are arguments passed to\n<code>measure-startup<\/code>, where this example times how long the <code>dotnet<\/code>\ncommand launches and takes to print the text <code>help<\/code>. This example\nisn&#8217;t that useful, but can be easily applied to a .NET MAUI\napplication.<\/p>\n<p>To measure a .NET MAUI app, first add a subscription to your main\n<code>Page<\/code>&#8216;s <code>Loaded<\/code> event:<\/p>\n<pre><code class=\"language-csharp\">Loaded += (sender, e) =&gt; Dispatcher.Dispatch(() =&gt; Console.WriteLine(\"loaded\"));<\/code><\/pre>\n<p><code>Dispatcher<\/code> is used to give the app one pass for rendering\/layout. In\nan app using <code>BlazorWebView<\/code>, you might consider logging this message\nwhen the web view finishes loading.<\/p>\n<p>On Windows, build the app for <code>Release<\/code> mode, such as:<\/p>\n<pre><code class=\"language-dotnetcli\">dotnet publish -f net7.0-windows10.0.19041.0 -c Release<\/code><\/pre>\n<p>You can then measure startup time via:<\/p>\n<pre><code class=\"language-dotnetcli\">dotnet run -c Release -- bin\\Release\\net7.0-windows10.0.19041.0\\win10-x64\\publish\\YourApp.exe loaded<\/code><\/pre>\n<p>This launches <code>YourApp.exe<\/code> recording the time it takes for <code>loaded<\/code>\nto be printed to stdout:<\/p>\n<pre><code class=\"language-md\">0:00:07.0961628\r\nDropping first run...\r\n0:00:01.4743315\r\n0:00:01.4700848\r\n0:00:01.4834235\r\n0:00:01.4752893\r\n0:00:01.4695317\r\nAverage(ms): 1474.53216\r\nStd Err(ms): 2.4945246400065977\r\nStd Dev(ms): 5.577926666602944<\/code><\/pre>\n<p>This also works for .NET MAUI applications running on macOS:<\/p>\n<pre><code class=\"language-dotnetcli\">dotnet publish -f net7.0-maccatalyst -c Release\r\ndotnet run -- bin\/Release\/net7.0-maccatalyst\/YourApp.app\/Contents\/MacOS\/YourApp loaded<\/code><\/pre>\n<p>If the <a href=\"https:\/\/github.com\/jonathanpeppers\/measure-startup\"><code>measure-startup<\/code><\/a> tool is useful for you,\nlet us know. We can release it as a .NET global tool on NuGet.org if\nthere is enough interest.<\/p>\n<h3>App Size Reporting Tools<\/h3>\n<p>To assist with diagnosing app size, we have two tools for Android and\niOS: <a href=\"https:\/\/github.com\/radekdoulik\/apkdiff\"><code>apkdiff<\/code><\/a> and <a href=\"https:\/\/github.com\/rolfbjarne\/dotnet-appsize-report\"><code>dotnet-appsize-report<\/code><\/a>.<\/p>\n<p><a href=\"https:\/\/github.com\/radekdoulik\/apkdiff\"><code>apkdiff<\/code><\/a> can compare sizes between two Android <code>.apk<\/code>\nfiles and report what changed.<\/p>\n<p>To simulate what will be delivered to a user&#8217;s device from Google Play\n(which now uses <a href=\"https:\/\/developer.android.com\/guide\/app-bundle\">Android App Bundles<\/a>), build an <code>.apk<\/code>\nfor a single architecture:<\/p>\n<pre><code class=\"language-dotnetcli\">dotnet build -c Release -f net7.0-android -r android-arm64 -p:AndroidPackageFormat=apk<\/code><\/pre>\n<p>Google Play delivers a device-specific <code>.apk<\/code> to users&#8217; devices, and\nso we can mimic this behavior by building for a single runtime\nidentifier: <code>-r android-arm64<\/code>.<\/p>\n<p>You can compare two <code>.apk<\/code> files, using <code>apkdiff<\/code> such as:<\/p>\n<pre><code class=\"language-dotnetcli\">dotnet tool install --global apkdiff\r\n...\r\napkdiff -f before.apk after.apk\r\nSize difference in bytes ([*1] apk1 only, [*2] apk2 only):\r\n+           8 lib\/arm64-v8a\/libaot-Microsoft.Maui.Controls.Xaml.dll.so\r\n-         608 lib\/arm64-v8a\/libaot-Microsoft.Maui.Controls.Compatibility.dll.so\r\n-       3,448 lib\/arm64-v8a\/libaot-Microsoft.Maui.Controls.dll.so\r\n-       4,424 lib\/arm64-v8a\/libaot-Microsoft.Maui.dll.so\r\n-       5,656 lib\/arm64-v8a\/libaot-Microsoft.NetConf2021.Maui.dll.so\r\n-     129,617 assemblies\/assemblies.blob\r\nSummary:\r\n-     129,617 Other entries -1.03% (of 12,605,899)\r\n+           0 Dalvik executables 0.00% (of 11,756,376)\r\n-      14,128 Shared libraries -0.10% (of 14,849,096)\r\n-     131,072 Package size difference -0.64% (of 20,380,049)<\/code><\/pre>\n<p>Likewise for iOS, we have <a href=\"https:\/\/github.com\/rolfbjarne\/dotnet-appsize-report\"><code>dotnet-appsize-report<\/code><\/a>:<\/p>\n<pre><code class=\"language-dotnetcli\">git clone https:\/\/github.com\/rolfbjarne\/dotnet-appsize-report.git\r\ncd dotnet-appsize-report\r\ndotnet run -- -i path\/to\/bin\/Release\/net7.0-ios\/YourApp.app<\/code><\/pre>\n<p>Which can give reports for:<\/p>\n<pre><code class=\"language-dotnetcli\">App size report for YourApp.app\r\n\r\n1) View 167 assemblies\r\n2) View 381 namespaces\r\n3) View 26592 types\r\nq) Quit<\/code><\/pre>\n<p>And show .NET assemblies, namespaces, or types sorted by IL size. For\nexample:<\/p>\n<pre><code class=\"language-md\">Assembly                                            Types    IL Size         File size\r\nSystem.Private.CoreLib                              2,157  1,126,514   3,605,144 bytes\r\nSystem.Private.Xml                                  1,667  1,378,583   3,099,800 bytes\r\nMicrosoft.iOS                                      14,978  5,086,414  24,121,232 bytes<\/code><\/pre>\n<h3>Experimental or Advanced Options<\/h3>\n<p>Just as in .NET 6, many of the same experimental or advanced options\nare still available in .NET 7:<\/p>\n<ul>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-dotnet-maui\/#r8-java-code-shrinker\">R8 Java Code Shrinker<\/a><\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-dotnet-maui\/#aot-everything\">AOT Everything<\/a><\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-dotnet-maui\/#aot-and-llvm\">AOT and LLVM<\/a><\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-dotnet-maui\/#record-a-custom-aot-profile\">Record a Custom AOT Profile<\/a><\/li>\n<\/ul>\n<p>In future .NET releases, we will work toward making some of these\nsettings the default &#8212; where it best makes sense.<\/p>\n<h2>Conclusion<\/h2>\n<p>We hope that our continued investment in performance in .NET MAUI will\nbe helpful for your own cross-platform desktop and mobile\napplications. <em>Always be profiling!<\/em><\/p>\n<p>Please try out .NET MAUI, file issues, or learn more at\n<a href=\"https:\/\/dot.net\/maui\">http:\/\/dot.net\/maui<\/a>!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>What improvements did we bring to .NET MAUI in .NET 7? Click to find out more!<\/p>\n","protected":false},"author":1345,"featured_media":43055,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7233,3009],"tags":[7611,7238,108],"class_list":["post-42998","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-maui","category-performance","tag-dotnet-7","tag-net-maui","tag-performance"],"acf":[],"blog_post_summary":"<p>What improvements did we bring to .NET MAUI in .NET 7? Click to find out more!<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/42998","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/1345"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=42998"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/42998\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/43055"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=42998"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=42998"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=42998"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}