{"id":48594,"date":"2023-10-31T10:05:00","date_gmt":"2023-10-31T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=48594"},"modified":"2023-10-30T15:04:02","modified_gmt":"2023-10-30T22:04:02","slug":"dotnet-8-performance-improvements-in-dotnet-maui","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-8-performance-improvements-in-dotnet-maui\/","title":{"rendered":".NET 8 Performance Improvements in .NET MAUI"},"content":{"rendered":"<p>The major focus for .NET MAUI in the .NET 8 release is <em>quality<\/em>. As such, alot\nof our focus has been fixing bugs instead of chasing lofty performance goals. In\n.NET 8, we merged 1,559 pull requests that closed 596 total issues. These\ninclude changes from the .NET MAUI team as well as the .NET MAUI community. We\nare optimistic that this should result in a significant increase in quality in\n.NET 8.<\/p>\n<p>However! We still have plenty of performance changes to showcase. Building upon\nthe fundamental <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-8\/\">performance improvements in .NET 8<\/a> we discover\n&#8220;low-hanging&#8221; fruit constantly, and there were high-voted performance issues on\nGitHub we tried to tackle. Our goal is to continue to make .NET MAUI faster in\neach release, read on for details!<\/p>\n<p>For a review of the performance improvements in past releases, see our posts for\n.NET 6 and 7. This also gives you an idea of the improvements you would see\nmigrating from Xamarin.Forms to .NET MAUI:<\/p>\n<ul>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-7-performance-improvements-in-dotnet-maui\/\">.NET 7 Performance Improvements in .NET MAUI<\/a><\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-dotnet-maui\/\">.NET 6 Performance Improvements in .NET MAUI<\/a><\/li>\n<\/ul>\n<h2>Table Of Contents<\/h2>\n<p>New features<\/p>\n<ul>\n<li><a href=\"#androidstripilafteraot\"><code>AndroidStripILAfterAOT<\/code><\/a><\/li>\n<li><a href=\"#androidenablemarshalmethods\"><code>AndroidEnableMarshalMethods<\/code><\/a><\/li>\n<li><a href=\"#nativeaot-on-ios\">NativeAOT on iOS<\/a><\/li>\n<\/ul>\n<p>Build &amp; Inner Loop Performance<\/p>\n<ul>\n<li><a href=\"#filter-android-ps--a-output-with-grep\">Filter Android <code>ps -A<\/code> output with <code>grep<\/code><\/a><\/li>\n<li><a href=\"#port-windowsappsdk-usage-of-vcmetadll-to-c\">Port WindowsAppSDK usage of <code>vcmeta.dll<\/code> to C#<\/a><\/li>\n<li><a href=\"#improvements-to-remote-ios-builds-on-windows\">Improvements to remote iOS builds on Windows<\/a><\/li>\n<li><a href=\"#improvements-to-android-inner-loop\">Improvements to Android inner-loop<\/a><\/li>\n<li><a href=\"#xaml-compilation-no-longer-uses-loadinseparateappdomain\">XAML Compilation no longer uses <code>LoadInSeparateAppDomain<\/code><\/a><\/li>\n<\/ul>\n<p>Performance or App Size Improvements<\/p>\n<ul>\n<li><a href=\"#structs-and-iequatable-in-net-maui\">Structs and <code>IEquatable<\/code> in .NET MAUI<\/a><\/li>\n<li><a href=\"#fix-performance-issue-in-appthemebinding\">Fix performance issue in <code>{AppThemeBinding}<\/code><\/a><\/li>\n<li><a href=\"#address-ca1307-and-ca1309-for-performance\">Address <code>CA1307<\/code> and <code>CA1309<\/code> for performance<\/a><\/li>\n<li><a href=\"#address-ca1311-for-performance\">Address <code>CA1311<\/code> for performance<\/a><\/li>\n<li><a href=\"#remove-unused-viewattachedtowindow-event-on-android\">Remove unused <code>ViewAttachedToWindow<\/code> event on Android<\/a><\/li>\n<li><a href=\"#remove-unneeded-systemreflection-for-binding\">Remove unneeded <code>System.Reflection<\/code> for <code>{Binding}<\/code><\/a><\/li>\n<li><a href=\"#use-stringcomparerordinal-for-dictionary-and-hashset\">Use <code>StringComparer.Ordinal<\/code> for <code>Dictionary<\/code> and <code>HashSet<\/code><\/a><\/li>\n<li><a href=\"#reduce-java-interop-in-mauidrawable-on-android\">Reduce Java interop in <code>MauiDrawable<\/code> on Android<\/a><\/li>\n<li><a href=\"#improve-layout-performance-of-label-on-android\">Improve layout performance of <code>Label<\/code> on Android<\/a><\/li>\n<li><a href=\"#reduce-java-interop-calls-for-controls-in-net-maui\">Reduce Java interop calls for controls in .NET MAUI<\/a><\/li>\n<li><a href=\"#improve-performance-of-entrymaxlength-on-android\">Improve performance of <code>Entry.MaxLength<\/code> on Android<\/a><\/li>\n<li><a href=\"#improve-memory-usage-of-collectionview-on-windows\">Improve memory usage of <code>CollectionView<\/code> on Windows<\/a><\/li>\n<li><a href=\"#use-unmanagedcallersonlyattribute-on-apple-platforms\">Use <code>UnmanagedCallersOnlyAttribute<\/code> on Apple platforms<\/a><\/li>\n<li><a href=\"#faster-java-interop-for-strings-on-android\">Faster Java interop for strings on Android<\/a><\/li>\n<li><a href=\"#faster-java-interop-for-c-events-on-android\">Faster Java interop for C# events on Android<\/a><\/li>\n<li><a href=\"#use-function-pointers-for-jni\">Use Function Pointers for JNI<\/a><\/li>\n<li><a href=\"#removed-xamarinandroidxlegacysupportv4\">Removed <code>Xamarin.AndroidX.Legacy.Support.V4<\/code><\/a><\/li>\n<li><a href=\"#deduplication-of-generics-on-ios-and-macos\">Deduplication of generics on iOS and macOS<\/a><\/li>\n<li><a href=\"#fix-systemlinqexpressions-implementation-on-ios-like-platforms\">Fix <code>System.Linq.Expressions<\/code> implementation on iOS-like platforms<\/a><\/li>\n<li><a href=\"#set-dynamiccodesupportfalse-for-ios-and-catalyst\">Set <code>DynamicCodeSupport=false<\/code> for iOS and Catalyst<\/a><\/li>\n<\/ul>\n<p>Memory Leaks<\/p>\n<ul>\n<li><a href=\"#memory-leaks-and-quality\">Memory Leaks and Quality<\/a><\/li>\n<li><a href=\"#diagnosing-leaks-in-net-maui\">Diagnosing leaks in .NET MAUI<\/a><\/li>\n<li><a href=\"#patterns-that-cause-leaks-c-events\">Patterns that cause leaks: C# events<\/a><\/li>\n<li><a href=\"#circular-references-on-apple-platforms\">Circular references on Apple platforms<\/a><\/li>\n<li><a href=\"#roslyn-analyzer-for-apple-platforms\">Roslyn analyzer for Apple platforms<\/a><\/li>\n<\/ul>\n<p>Tooling and Documentation<\/p>\n<ul>\n<li><a href=\"#simplified-dotnet-trace-and-dotnet-dsrouter\">Simplified <code>dotnet-trace<\/code> and <code>dotnet-dsrouter<\/code><\/a><\/li>\n<li><a href=\"#dotnet-gcdump-support-for-mobile\"><code>dotnet-gcdump<\/code> Support for Mobile<\/a><\/li>\n<\/ul>\n<h2>New Features<\/h2>\n<h3><code>AndroidStripILAfterAOT<\/code><\/h3>\n<p>Once Upon A Time\u2122 we had a brilliant thought: if AOT pre-compiles C# methods, do\nwe need the managed method anymore?  Removing the C# method body would allow\nassemblies to be smaller. .NET iOS applications already do this, so why not\nAndroid as well?<\/p>\n<p>While the idea is straightforward, implementation was not: iOS uses <a href=\"https:\/\/www.mono-project.com\/docs\/advanced\/aot\/#full-aot\">&#8220;Full&#8221;\nAOT<\/a>, which AOT&#8217;s <em>all<\/em> methods into a form that doesn&#8217;t require a\nruntime JIT. This allowed iOS to run <a href=\"https:\/\/github.com\/mono\/mono\/tree\/2020-02\/mcs\/tools\/cil-strip\"><code>cil-strip<\/code><\/a>, removing all\nmethod bodies from all managed types.<\/p>\n<p>At the time, Xamarin.Android only supported &#8220;normal&#8221; AOT, and normal AOT\nrequires a JIT for certain constructs such as generic types and generic methods.\nThis meant that attempting to run <code>cil-strip<\/code> would result in runtime errors if\na method body was removed that was actually required at runtime. This was\nparticularly bad because <code>cil-strip<\/code> could only remove <em>all<\/em> method bodies!<\/p>\n<p>We are re-intoducing IL stripping for .NET 8. Add a new\n<code>$(AndroidStripILAfterAOT)<\/code> MSBuild property. When true, the\n<code>&lt;MonoAOTCompiler\/&gt;<\/code> task will track which method bodies were actually AOT&#8217;d,\nstoring this information into <code>%(_MonoAOTCompiledAssemblies.MethodTokenFile)<\/code>,\nand the new <code>&lt;ILStrip\/&gt;<\/code> task will update the input assemblies, removing all\nmethod bodies that can be removed.<\/p>\n<p>By default enabling <code>$(AndroidStripILAfterAOT)<\/code> will <em>override<\/em> the default\n<code>$(AndroidEnableProfiledAot)<\/code> setting, allowing all trimmable AOT&#8217;d methods to\nbe removed. This choice was made because <code>$(AndroidStripILAfterAOT)<\/code> is <em>most<\/em>\nuseful when AOT-compiling your entire application. Profiled AOT and IL stripping\ncan be used together by explicitly setting both within the <code>.csproj<\/code>, but with\nthe only benefit being a small <code>.apk<\/code> size improvement:<\/p>\n<pre><code class=\"language-xml\">&lt;PropertyGroup Condition=\" '$(Configuration)' == 'Release' \"&gt;\r\n    &lt;AndroidStripILAfterAOT&gt;true&lt;\/AndroidStripILAfterAOT&gt;\r\n    &lt;AndroidEnableProfiledAot&gt;true&lt;\/AndroidEnableProfiledAot&gt;\r\n&lt;\/PropertyGroup&gt;<\/code><\/pre>\n<p><code>.apk<\/code> size results for a <code>dotnet new android<\/code> app:<\/p>\n<table>\n<thead>\n<tr>\n<th><code>$(AndroidStripILAfterAOT)<\/code><\/th>\n<th><code>$(AndroidEnableProfiledAot)<\/code><\/th>\n<th><code>.apk<\/code> size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>true<\/td>\n<td>true<\/td>\n<td>7.7MB<\/td>\n<\/tr>\n<tr>\n<td>true<\/td>\n<td>false<\/td>\n<td>8.1MB<\/td>\n<\/tr>\n<tr>\n<td>false<\/td>\n<td>true<\/td>\n<td>7.7MB<\/td>\n<\/tr>\n<tr>\n<td>false<\/td>\n<td>false<\/td>\n<td>8.4MB<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Note that <code>AndroidStripILAfterAOT=false<\/code> and <code>AndroidEnableProfiledAot=true<\/code> is\nthe <em>default<\/em> Release configuration environment, for 7.7MB.<\/p>\n<p>A project that <em>only<\/em> sets <code>AndroidStripILAfterAOT=true<\/code> implicitly sets\n<code>AndroidEnableProfiledAot=false<\/code>, resulting in an 8.1MB app.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/8172\">xamarin-android#8172<\/a> and\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/86722\">dotnet\/runtime#86722<\/a> for details about this feature.<\/p>\n<h3><code>AndroidEnableMarshalMethods<\/code><\/h3>\n<p>.NET 8 introduces a new experimental setting for <code>Release<\/code> configurations:<\/p>\n<pre><code class=\"language-xml\">&lt;PropertyGroup Condition=\" '$(Configuration)' == 'Release' \"&gt;\r\n    &lt;AndroidEnableMarshalMethods&gt;true&lt;\/AndroidEnableMarshalMethods&gt;\r\n    &lt;!-- Note that single-architecture apps will be most successful --&gt;\r\n    &lt;RuntimeIdentifier&gt;android-arm64&lt;\/RuntimeIdentifier&gt;\r\n&lt;\/PropertyGroup&gt;<\/code><\/pre>\n<p>We hope to enable this feature by default in .NET 9, but for now we are\nproviding the setting as an opt-in, experimental feature. Applications that only\ntarget one architecture, such as <code>RuntimeIdentifier=android-arm64<\/code>, will likely\nbe able to enable this feature without issue.<\/p>\n<h4>Background on Marshal Methods<\/h4>\n<p>A JNI marshal method is a <a href=\"https:\/\/docs.oracle.com\/javase\/8\/docs\/technotes\/guides\/jni\/spec\/design.html#native_method_arguments\">JNI-callable<\/a> function pointer provided\nto <a href=\"https:\/\/docs.oracle.com\/javase\/8\/docs\/technotes\/guides\/jni\/spec\/functions.html#RegisterNatives\"><code>JNIEnv::RegisterNatives()<\/code><\/a>. Currently, JNI marshal\nmethods are provided via the interaction between code we generate and\n<code>JNINativeWrapper.CreateDelegate()<\/code>:<\/p>\n<ul>\n<li>\n<p>Our code-generator emits the &#8220;actual&#8221; JNI-callable method.<\/p>\n<\/li>\n<li>\n<p><code>JNINativeWrapper.CreateDelegate()<\/code> uses System.Reflection.Emit to <em>wrap<\/em> the\nmethod for exception marshaling.<\/p>\n<\/li>\n<\/ul>\n<p>JNI marshal methods are needed for all Java-to-C# transitions.<\/p>\n<p>Consider the virtual <code>Activity.OnCreate()<\/code> method:<\/p>\n<pre><code class=\"language-csharp\">partial class Activity {\r\n    static Delegate? cb_onCreate_Landroid_os_Bundle_;\r\n    static Delegate GetOnCreate_Landroid_os_Bundle_Handler ()\r\n    {\r\n        if (cb_onCreate_Landroid_os_Bundle_ == null)\r\n            cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_);\r\n        return cb_onCreate_Landroid_os_Bundle_;\r\n    }\r\n\r\n    static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)\r\n    {\r\n        var __this = global::Java.Lang.Object.GetObject&lt;Android.App.Activity&gt; (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;\r\n        var savedInstanceState = global::Java.Lang.Object.GetObject&lt;Android.OS.Bundle&gt; (native_savedInstanceState, JniHandleOwnership.DoNotTransfer);\r\n        __this.OnCreate (savedInstanceState);\r\n    }\r\n\r\n    \/\/ Metadata.xml XPath method reference: path=\"\/api\/package[@name='android.app']\/class[@name='Activity']\/method[@name='onCreate' and count(parameter)=1 and parameter[1][@type='android.os.Bundle']]\"\r\n    [Register (\"onCreate\", \"(Landroid\/os\/Bundle;)V\", \"GetOnCreate_Landroid_os_Bundle_Handler\")]\r\n    protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) =&gt; ...\r\n}<\/code><\/pre>\n<p><code>Activity.n_OnCreate_Landroid_os_Bundle_()<\/code> is the JNI marshal method,\nresponsible for marshaling parameters from JNI values into C# types, forwarding\nthe method invocation to <code>Activity.OnCreate()<\/code>, and (if necessary) marshaling\nthe return value back to JNI.<\/p>\n<p><code>Activity.GetOnCreate_Landroid_os_Bundle_Handler()<\/code> is part of the type\nregistration infrastructure, providing a <code>Delegate<\/code> instance to\n<code>RegisterNativeMembers .RegisterNativeMembers()<\/code>, which is eventually passed to\n<code>JNIEnv::RegisterNatives()<\/code>.<\/p>\n<p>While this works, it&#8217;s not incredibly performant: unless using one of the\noptimized delegate types added in <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6657\">xamarin-android#6657<\/a>,\nSystem.Reflection.Emit is used to create a wrapper around the marshal method,\nwhich is something we&#8217;ve wanted to avoid doing for years.<\/p>\n<p>Thus, the idea: since we&#8217;re <em>already<\/em> bundling a native toolchain and using\nLLVM-IR to produce <code>libxamarin-app.so<\/code>, what if we emitted Java native method\nnames and <em>skipped<\/em> all the done as part of <code>Runtime.register()<\/code> and\n<code>JNIEnv.RegisterJniNatives()<\/code>?<\/p>\n<p>Given:<\/p>\n<pre><code class=\"language-csharp\">class MyActivity : Activity {\r\n    protected override void OnCreate(Bundle? state) =&gt; ...\r\n}<\/code><\/pre>\n<p>During the build, <code>libxamarin-app.so<\/code> would contain the function:<\/p>\n<pre><code class=\"language-c\">JNIEXPORT void JNICALL\r\nJava_crc..._MyActivity_n_1onCreate (JNIEnv *env, jobject self, jobject state);<\/code><\/pre>\n<p>During App runtime, the <code>Runtime.register()<\/code> invocation present in <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/wiki\/Blueprint#java-type-registration\">Java\nCallable Wrappers<\/a> would either be omitted or would be a\nno-op, and Android\/JNI would instead resolve <code>MyActivity.n_onCreate()<\/code> as\n<code>Java_crc..._MyActivity_n_1onCreate()<\/code>.<\/p>\n<p>We call this effort &#8220;LLVM Marshal Methods&#8221;, which is currently experimental in\n.NET 8. Many of the specifics are still being investigated, and this feature\nwill be spread across various areas.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7351\">xamarin-android#7351<\/a> for details about this\nexperimental feature.<\/p>\n<h3>NativeAOT on iOS<\/h3>\n<p>In .NET 7, we started an experiment to see what it would take to support\nNativeAOT on iOS. Going from prototype to an initial implementation: .NET 8\nPreview 6 included NativeAOT as an experimental feature for iOS.<\/p>\n<p>To opt into NativeAOT in a MAUI iOS project, use the following settings in your\nproject file:<\/p>\n<pre><code class=\"language-xml\">&lt;PropertyGroup Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios' and '$(Configuration)' == 'Release'\"&gt;\r\n    &lt;!-- PublishAot=true indicates NativeAOT, while omitting this property would use Mono's AOT --&gt;\r\n    &lt;PublishAot&gt;true&lt;\/PublishAot&gt;\r\n&lt;\/PropertyGroup&gt;<\/code><\/pre>\n<p>Then to build the application for an iOS device:<\/p>\n<pre><code class=\"language-bash\">$ dotnet publish -f net8.0-ios -r ios-arm64\r\nMSBuild version 17.8.0+6cdef4241 for .NET\r\n...\r\nBuild succeeded.\r\n    0 Error(s)<\/code><\/pre>\n<blockquote>\n<p><strong>Note<\/strong>\nWe may consider unifying and improving MSBuild property names for this feature\nin future .NET releases. To do a one-off build at the command-line you may\nalso need to specify <code>-p:PublishAotUsingRuntimePack=true<\/code> in addition to\n<code>-p:PublishAot=true<\/code>.<\/p>\n<\/blockquote>\n<p>One of the main culprits for the first release was how the iOS workload supports\nObjective-C interoperability. The problem was mainly related to the type\nregistration system which is the key component for efficiently supporting\niOS-like platforms (<a href=\"https:\/\/learn.microsoft.com\/xamarin\/ios\/internals\/registrar\">see docs for details<\/a>). In its implementation,\nthe type registration system depends on type metadata tokens which are not\navailable with NativeAOT. Therefore, in order to leverage the benefits of highly\nefficient NativeAOT runtime, we had to adapt.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/80912\">dotnet\/runtime#80912<\/a> includes the discussion around how\nto tackle this problem, and finally in\n<a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/18268\">xamarin-macios#18268<\/a> we implemented a new managed static\nregistrar that works with NativeAOT. The new managed static registrar does not\njust benefit us with being compatible with NativeAOT, but is also much faster\nthan the default one, and is available for all supported runtimes (<a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/blob\/net8.0\/docs\/managed-static-registrar.md#managed-static-registrar\">see docs for\ndetails<\/a>).<\/p>\n<p>Along the way, we had a great help from our GH community and their contribution\n(code reviews, PRs) was essential to helps us move forward quickly and deliver\nthis feature on time. A few from many PR&#8217;s that helped and unblocked us on our\njourney were:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/77956\">dotnet\/runtime#77956<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/78280\">dotnet\/runtime#78280<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/82317\">dotnet\/runtime#82317<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/85996\">dotnet\/runtime#85996<\/a><\/li>\n<\/ul>\n<p>and the list goes on&#8230;<\/p>\n<p>As .NET 8 Preview 6 came along, we finally managed to release our first version\nof the NativeAOT on iOS which also supports MAUI. See <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-dotnet-8-preview-6\/#support-for-targeting-ios-platforms-with-nativeaot\">the blog post on .NET 8\nPreview 6<\/a> for details about what we were able to accomplish in the\ninitial release.<\/p>\n<p>In subsequent .NET 8 releases, results improved quite a bit, as we were\nidentifying and resolving issues along the way. The graph below shows the .NET\nMAUI iOS template app size comparison throughout the preview releases:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/nativeaot-ios-app-size.png\" alt=\".NET MAUI iOS app - size comparison\" \/><\/p>\n<p>We had steady progress and estimated size savings reported, due to fixing the\nfollowing issues:<\/p>\n<ul>\n<li>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/87924\">dotnet\/runtime#87924<\/a> &#8211; fixed major NativeAOT size\nissue with AOT-incompatible code paths in <code>System.Linq.Expressions<\/code> and also\nmade fully NativeAOT compatible when targeting iOS<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/18332\">xamarin-macios#18332<\/a> &#8211; reduced the size of <code>__LINKEDIT Export Info<\/code> section in stripped binaries<\/p>\n<\/li>\n<\/ul>\n<p>Furthermore, in the latest RC 1 release the app size went even further down\nreaching -50% smaller apps for the template .NET MAUI iOS applications compared\nto Mono. Most impactful issues\/PRs that contributed to this:<\/p>\n<ul>\n<li>\n<p><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/18734\">xamarin-macios#18734<\/a> &#8211; Make <code>Full<\/code> the default link\nmode for NativeAOT<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/issues\/18584\">xamarin-macios#18584<\/a> &#8211; Make the codebase trimming\ncompatible through a series of PRs.<\/p>\n<\/li>\n<\/ul>\n<p>Even though app size was our primary metric to focus on, for the RC 1 release, we\nalso measured startup time performance comparisons for a .NET MAUI iOS template\napp comparing NativeAOT and Mono where NativeAOT results with almost 2x faster\nstartup time.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/nativeaot-ios-startup.png\" alt=\".NET MAUI iOS app - startup time\" \/><\/p>\n<h4>Key Takeaways<\/h4>\n<p>For NativeAOT scenarios on iOS, changing the default link mode to <code>Full<\/code>\n(<a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/18734\">xamarin-macios#18734<\/a>) is probably the biggest\nimprovement for application size. But at the same time, this change can also\nbreak applications which are not fully AOT and trim-compatible. In <code>Full<\/code> link\nmode, the trimmer might trim away AOT incompatible code paths (think about\nreflection usage) which are accessed dynamically at runtime. <code>Full<\/code> link mode is\nnot the default configuration when using the Mono runtime, so it is possible\nthat some applications are not fully AOT-compatible.<\/p>\n<p>Supporting NativeAOT on iOS is an experimental feature and still a\nwork-in-progress, and our plan is to address the potential issues with <code>Full<\/code>\nlink mode incrementally:<\/p>\n<ul>\n<li>\n<p>As a first step, we enabled trim, AOT, and single-file warnings by default in\n<a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/issues\/18571\">xamarin-macios#18571<\/a>. The enabled warnings should make\nour customers aware at build-time, whether a use of a certain framework or a\nlibrary, or some C# constructs in their code, is incompatible with NativeAOT &#8211;\nand could crash at runtime. This information should guide our customers to write\nAOT-compatible code, but also to help us improve our frameworks and libraries\nwith the same goal of fully utilising the benefits of AOT compilation.<\/p>\n<\/li>\n<li>\n<p>The second step, was clearing up all the warnings coming from <code>Microsoft.iOS<\/code>\nand <code>System.Private.CoreLib<\/code> assemblies reported for a template iOS application\nwith: <a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/issues\/18629\">xamarin-macios#18629<\/a> and\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/91520\">dotnet\/runtime#91520<\/a>.<\/p>\n<\/li>\n<li>\n<p>In future releases, we plan to address the warnings coming from the MAUI\nframework and further improve the overall user-experience. Our goal is to have\nfully AOT and trim-compatible frameworks.<\/p>\n<\/li>\n<\/ul>\n<p>.NET 8 will support targeting iOS platforms with NativeAOT as an opt-in feature\nand shows great potential by generating up to 50% smaller and 50% faster startup\ncompared to Mono. Considering the great performance that NativeAOT promises,\nplease help us on this journey and try out your applications with NativeAOT and\nreport any potential issues. At the same time, let us know when NativeAOT &#8220;just\nworks&#8221; out-of-the-box.<\/p>\n<p>To follow future progress, see <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/80905\">dotnet\/runtime#80905<\/a>.\nLast but not least, we would like to thank our GH contributors, who are helping\nus make NativeAOT on iOS possible.<\/p>\n<h2>Build &amp; Inner Loop Performance<\/h2>\n<h3>Filter Android <code>ps -A<\/code> output with <code>grep<\/code><\/h3>\n<p>When profiling the Android inner loop for a .NET MAUI project with\n<a href=\"https:\/\/github.com\/microsoft\/perfview\">PerfView<\/a> we found around <code>1.2%<\/code> of CPU time was spent just trying to\nget the process ID of the running Android application.<\/p>\n<p>When changing <code>Tools &gt; Options &gt; Xamarin &gt; Xamarin Diagnostics output<\/code> verbosity\nto be <code>Diagnostics<\/code>, you could see:<\/p>\n<pre><code class=\"language-log\">-- Start GetProcessId - 12\/02\/2022 11:05:57 (96.9929ms) --\r\n[INPUT] ps -A\r\n[OUTPUT]\r\nUSER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME\r\nroot             1     0 10943736  4288 0                   0 S init\r\nroot             2     0       0      0 0                   0 S [kthreadd]\r\n... Hundreds of more lines!\r\nu0_a993      14500  1340 14910808 250404 0                  0 R com.companyname.mauiapp42\r\n-- End GetProcessId --<\/code><\/pre>\n<p>The Xamarin\/.NET MAUI extension in Visual Studio polls every second to see if\nthe application has exited. This is useful for changing the play\/stop button\nstate if you force close the app, etc.<\/p>\n<p>Testing on a Pixel 5, we could see the command is actually 762 lines of output!<\/p>\n<pre><code class=\"language-powershell\">&gt; (adb shell ps -A).Count\r\n762<\/code><\/pre>\n<p>What we could do instead is something like:<\/p>\n<pre><code class=\"language-powershell\">&gt; adb shell \"ps -A | grep -w -E 'PID|com.companyname.mauiapp42'\"<\/code><\/pre>\n<p>Where we pipe the output of <code>ps -A<\/code> to the <code>grep<\/code> command on the Android device.\nYes, Android has a subset of unix commands available! We filter on either a line\ncontaining <code>PID<\/code> or your application&#8217;s package name.<\/p>\n<p>The result is now the IDE is only parsing 4 lines:<\/p>\n<pre><code class=\"language-log\">[INPUT] ps -A | grep -w -E 'PID|com.companyname.mauiapp42'\r\n[OUTPUT]\r\nUSER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME\r\nu0_a993      12856  1340 15020476 272724 0                  0 S com.companyname.mauiapp42<\/code><\/pre>\n<p>This not only improves memory used to split and parse this information in C#,\nbut <code>adb<\/code> is also transmitting way less bytes across your USB cable or virtually\nfrom an emulator.<\/p>\n<p>This feature shipped in recent versions of Visual Studio 2022, improving this\nscenario for all Xamarin and .NET MAUI customers.<\/p>\n<h3>Port WindowsAppSDK usage of <code>vcmeta.dll<\/code> to C#<\/h3>\n<p>We found that every incremental build of a .NET MAUI project running on Windows\nspent time in:<\/p>\n<pre><code class=\"language-log\">Top 10 most expensive tasks\r\nCompileXaml = 3.972 s\r\n... various tasks ...<\/code><\/pre>\n<p>This is the XAML compiler for WindowsAppSDK, that compiles the WinUI3 flavor of\nXAML (not .NET MAUI XAML). There is very little XAML of this type in .NET MAUI\nprojects, in fact, the only file is <code>Platforms\/Windows\/App.xaml<\/code> in the project\ntemplate.<\/p>\n<p>Interestingly, if you installed the <code>Desktop development with C++<\/code> workload in\nthe Visual Studio installer, this time just completely went away!<\/p>\n<pre><code class=\"language-log\">Top 10 most expensive tasks\r\n... various tasks ...\r\nCompileXaml = 9 ms<\/code><\/pre>\n<p>The WindowsAppSDK XAML compiler p\/invokes into a native library from the C++\nworkload, <code>vcmeta.dll<\/code>, to calculate a hash for .NET assembly files. This is\nused to make incremental builds fast &#8212; if the hash changes, compile the XAML\nagain. If <code>vcmeta.dll<\/code> was not found on disk, the XAML compiler was effectively\n&#8220;recompiling everything&#8221; on every incremental build.<\/p>\n<p>For an initial fix, we simply included a small part of the C++ workload as a\ndependency of .NET MAUI in Visual Studio. The slightly larger install size was a\ngood tradeoff for saving upwards of 4 seconds in incremental build time.<\/p>\n<p>Next, we implemented <code>vcmeta.dll<\/code>&#8216;s hashing functionality in plain C# with\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.reflection.metadata\"><code>System.Reflection.Metadata<\/code><\/a> to compute indentical hash values as before.\nNot only was this implementation better, in that we could drop a dependency on\nthe C++ workload, but it was also faster! The time to compute a single hash:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right\">Mean<\/th>\n<th style=\"text-align: right\">Error<\/th>\n<th style=\"text-align: right\">StdDev<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Native<\/td>\n<td style=\"text-align: right\">217.31 us<\/td>\n<td style=\"text-align: right\">1.704 us<\/td>\n<td style=\"text-align: right\">1.594 us<\/td>\n<\/tr>\n<tr>\n<td>Managed<\/td>\n<td style=\"text-align: right\">86.43 us<\/td>\n<td style=\"text-align: right\">1.700 us<\/td>\n<td style=\"text-align: right\">2.210 us<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Some of the reasons this was faster:<\/p>\n<ul>\n<li>\n<p>No p\/invoke or COM-interfaces involved.<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.reflection.metadata\"><code>System.Reflection.Metadata<\/code><\/a> has a fast struct-based API, perfect for\niterating over types in a .NET assembly and computing a hash value.<\/p>\n<\/li>\n<\/ul>\n<p>The end result being that <code>CompileXaml<\/code> might actually be even faster than 9ms\nin incremental builds.<\/p>\n<p>This feature shipped in WindowsAppSDK 1.3, which is now used by .NET MAUI in\n.NET 8. See <a href=\"https:\/\/github.com\/microsoft\/WindowsAppSDK\/issues\/3128\">WindowsAppSDK#3128<\/a> for details about this improvement.<\/p>\n<h3>Improvements to remote iOS builds on Windows<\/h3>\n<p>Comparing inner loop performance for iOS, there was a considerable gap between\ndoing &#8220;remote iOS&#8221; development on Windows versus doing everything locally on\nmacOS. Many small improvements were made, based on comparing inner-loop\n<a href=\"https:\/\/aka.ms\/binlog\"><code>.binlog<\/code><\/a> files recorded on macOS versus one recorded inside Visual\nStudio on Windows.<\/p>\n<p>Some examples include:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/12747\">maui#12747<\/a>: don&#8217;t explicitly copy files to the build server<\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/16752\">xamarin-macios#16752<\/a>: do not copy files to build server for a <code>Delete<\/code> operation<\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/16929\">xamarin-macios#16929<\/a>: batch file deletion via <code>DeleteFilesAsync<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/17033\">xamarin-macios#17033<\/a>: cache AOT compiler path<\/li>\n<li>Xamarin\/MAUI Visual Studio extension: when running <code>dotnet-install.sh<\/code> on\nremote build hosts, set the explicit processor flag for M1 Macs.<\/li>\n<\/ul>\n<p>We also made some improvements for <em>all<\/em> iOS &amp; MacCatalyst projects, such as:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/16416\">xamarin-macios#16416<\/a>: don&#8217;t process assemblies over and over again<\/li>\n<\/ul>\n<h3>Improvements to Android inner-loop<\/h3>\n<p>We also made many small improvements to the &#8220;inner-loop&#8221; on Android &#8212; most of\nwhich were focused in a specific area.<\/p>\n<p>Previously, Xamarin.Forms projects had the luxury of being organized into\nmultiple projects, such as:<\/p>\n<ul>\n<li><code>YourApp.Android.csproj<\/code>: Xamarin.Android application project<\/li>\n<li><code>YourApp.iOS.csproj<\/code>: Xamarin.iOS application project<\/li>\n<li><code>YourApp.csproj<\/code>: <code>netstandard2.0<\/code> class library<\/li>\n<\/ul>\n<p>Where almost <em>all<\/em> of the logic for a Xamarin.Forms app was contained in the\n<code>netstandard2.0<\/code> project. Nearly all the incremental builds would be changes to\nXAML or C# in the class library. This structure enabled the Xamarin.Android\nMSBuild targets to completely skip many Android-specific MSBuild steps. In .NET\nMAUI, the &#8220;single project&#8221; feature means that every incremental build <em>has<\/em> to\nrun these Android-specific build steps.<\/p>\n<p>In focusing specifically improving this area, we made many small changes, such\nas:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1061\">java-interop#1061<\/a>: avoid <code>string.Format()<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1064\">java-interop#1064<\/a>: improve <code>ToJniNameFromAttributesForAndroid<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1065\">java-interop#1065<\/a>: avoid <code>File.Exists()<\/code> checks<\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1069\">java-interop#1069<\/a>: fix more places to use <code>TypeDefinitionCache<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1072\">java-interop#1072<\/a>: use less <code>System.Linq<\/code> for custom attributes<\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1103\">java-interop#1103<\/a>: use <code>MemoryMappedFile<\/code> when using <code>Mono.Cecil<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7621\">xamarin-android#7621<\/a>: avoid <code>File.Exists()<\/code> checks<\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7626\">xamarin-android#7626<\/a>: perf improvements for <code>LlvmIrGenerator<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7652\">xamarin-android#7652<\/a>: fast path for <code>&lt;CheckClientHandlerType\/&gt;<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7653\">xamarin-android#7653<\/a>: delay <code>ToJniName<\/code> when generating <code>AndroidManifest.xml<\/code><\/li>\n<li><a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/7686\">xamarin-android#7686<\/a>: lazily populate <code>Resource<\/code> lookup<\/li>\n<\/ul>\n<p>These changes should improve incremental builds in all .NET 8 Android project\ntypes.<\/p>\n<h3>XAML Compilation no longer uses <code>LoadInSeparateAppDomain<\/code><\/h3>\n<p>Looking at the JITStats report in <a href=\"https:\/\/github.com\/microsoft\/perfview\">PerfView<\/a> (for <code>MSBuild.exe<\/code>):<\/p>\n<table>\n<thead>\n<tr>\n<th>Name<\/th>\n<th style=\"text-align: right\">JitTime (ms)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Microsoft.Maui.Controls.Build.Tasks.dll<\/td>\n<td style=\"text-align: right\">214.0<\/td>\n<\/tr>\n<tr>\n<td>Mono.Cecil<\/td>\n<td style=\"text-align: right\">119.0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>It appears that <code>Microsoft.Maui.Controls.Build.Tasks.dll<\/code> was spending a lot of\ntime in the JIT. What was confusing, is this was an incremental build where\neverything should already be loaded. The JIT&#8217;s work should be done already?<\/p>\n<p>The cause appears to be usage of the\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.build.framework.loadinseparateappdomainattribute\"><code>[LoadInSeparateAppDomain]<\/code><\/a> attribute defined by the\n<code>&lt;XamlCTask\/&gt;<\/code> in .NET MAUI. This is an MSBuild feature that gives MSBuild tasks\nto run in an isolated <code>AppDomain<\/code> &#8212; with an obvious performance drawback.\nHowever, we couldn&#8217;t <em>just remove it<\/em> as there would be complications&#8230;<\/p>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.build.framework.loadinseparateappdomainattribute\"><code>[LoadInSeparateAppDomain]<\/code><\/a> also conveniently resets all static\nstate when <code>&lt;XamlCTask\/&gt;<\/code> runs again. Meaning that future incremental builds\nwould potentially use old (garbage) values. There are several places that cache\nMono.Cecil objects for performance reasons. Really weird bugs would result if we\ndidn&#8217;t address this.<\/p>\n<p>So, to actually make this change, we reworked all <code>static<\/code> state in the XAML\ncompiler to be stored in instance fields &amp; properties instead. This is a general\nsoftware design improvement, in addition to giving us the ability to safely\nremove <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.build.framework.loadinseparateappdomainattribute\"><code>[LoadInSeparateAppDomain]<\/code><\/a>.<\/p>\n<p>The results of this change, for an incremental build on a Windows PC:<\/p>\n<pre><code class=\"language-log\">Before:\r\nXamlCTask = 743 ms\r\nXamlCTask = 706 ms\r\nXamlCTask = 692 ms\r\nAfter:\r\nXamlCTask = 128 ms\r\nXamlCTask = 134 ms\r\nXamlCTask = 117 ms<\/code><\/pre>\n<p>This saved about ~587ms on incremental builds on all platforms, an 82%\nimprovement. This will help even more on large solutions with multiple .NET MAUI\nprojects, where <code>&lt;XamlCTask\/&gt;<\/code> runs multiple times.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/11982\">maui#11982<\/a> for further details about this improvement.<\/p>\n<h2>Performance or App Size Improvements<\/h2>\n<h3>Structs and <code>IEquatable<\/code> in .NET MAUI<\/h3>\n<p>Using the Visual Studio&#8217;s <code>.NET Object Allocation Tracking<\/code> profiler on a\ncustomer .NET MAUI sample application, we saw:<\/p>\n<pre><code class=\"language-csharp\">Microsoft.Maui.WeakEventManager+Subscription\r\nAllocations: 686,114\r\nBytes: 21,955,648<\/code><\/pre>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/struct-iequatable.png\" alt=\"Screenshot of WeakEventManager+Subscription in the Visual Studio Profiler\" \/><\/p>\n<p>This seemed like an exorbitant amount of memory to be used in a sample\napplication&#8217;s startup!<\/p>\n<p>Drilling in to see where these <code>struct<\/code>&#8216;s were being created:<\/p>\n<pre><code class=\"language-csharp\">System.Collections.Generic.ObjectEqualityComparer&lt;Microsoft.Maui.WeakEventManager+Subscription&gt;.IndexOf()<\/code><\/pre>\n<p>The underlying problem was this <code>struct<\/code> didn&#8217;t implement <code>IEquatable&lt;T&gt;<\/code> and\nwas being used as the key for a dictionary. The <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/code-analysis\/quality-rules\/ca1815\"><code>CA1815<\/code><\/a> code analysis\nrule was designed to catch this problem. This is not a rule that is enabled by\ndefault, so projects must opt into it.<\/p>\n<p>To solve this:<\/p>\n<ul>\n<li>\n<p><code>Subscription<\/code> is internal to .NET MAUI, and its usage made it possible to be\na <code>readonly struct<\/code>. This was just an extra improvement.<\/p>\n<\/li>\n<li>\n<p>We made <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/code-analysis\/quality-rules\/ca1815\"><code>CA1815<\/code><\/a> a build error across the entire dotnet\/maui\nrepository.<\/p>\n<\/li>\n<li>\n<p>We implemented <code>IEquatable&lt;T&gt;<\/code> for <em>all<\/em> <code>struct<\/code> types.<\/p>\n<\/li>\n<\/ul>\n<p>After these changes, we could no longer found <code>Microsoft.Maui.WeakEventManager+Subscription<\/code>\nin memory snapshots at all. Which saved ~21 MB of allocations in this sample\napplication. If your own projects have usage of <code>struct<\/code>, it seems quite\nworthwhile to make <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/code-analysis\/quality-rules\/ca1815\"><code>CA1815<\/code><\/a> a build error.<\/p>\n<p>A smaller, targeted version of this change was backported to MAUI in .NET 7. See\n<a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/13232\">maui#13232<\/a> for details about this improvement.<\/p>\n<h3>Fix performance issue in <code>{AppThemeBinding}<\/code><\/h3>\n<p>Profiling a .NET MAUI sample application from a customer, we noticed a lot of\ntime spent in <code>{AppThemeBinding}<\/code> and <code>WeakEventManager<\/code> while scrolling:<\/p>\n<pre><code class=\"language-csharp\">2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr...\r\n2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()\r\n2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1&lt;TEventArgs_REF&gt;,string)<\/code><\/pre>\n<p>The following was happening in this application:<\/p>\n<ul>\n<li>\n<p>The standard .NET MAUI project template has lots of <code>{AppThemeBinding}<\/code> in the\ndefault <code>Styles.xaml<\/code>. This supports Light vs Dark theming.<\/p>\n<\/li>\n<li>\n<p><code>{AppThemeBinding}<\/code> subscribes to <code>Application.RequestedThemeChanged<\/code><\/p>\n<\/li>\n<li>\n<p>So, every MAUI view subscribe to this event &#8212; potentially multiple times.<\/p>\n<\/li>\n<li>\n<p>Subscribers are a <code>Dictionary&lt;string, List&lt;Subscriber&gt;&gt;<\/code>, where there is a\ndictionary lookup followed by a O(N) search for unsubscribe operations.<\/p>\n<\/li>\n<\/ul>\n<p>There is potentially a usecase here to come up with a generalized &#8220;weak event&#8221;\npattern for .NET. The implementation currently in .NET MAUI came over from\nXamarin.Forms, but a generalized pattern could be useful for .NET developers\nusing other UI frameworks.<\/p>\n<p>To make this scenario fast, for now, in .NET 8:<\/p>\n<p>Before:<\/p>\n<ul>\n<li>For any <code>{AppThemeBinding}<\/code>, it calls both:\n<ul>\n<li><code>RequestedThemeChanged -= OnRequestedThemeChanged<\/code> O(N) time<\/li>\n<li><code>RequestedThemeChanged += OnRequestedThemeChanged<\/code> constant time<\/li>\n<\/ul>\n<\/li>\n<li>Where the <code>-=<\/code> is notably slower, due to possibly 100s of subscribers.<\/li>\n<\/ul>\n<p>After:<\/p>\n<ul>\n<li>\n<p>Create an <code>_attached<\/code> boolean, so we know know the &#8220;state&#8221; if it is attached\nor not.<\/p>\n<\/li>\n<li>\n<p>New bindings only call <code>+=<\/code>, where <code>-=<\/code> will now only be called by\n<code>{AppThemeBinding}<\/code> in <em>rare<\/em> cases.<\/p>\n<\/li>\n<li>\n<p>Most .NET MAUI apps do not &#8220;unapply&#8221; bindings, but <code>-=<\/code> would only be used in\nthat case.<\/p>\n<\/li>\n<\/ul>\n<p>See the full details about this fix in <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14625\">maui#14625<\/a>. See\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/61517\">dotnet\/runtime#61517<\/a> for how we could implement &#8220;weak\nevents&#8221; in .NET in the future.<\/p>\n<h3>Address <code>CA1307<\/code> and <code>CA1309<\/code> for performance<\/h3>\n<p>Profiling a .NET MAUI sample application from a customer, we noticed time spent\nduring &#8220;culture-aware&#8221; string operations:<\/p>\n<pre><code class=\"language-csharp\">77.22ms microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.SetDefaultBackgroundColor()\r\n42.55ms System.Private.CoreLib!System.String.ToLower()<\/code><\/pre>\n<p>This case, we can improve by simply calling <code>ToLowerInvariant()<\/code> instead. In\nsome cases you might even consider using <code>string.Equals()<\/code> with\n<code>StringComparer.Ordinal<\/code>. In this case, our code was further reviewed and\noptimized in <a href=\"#reduce-java-interop-in-mauidrawable-on-android\">Reduce Java interop in <code>MauiDrawable<\/code> on Android<\/a>.<\/p>\n<p>In .NET 7, we added <code>CA1307<\/code> and <code>CA1309<\/code> code analysis rules to catch cases\nlike this, but it appears we missed some in <code>Microsoft.Maui.Graphics.dll<\/code>. These\nare likely useful rules to enable in your own .NET MAUI applications, as\navoiding all culture-aware string operations can be quite impactful on mobile.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14627\">maui#14627<\/a> for details about this improvement.<\/p>\n<h3>Address <code>CA1311<\/code> for performance<\/h3>\n<p>After addressing the <code>CA1307<\/code> and <code>CA1309<\/code> code analysis rules, we took things\nfurther and addressed <code>CA1311<\/code>.<\/p>\n<p>As mentioned in <a href=\"https:\/\/learn.microsoft.com\/previous-versions\/dotnet\/articles\/ms994325(v=msdn.10)#the-turkish-example\">the turkish example<\/a>, doing something like:<\/p>\n<pre><code class=\"language-csharp\">string text = something.ToUpper();\r\nswitch (text) { ... }<\/code><\/pre>\n<p>Can actually cause unexpected behavior in Turkish locales, because in Turkish,\nthe character I (Unicode 0049) is considered the upper case version of a\ndifferent character \u00fd (Unicode 0131), and i (Unicode 0069) is considered the\nlower case version of yet another character \u00dd (Unicode 0130).<\/p>\n<p><code>ToLowerInvariant()<\/code> and <code>ToUpperInvariant()<\/code> are also better for performance as\nan invariant <code>ToLower<\/code> \/ <code>ToUpper<\/code> operation is slightly faster. Doing this also\navoids loading the current culture, improving startup performance.<\/p>\n<p>There <em>are<\/em> cases where you would want the current culture, such as in a\n<code>CaseConverter<\/code> type in .NET MAUI. To do this, you simply have to be explicit in\nwhich culture you want to use:<\/p>\n<pre><code class=\"language-csharp\">return ConvertToUpper ?\r\n    v.ToUpper(CultureInfo.CurrentCulture) :\r\n    v.ToLower(CultureInfo.CurrentCulture);<\/code><\/pre>\n<p>The goal of this <code>CaseConverter<\/code> is to display upper or lowercase text to a\nuser. So it makes sense to use the <code>CurrentCulture<\/code> for this.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14773\">maui#14773<\/a> for details about this improvement.<\/p>\n<h3>Remove unused <code>ViewAttachedToWindow<\/code> event on Android<\/h3>\n<p>Every <code>Label<\/code> in .NET MAUI was subscribing to:<\/p>\n<pre><code class=\"language-csharp\">public class MauiTextView : AppCompatTextView\r\n{\r\n    public MauiTextView(Context context) : base(context)\r\n    {\r\n        this.ViewAttachedToWindow += MauiTextView_ViewAttachedToWindow;\r\n    }\r\n\r\n    private void MauiTextView_ViewAttachedToWindow(object? sender, ViewAttachedToWindowEventArgs e)\r\n    {\r\n    }\r\n\r\n    \/\/...<\/code><\/pre>\n<p>This was leftover from refactoring, but appeared in <code>dotnet-trace<\/code> output as:<\/p>\n<pre><code class=\"language-csharp\">278.55ms (2.4%) mono.android!Android.Views.View.add_ViewAttachedToWindow(System.EventHandler`1&lt;Android.Views.View\/ViewAttachedToWindowEv\r\n 30.55ms (0.26%) mono.android!Android.Views.View.IOnAttachStateChangeListenerInvoker.n_OnViewAttachedToWindow_Landroid_view_View__mm_wra<\/code><\/pre>\n<p>Where the first is the subscription, and the second is the event firing from\nJava to C# &#8212; only to run an empty managed method.<\/p>\n<p>Simply removing this event subscription and empty method, resulted in only a few\ncontrols to subscribe to this event as needed:<\/p>\n<pre><code class=\"language-csharp\">2.76ms (0.02%) mono.android!Android.Views.View.add_ViewAttachedToWindow(System.EventHandler`1&lt;Android.Views.View\/ViewAttachedToWindowEv<\/code><\/pre>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14833\">maui#14833<\/a> for details about this improvement.<\/p>\n<h3>Remove unneeded <code>System.Reflection<\/code> for <code>{Binding}<\/code><\/h3>\n<p>All bindings in .NET MAUI commonly hit the code path:<\/p>\n<pre><code class=\"language-csharp\">if (property.CanWrite &amp;&amp; property.SetMethod.IsPublic &amp;&amp; !property.SetMethod.IsStatic)\r\n{\r\n    part.LastSetter = property.SetMethod;\r\n    var lastSetterParameters = part.LastSetter.GetParameters();\r\n    part.SetterType = lastSetterParameters[lastSetterParameters.Length - 1].ParameterType;\r\n    \/\/...<\/code><\/pre>\n<p>Where ~53% of the time spent applying a binding appeared in <code>dotnet-trace<\/code> in\nthe <code>MethodInfo.GetParameters()<\/code> method:<\/p>\n<pre><code class=\"language-csharp\">core.benchmarks!Microsoft.Maui.Benchmarks.BindingBenchmarker.BindName()\r\n...\r\nmicrosoft.maui.controls!Microsoft.Maui.Controls.BindingExpression.SetupPart()\r\nSystem.Private.CoreLib.il!System.Reflection.RuntimeMethodInfo.GetParameters()<\/code><\/pre>\n<p>The above C# is simply finding the property type. It is using a roundabout way\nof using the property setter&#8217;s first parameter, which can be simplified to:<\/p>\n<pre><code class=\"language-csharp\">part.SetterType = property.PropertyType;<\/code><\/pre>\n<p>We could see the results of this change in a BenchmarkDotNet benchmark:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right\">Mean<\/th>\n<th style=\"text-align: right\">Error<\/th>\n<th style=\"text-align: right\">StdDev<\/th>\n<th style=\"text-align: right\">Gen0<\/th>\n<th style=\"text-align: right\">Gen1<\/th>\n<th style=\"text-align: right\">Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>&#8211;BindName<\/td>\n<td style=\"text-align: right\">18.82 us<\/td>\n<td style=\"text-align: right\">0.336 us<\/td>\n<td style=\"text-align: right\">0.471 us<\/td>\n<td style=\"text-align: right\">1.2817<\/td>\n<td style=\"text-align: right\">1.2512<\/td>\n<td style=\"text-align: right\">10.55 KB<\/td>\n<\/tr>\n<tr>\n<td>++BindName<\/td>\n<td style=\"text-align: right\">18.80 us<\/td>\n<td style=\"text-align: right\">0.371 us<\/td>\n<td style=\"text-align: right\">0.555 us<\/td>\n<td style=\"text-align: right\">1.2512<\/td>\n<td style=\"text-align: right\">1.2207<\/td>\n<td style=\"text-align: right\">10.23 KB<\/td>\n<\/tr>\n<tr>\n<td>&#8211;BindChild<\/td>\n<td style=\"text-align: right\">27.47 us<\/td>\n<td style=\"text-align: right\">0.542 us<\/td>\n<td style=\"text-align: right\">0.827 us<\/td>\n<td style=\"text-align: right\">2.0142<\/td>\n<td style=\"text-align: right\">1.9836<\/td>\n<td style=\"text-align: right\">16.56 KB<\/td>\n<\/tr>\n<tr>\n<td>++BindChild<\/td>\n<td style=\"text-align: right\">26.71 us<\/td>\n<td style=\"text-align: right\">0.516 us<\/td>\n<td style=\"text-align: right\">0.652 us<\/td>\n<td style=\"text-align: right\">1.9226<\/td>\n<td style=\"text-align: right\">1.8921<\/td>\n<td style=\"text-align: right\">15.94 KB<\/td>\n<\/tr>\n<tr>\n<td>&#8211;BindChildIndexer<\/td>\n<td style=\"text-align: right\">58.39 us<\/td>\n<td style=\"text-align: right\">1.113 us<\/td>\n<td style=\"text-align: right\">1.143 us<\/td>\n<td style=\"text-align: right\">3.1738<\/td>\n<td style=\"text-align: right\">3.1128<\/td>\n<td style=\"text-align: right\">26.17 KB<\/td>\n<\/tr>\n<tr>\n<td>++BindChildIndexer<\/td>\n<td style=\"text-align: right\">58.00 us<\/td>\n<td style=\"text-align: right\">1.055 us<\/td>\n<td style=\"text-align: right\">1.295 us<\/td>\n<td style=\"text-align: right\">3.1128<\/td>\n<td style=\"text-align: right\">3.0518<\/td>\n<td style=\"text-align: right\">25.47 KB<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Where <code>++<\/code> denotes the new changes.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14830\">maui#14830<\/a> for further details about this improvement.<\/p>\n<h3>Use <code>StringComparer.Ordinal<\/code> for <code>Dictionary<\/code> and <code>HashSet<\/code><\/h3>\n<p>Profiling a .NET MAUI sample application from a customer, we noticed 4% of the\ntime while scrolling was spent doing dictionary lookups:<\/p>\n<pre><code class=\"language-csharp\">(4.0%) System.Private.CoreLib!System.Collections.Generic.Dictionary&lt;TKey_REF,TValue_REF&gt;.FindValue(TKey_REF)<\/code><\/pre>\n<p>Observing the call stack, some of these were coming from culture-aware string\nlookups in .NET MAUI:<\/p>\n<ul>\n<li><code>microsoft.maui!Microsoft.Maui.PropertyMapper.GetProperty(string)<\/code><\/li>\n<li><code>microsoft.maui!Microsoft.Maui.WeakEventManager.AddEventHandler(System.EventHandler&lt;TEventArgs_REF&gt;,string)<\/code><\/li>\n<li><code>microsoft.maui!Microsoft.Maui.CommandMapper.GetCommand(string)<\/code><\/li>\n<\/ul>\n<p>Which show up in <code>dotnet-trace<\/code> as a mixture of <code>string<\/code> comparers:<\/p>\n<pre><code class=\"language-csharp\">(0.98%) System.Private.CoreLib!System.Collections.Generic.NonRandomizedStringEqualityComparer.OrdinalComparer.GetHashCode(string)\r\n(0.71%) System.Private.CoreLib!System.String.GetNonRandomizedHashCode()\r\n(0.31%) System.Private.CoreLib!System.Collections.Generic.NonRandomizedStringEqualityComparer.OrdinalComparer.Equals(string,stri\r\n(0.01%) System.Private.CoreLib!System.Collections.Generic.NonRandomizedStringEqualityComparer.GetStringComparer(object)<\/code><\/pre>\n<p>In cases of <code>Dictionary&lt;string, TValue&gt;<\/code> or <code>HashSet&lt;string&gt;<\/code>, we can use\n<code>StringComparer.Ordinal<\/code> in many cases to get faster dictionary lookups. This\nshould slightly improve the performance of handlers &amp; all .NET MAUI controls on\nall platforms.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14900\">maui#14900<\/a> for details about this improvement.<\/p>\n<h3>Reduce Java interop in <code>MauiDrawable<\/code> on Android<\/h3>\n<p>Profiling a .NET MAUI customer sample while scrolling on a Pixel 5, we saw some\ninteresting time being spent in:<\/p>\n<pre><code class=\"language-csharp\">(0.76%) microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.OnDraw(Android.Graphics.Drawables.Shapes.Shape,Android.Graphics.Canv\r\n(0.54%) microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.SetDefaultBackgroundColor()<\/code><\/pre>\n<p>This sample has a <code>&lt;Border\/&gt;<\/code> inside a <code>&lt;CollectionView\/&gt;<\/code> and so you can see\nthis work happening while scrolling.<\/p>\n<p>Specifically, we reviewed code in .NET MAUI, such as:<\/p>\n<pre><code class=\"language-csharp\">_borderPaint.StrokeWidth = _strokeThickness;\r\n_borderPaint.StrokeJoin = _strokeLineJoin;\r\n_borderPaint.StrokeCap = _strokeLineCap;\r\n_borderPaint.StrokeMiter = _strokeMiterLimit * 2;\r\nif (_borderPathEffect != null)\r\n    _borderPaint.SetPathEffect(_borderPathEffect);<\/code><\/pre>\n<p>This calls from C# to Java five times. Creating a new method in\n<code>PlatformInterop.java<\/code> allowed us to reduce it to a single time.<\/p>\n<p>We also improved the following method, which would perform many calls from C# to\nJava:<\/p>\n<pre><code class=\"language-csharp\">\/\/ C#\r\n\r\nvoid SetDefaultBackgroundColor()\r\n{\r\n    using (var background = new TypedValue())\r\n    {\r\n        if (_context == null || _context.Theme == null || _context.Resources == null)\r\n            return;\r\n\r\n        if (_context.Theme.ResolveAttribute(global::Android.Resource.Attribute.WindowBackground, background, true))\r\n        {\r\n            var resource = _context.Resources.GetResourceTypeName(background.ResourceId);\r\n            var type = resource?.ToLowerInvariant();\r\n\r\n            if (type == \"color\")\r\n            {\r\n                var color = new Android.Graphics.Color(ContextCompat.GetColor(_context, background.ResourceId));\r\n                _backgroundColor = color;\r\n            }\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p>To be more succinctly implemented in Java as:<\/p>\n<pre><code class=\"language-java\">\/\/ Java\r\n\r\n\/**\r\n * Gets the value of android.R.attr.windowBackground from the given Context\r\n * @param context\r\n * @return the color or -1 if not found\r\n *\/\r\npublic static int getWindowBackgroundColor(Context context)\r\n{\r\n    TypedValue value = new TypedValue();\r\n    if (!context.getTheme().resolveAttribute(android.R.attr.windowBackground, value, true) &amp;&amp; isColorType(value)) {\r\n        return value.data;\r\n    } else {\r\n        return -1;\r\n    }\r\n}\r\n\r\n\/**\r\n * Needed because TypedValue.isColorType() is only API Q+\r\n * https:\/\/github.com\/aosp-mirror\/platform_frameworks_base\/blob\/1d896eeeb8744a1498128d62c09a3aa0a2a29a16\/core\/java\/android\/util\/TypedValue.java#L266-L268\r\n * @param value\r\n * @return true if the TypedValue is a Color\r\n *\/\r\nprivate static boolean isColorType(TypedValue value)\r\n{\r\n    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.Q) {\r\n        return value.isColorType();\r\n    } else {\r\n        \/\/ Implementation from AOSP\r\n        return (value.type &gt;= TypedValue.TYPE_FIRST_COLOR_INT &amp;&amp; value.type &lt;= TypedValue.TYPE_LAST_COLOR_INT);\r\n    }\r\n}<\/code><\/pre>\n<p>Which reduces our new implementation on the C# side to be a single Java call and\ncreation of an <code>Android.Graphics.Color<\/code> struct:<\/p>\n<pre><code class=\"language-csharp\">void SetDefaultBackgroundColor()\r\n{\r\n    var color = PlatformInterop.GetWindowBackgroundColor(_context);\r\n    if (color != -1)\r\n    {\r\n        _backgroundColor = new Android.Graphics.Color(color);\r\n    }\r\n}<\/code><\/pre>\n<p>After these changes, we instead saw <code>dotnet-trace<\/code> output, such as:<\/p>\n<pre><code class=\"language-csharp\">(0.28%) microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.OnDraw(Android.Graphics.Drawables.Shapes.Shape,Android.Graphics.Canv\r\n(0.04%) microsoft.maui!Microsoft.Maui.Graphics.MauiDrawable.SetDefaultBackgroundColor()<\/code><\/pre>\n<p>This improves the performance of any <code>&lt;Border\/&gt;<\/code> (and other shapes) on Android,\nand drops about ~1% of the CPU usage while scrolling in this example.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14933\">maui#14933<\/a> for further details about this improvement.<\/p>\n<h3>Improve layout performance of <code>Label<\/code> on Android<\/h3>\n<p>Testing various .NET MAUI sample applications on Android, we noticed around 5.1%\nof time spent in <code>PrepareForTextViewArrange()<\/code>:<\/p>\n<pre><code class=\"language-csharp\">1.01s (5.1%) microsoft.maui!Microsoft.Maui.ViewHandlerExtensions.PrepareForTextViewArrange(Microsoft.Maui.IViewHandler,Microsoft.Maui\r\n635.99ms (3.2%) mono.android!Android.Views.View.get_Context()<\/code><\/pre>\n<p>Most of the time is spent just calling <code>Android.Views.View.Context<\/code> to be able\nto then call into the extension method:<\/p>\n<pre><code class=\"language-csharp\">internal static int MakeMeasureSpecExact(this Context context, double size)\r\n{\r\n    \/\/ Convert to a native size to create the spec for measuring\r\n    var deviceSize = (int)context!.ToPixels(size);\r\n    return MeasureSpecMode.Exactly.MakeMeasureSpec(deviceSize);\r\n}<\/code><\/pre>\n<p>Calling the <code>Context<\/code> property can be expensive due the interop from C# to Java.\nJava returns a handle to the instance, then we have to look up any existing,\nmanaged C# objects for the <code>Context<\/code>. If all this work can simply be avoided, it\ncan improve performance dramatically.<\/p>\n<p>In .NET 7, we made overloads to <code>ToPixels()<\/code> that allows you to get the same\nvalue with an <code>Android.Views.View<\/code><\/p>\n<p>So we can instead do:<\/p>\n<pre><code class=\"language-csharp\">internal static int MakeMeasureSpecExact(this PlatformView view, double size)\r\n{\r\n    \/\/ Convert to a native size to create the spec for measuring\r\n    var deviceSize = (int)view.ToPixels(size);\r\n    return MeasureSpecMode.Exactly.MakeMeasureSpec(deviceSize);\r\n}<\/code><\/pre>\n<p>Not only did this change show improvements in <code>dotnet-trace<\/code> output, but we saw\na noticeable difference in our <a href=\"https:\/\/github.com\/jonathanpeppers\/lols\">LOLs per second<\/a> test application from\nlast year:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/lols-context.png\" alt=\"385.85 to 396.64 LOLs per second\" \/><\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14980\">maui#14980<\/a> for details about this improvement.<\/p>\n<h3>Reduce Java interop calls for controls in .NET MAUI<\/h3>\n<p>Reviewing the beautiful .NET MAUI <a href=\"https:\/\/github.com\/jsuarezruiz\/netmaui-surfing-app-challenge\">&#8220;Surfing App&#8221;<\/a> sample by\n<a href=\"https:\/\/github.com\/jsuarezruiz\">@jsuarezruiz<\/a>:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/surfing-maui.png\" alt=\"Surfing app, made with .NET MAUI screenshot\" \/><\/p>\n<p>We noticed that a lot of time is spent doing Java interop while scrolling:<\/p>\n<pre><code class=\"language-csharp\">1.76s (35%) Microsoft.Maui!Microsoft.Maui.Platform.WrapperView.DispatchDraw(Android.Graphics.Canvas)\r\n1.76s (35%) Microsoft.Maui!Microsoft.Maui.Platform.ContentViewGroup.DispatchDraw(Android.Graphics.Canvas)<\/code><\/pre>\n<p>These methods were deeply nested doing interop from Java -&gt; C# -&gt; Java many\nlevels deep. In this case, moving some code from C# to Java could make it where\nless interop would occur; and in some cases no interop at all!<\/p>\n<p>So for example, previously <code>DispatchDraw()<\/code> was overridden in C# to implement\nclipping behavior:<\/p>\n<pre><code class=\"language-csharp\">\/\/ C#\r\n\/\/ ContentViewGroup is used internally by many .NET MAUI Controls\r\nclass ContentViewGroup : Android.Views.ViewGroup\r\n{\r\n    protected override void DispatchDraw(Canvas? canvas)\r\n    {\r\n        if (Clip != null)\r\n            ClipChild(canvas);\r\n\r\n        base.DispatchDraw(canvas);\r\n    }\r\n}<\/code><\/pre>\n<p>By creating a <code>PlatformContentViewGroup.java<\/code>, we can do something like:<\/p>\n<pre><code class=\"language-java\">\/\/ Java\r\n\r\n\/**\r\n * Set by C#, determining if we need to call getClipPath()\r\n * @param hasClip\r\n *\/\r\nprotected final void setHasClip(boolean hasClip) {\r\n    this.hasClip = hasClip;\r\n    postInvalidate();\r\n}\r\n\r\n@Override\r\nprotected void dispatchDraw(Canvas canvas) {\r\n    \/\/ Only call into C# if there is a Clip\r\n    if (hasClip) {\r\n        Path path = getClipPath(canvas.getWidth(), canvas.getHeight());\r\n        if (path != null) {\r\n            canvas.clipPath(path);\r\n        }\r\n    }\r\n    super.dispatchDraw(canvas);\r\n}<\/code><\/pre>\n<p><code>setHasClip()<\/code> is called when clipping is enabled\/disabled on any .NET MAUI\ncontrol. This allowed the common path to not interop into C# <em>at all<\/em>, and only\nviews that have opted into clipping would need to. This is <em>very<\/em> good because\n<code>dispatchDraw()<\/code> is called quite often during Android layout, scrolling, etc.<\/p>\n<p>This same treatment was also done to a few other internal .NET MAUI types like\n<code>WrapperView<\/code>: improving the common case, making interop only occur when views\nhave opted into clipping or drop shadows.<\/p>\n<p>For testing the impact of these changes, we used Google&#8217;s\n<a href=\"https:\/\/developer.android.com\/topic\/performance\/measuring-performance#identifying-issues\"><code>FrameMetricsAggregator<\/code><\/a> that can be setup in any .NET\nMAUI application&#8217;s <code>Platforms\/Android\/MainActivity.cs<\/code>:<\/p>\n<pre><code class=\"language-csharp\">\/\/ How often in ms you'd like to print the statistics to the console\r\nconst int Duration = 1000;\r\nFrameMetricsAggregator aggregator;\r\nHandler handler;\r\n\r\nprotected override void OnCreate(Bundle savedInstanceState)\r\n{\r\n    base.OnCreate(savedInstanceState);\r\n\r\n    handler = new Handler(Looper.MainLooper);\r\n\r\n    \/\/ We were interested in the \"Total\" time, other metrics also available\r\n    aggregator = new FrameMetricsAggregator(FrameMetricsAggregator.TotalDuration);\r\n    aggregator.Add(this);\r\n\r\n    handler.PostDelayed(OnFrame, Duration);\r\n}\r\n\r\nvoid OnFrame()\r\n{\r\n    \/\/ We were interested in the \"Total\" time, other metrics also available\r\n    var metrics = aggregator.GetMetrics()[FrameMetricsAggregator.TotalIndex];\r\n    int size = metrics.Size();\r\n    double sum = 0, count = 0, slow = 0;\r\n    for (int i = 0; i &lt; size; i++)\r\n    {\r\n        int value = metrics.Get(i);\r\n        if (value != 0)\r\n        {\r\n            count += value;\r\n            sum += i * value;\r\n            if (i &gt; 16)\r\n                slow += value;\r\n            Console.WriteLine($\"Frame(s) that took ~{i}ms, count: {value}\");\r\n        }\r\n    }\r\n    if (sum &gt; 0)\r\n    {\r\n        Console.WriteLine($\"Average frame time: {sum \/ count:0.00}ms\");\r\n        Console.WriteLine($\"No. of slow frames: {slow}\");\r\n        Console.WriteLine(\"-----\");\r\n    }\r\n    handler.PostDelayed(OnFrame, Duration);\r\n}<\/code><\/pre>\n<p><a href=\"https:\/\/developer.android.com\/topic\/performance\/measuring-performance#identifying-issues\"><code>FrameMetricsAggregator<\/code><\/a>&#8216;s API is admittedly a bit\nodd, but the data we get out is quite useful. The result is basically a lookup\ntable where the key is a duration in milliseconds, and the value is the number\nof &#8220;frames&#8221; that took that duration. The idea is any frame that takes longer\nthan 16ms is considered &#8220;slow&#8221; or &#8220;janky&#8221; as the Android docs sometimes refer.<\/p>\n<p>An example of the .NET MAUI <a href=\"https:\/\/github.com\/jsuarezruiz\/netmaui-surfing-app-challenge\">&#8220;Surfing App&#8221;<\/a> running on a Pixel 5:<\/p>\n<pre><code class=\"language-csharp\">Before:\r\nFrame(s) that took ~4ms, count: 1\r\nFrame(s) that took ~5ms, count: 6\r\nFrame(s) that took ~6ms, count: 10\r\nFrame(s) that took ~7ms, count: 12\r\nFrame(s) that took ~8ms, count: 10\r\nFrame(s) that took ~9ms, count: 6\r\nFrame(s) that took ~10ms, count: 1\r\nFrame(s) that took ~11ms, count: 2\r\nFrame(s) that took ~12ms, count: 4\r\nFrame(s) that took ~13ms, count: 2\r\nFrame(s) that took ~15ms, count: 1\r\nFrame(s) that took ~16ms, count: 1\r\nFrame(s) that took ~18ms, count: 2\r\nFrame(s) that took ~19ms, count: 1\r\nFrame(s) that took ~20ms, count: 5\r\nFrame(s) that took ~21ms, count: 2\r\nFrame(s) that took ~22ms, count: 1\r\nFrame(s) that took ~25ms, count: 1\r\nFrame(s) that took ~32ms, count: 1\r\nFrame(s) that took ~34ms, count: 1\r\nFrame(s) that took ~60ms, count: 1\r\nFrame(s) that took ~62ms, count: 1\r\nFrame(s) that took ~63ms, count: 1\r\nFrame(s) that took ~64ms, count: 2\r\nFrame(s) that took ~66ms, count: 1\r\nFrame(s) that took ~67ms, count: 1\r\nFrame(s) that took ~68ms, count: 1\r\nFrame(s) that took ~69ms, count: 2\r\nFrame(s) that took ~70ms, count: 2\r\nFrame(s) that took ~71ms, count: 2\r\nFrame(s) that took ~72ms, count: 1\r\nFrame(s) that took ~73ms, count: 2\r\nFrame(s) that took ~74ms, count: 2\r\nFrame(s) that took ~75ms, count: 1\r\nFrame(s) that took ~76ms, count: 1\r\nFrame(s) that took ~77ms, count: 2\r\nFrame(s) that took ~78ms, count: 3\r\nFrame(s) that took ~79ms, count: 1\r\nFrame(s) that took ~80ms, count: 1\r\nFrame(s) that took ~81ms, count: 1\r\nAverage frame time: 28.67ms\r\nNo. of slow frames: 43<\/code><\/pre>\n<p>After the changes to <code>ContentViewGroup<\/code> and <code>WrapperView<\/code> were in place, we got\na very nice improvement! Even in an app making heavy usage of clipping and\nshadows:<\/p>\n<pre><code class=\"language-csharp\">After:\r\nFrame(s) that took ~5ms, count: 3\r\nFrame(s) that took ~6ms, count: 5\r\nFrame(s) that took ~7ms, count: 7\r\nFrame(s) that took ~8ms, count: 7\r\nFrame(s) that took ~9ms, count: 4\r\nFrame(s) that took ~10ms, count: 2\r\nFrame(s) that took ~11ms, count: 6\r\nFrame(s) that took ~12ms, count: 2\r\nFrame(s) that took ~13ms, count: 3\r\nFrame(s) that took ~14ms, count: 4\r\nFrame(s) that took ~15ms, count: 1\r\nFrame(s) that took ~16ms, count: 1\r\nFrame(s) that took ~17ms, count: 1\r\nFrame(s) that took ~18ms, count: 2\r\nFrame(s) that took ~19ms, count: 1\r\nFrame(s) that took ~20ms, count: 3\r\nFrame(s) that took ~21ms, count: 2\r\nFrame(s) that took ~22ms, count: 2\r\nFrame(s) that took ~27ms, count: 2\r\nFrame(s) that took ~29ms, count: 2\r\nFrame(s) that took ~32ms, count: 1\r\nFrame(s) that took ~34ms, count: 1\r\nFrame(s) that took ~35ms, count: 1\r\nFrame(s) that took ~64ms, count: 1\r\nFrame(s) that took ~67ms, count: 1\r\nFrame(s) that took ~68ms, count: 2\r\nFrame(s) that took ~69ms, count: 1\r\nFrame(s) that took ~72ms, count: 3\r\nFrame(s) that took ~74ms, count: 3\r\nAverage frame time: 21.99ms\r\nNo. of slow frames: 29<\/code><\/pre>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/14275\">maui#14275<\/a> for further detail about these changes.<\/p>\n<h3>Improve performance of <code>Entry.MaxLength<\/code> on Android<\/h3>\n<p>Investigating a .NET MAUI customer sample:<\/p>\n<ul>\n<li>\n<p>Navigating from a <code>Shell<\/code> flyout.<\/p>\n<\/li>\n<li>\n<p>To a new page with several <code>Entry<\/code> controls.<\/p>\n<\/li>\n<li>\n<p>There was a noticeable performance delay.<\/p>\n<\/li>\n<\/ul>\n<p>When profiling on a Pixel 5, one &#8220;hot path&#8221; was <code>Entry.MaxLength<\/code>:<\/p>\n<pre><code class=\"language-csharp\">18.52ms (0.22%) microsoft.maui!Microsoft.Maui.Platform.EditTextExtensions.UpdateMaxLength(Android.Widget.EditText,Microsoft.Maui.IEntry)\r\n16.03ms (0.19%) microsoft.maui!Microsoft.Maui.Platform.EditTextExtensions.UpdateMaxLength(Android.Widget.EditText,int)\r\n12.16ms (0.14%) microsoft.maui!Microsoft.Maui.Platform.EditTextExtensions.SetLengthFilter(Android.Widget.EditText,int)<\/code><\/pre>\n<ul>\n<li><code>EditTextExtensions.UpdateMaxLength()<\/code> calls<\/li>\n<li><code>EditText.Text<\/code> getter and setter<\/li>\n<li><code>EditTextExtensions.SetLengthFilter()<\/code> calls<\/li>\n<li><code>EditText.Get\/SetFilters()<\/code><\/li>\n<\/ul>\n<p>What happens is we end up marshaling strings and <code>IInputFilter[]<\/code> back and forth\nbetween C# and Java for every <code>Entry<\/code> control. All <code>Entry<\/code> controls go through\nthis code path (even ones with a default value for <code>MaxLength<\/code>), so it made\nsense to move some of this code from C# to Java instead.<\/p>\n<p>Our C# code before:<\/p>\n<pre><code class=\"language-csharp\">\/\/ C#\r\n\r\npublic static void UpdateMaxLength(this EditText editText, int maxLength)\r\n{\r\n    editText.SetLengthFilter(maxLength);\r\n\r\n    var newText = editText.Text.TrimToMaxLength(maxLength);\r\n    if (editText.Text != newText)\r\n        editText.Text = newText;\r\n}\r\n\r\npublic static void SetLengthFilter(this EditText editText, int maxLength)\r\n{\r\n    if (maxLength == -1)\r\n        maxLength = int.MaxValue;\r\n\r\n    var currentFilters = new List&lt;IInputFilter&gt;(editText.GetFilters() ?? new IInputFilter[0]);\r\n    var changed = false;\r\n\r\n    for (var i = 0; i &lt; currentFilters.Count; i++)\r\n    {\r\n        if (currentFilters[i] is InputFilterLengthFilter)\r\n        {\r\n            currentFilters.RemoveAt(i);\r\n            changed = true;\r\n            break;\r\n        }\r\n    }\r\n\r\n    if (maxLength &gt;= 0)\r\n    {\r\n        currentFilters.Add(new InputFilterLengthFilter(maxLength));\r\n        changed = true;\r\n    }\r\n\r\n    if (changed)\r\n        editText.SetFilters(currentFilters.ToArray());\r\n}<\/code><\/pre>\n<p>Moved to Java (with identical behavior) instead:<\/p>\n<pre><code class=\"language-java\">\/\/ Java\r\n\r\n \/**\r\n * Sets the maxLength of an EditText\r\n * @param editText\r\n * @param maxLength\r\n *\/\r\npublic static void updateMaxLength(@NonNull EditText editText, int maxLength)\r\n{\r\n    setLengthFilter(editText, maxLength);\r\n\r\n    if (maxLength &lt; 0)\r\n        return;\r\n\r\n    Editable currentText = editText.getText();\r\n    if (currentText.length() &gt; maxLength) {\r\n        editText.setText(currentText.subSequence(0, maxLength));\r\n    }\r\n}\r\n\r\n\/**\r\n * Updates the InputFilter[] of an EditText. Used for Entry and SearchBar.\r\n * @param editText\r\n * @param maxLength\r\n *\/\r\npublic static void setLengthFilter(@NonNull EditText editText, int maxLength)\r\n{\r\n    if (maxLength == -1)\r\n        maxLength = Integer.MAX_VALUE;\r\n\r\n    List&lt;InputFilter&gt; currentFilters = new ArrayList&lt;&gt;(Arrays.asList(editText.getFilters()));\r\n    boolean changed = false;\r\n    for (int i = 0; i &lt; currentFilters.size(); i++) {\r\n        InputFilter filter = currentFilters.get(i);\r\n        if (filter instanceof InputFilter.LengthFilter) {\r\n            currentFilters.remove(i);\r\n            changed = true;\r\n            break;\r\n        }\r\n    }\r\n\r\n    if (maxLength &gt;= 0) {\r\n        currentFilters.add(new InputFilter.LengthFilter(maxLength));\r\n        changed = true;\r\n    }\r\n    if (changed) {\r\n        InputFilter[] newFilter = new InputFilter[currentFilters.size()];\r\n        editText.setFilters(currentFilters.toArray(newFilter));\r\n    }\r\n}<\/code><\/pre>\n<p>This avoids marshaling (copying!) string and array values back and forth from C#\nto Java. With these changes in place, the calls to <code>EditTextExtensions.UpdateMaxLength()<\/code>\nare now so fast they are missing completely from <code>dotnet-trace<\/code> output, saving\n~19ms when navigating to the page in the customer sample.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/15614\">maui#15614<\/a> for details about this improvement.<\/p>\n<h3>Improve memory usage of <code>CollectionView<\/code> on Windows<\/h3>\n<p>We reviewed a .NET MAUI customer sample with a <code>CollectionView<\/code> of 150,000\ndata-bound rows. Debugging what happens at runtime, .NET MAUI was effectively\ndoing:<\/p>\n<pre><code class=\"language-csharp\">_itemTemplateContexts = new List&lt;ItemTemplateContext&gt;(capacity: 150_000);\r\nfor (int n = 0; n &lt; 150_000; n++)\r\n{\r\n    _itemTemplateContexts.Add(null);\r\n}<\/code><\/pre>\n<p>And then each item is created as it is scrolled into view:<\/p>\n<pre><code class=\"language-csharp\">if (_itemTemplateContexts[index] == null)\r\n{\r\n    _itemTemplateContexts[index] = context = new ItemTemplateContext(...);\r\n}\r\nreturn _itemTemplateContexts[index];<\/code><\/pre>\n<p>This wasn&#8217;t the best approach, but to improve things:<\/p>\n<ul>\n<li>\n<p>use a <code>Dictionary&lt;int, T&gt;<\/code> instead, just let it size dynamically.<\/p>\n<\/li>\n<li>\n<p>use <code>TryGetValue(..., out var context)<\/code>, so each call accesses the indexer one\nless time than before.<\/p>\n<\/li>\n<li>\n<p>use either the bound collection&#8217;s size or 64 (whichever is smaller) as a rough\nestimate of how many might fit on screen at a time<\/p>\n<\/li>\n<\/ul>\n<p>Our code changes to:<\/p>\n<pre><code class=\"language-csharp\">if (!_itemTemplateContexts.TryGetValue(index, out var context))\r\n{\r\n    _itemTemplateContexts[index] = context = new ItemTemplateContext(...);\r\n}\r\nreturn context;<\/code><\/pre>\n<p>With these changes in place, a memory snapshot of the app after startup:<\/p>\n<pre><code class=\"language-log\">Before:\r\nHeap Size: 82,899.54 KB\r\nAfter:\r\nHeap Size: 81,768.76 KB<\/code><\/pre>\n<p>Which is saving about 1MB of memory on launch. In this case, it feels better to\njust let the <code>Dictionary<\/code> size itself with an estimate of what capacity will be.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/16838\">maui#16838<\/a> for details about this improvement.<\/p>\n<h3>Use <code>UnmanagedCallersOnlyAttribute<\/code> on Apple platforms<\/h3>\n<p>When unmanaged code calls into managed code, such as invoking a callback from\nObjective-C, the <code>[MonoPInvokeCallbackAttribute]<\/code> was previously used in\nXamarin.iOS, Xamarin.Mac, and .NET 6+ for this purpose. The\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.runtime.interopservices.unmanagedcallersonlyattribute\"><code>[UnmanagedCallersOnlyAttribute]<\/code><\/a> attribute came along\nas a modern replacement for this Mono feature, which is implemented in a way\nwith performance in mind.<\/p>\n<p>Unfortunately, there are a few restrictions when using this new attribute:<\/p>\n<ul>\n<li>Method must be marked <code>static<\/code>.<\/li>\n<li>Must not be called from managed code.<\/li>\n<li>Must only have <a href=\"https:\/\/learn.microsoft.com\/dotnet\/framework\/interop\/blittable-and-non-blittable-types\">blittable<\/a> arguments.<\/li>\n<li>Must not have generic type parameters or be contained within a generic class.<\/li>\n<\/ul>\n<p>Not only did we have to refactor the &#8220;code generator&#8221; that produces many of the\nbindings for Apple APIs for AppKit, UIKit, etc., but we also had many manual\nbindings that would need the same treatment.<\/p>\n<p>The end result is that most callbacks from Objective-C to C# should be faster in\n.NET 8 than before. See <a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/issues\/10470\">xamarin-macios#10470<\/a> and\n<a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/issues\/15783\">xamarin-macios#15783<\/a> for details about these\nimprovements.<\/p>\n<h3>Faster Java interop for strings on Android<\/h3>\n<p>When binding members which have parameter types or return types which\nare <code>java.lang.CharSequence<\/code>, the member is &#8220;overloaded&#8221; to replace\n<code>CharSequence<\/code> with <code>System.String<\/code>, and the &#8220;original&#8221; member has a\n<code>Formatted<\/code> <em>suffix<\/em>.<\/p>\n<p>For example, consider <a href=\"https:\/\/developer.android.com\/reference\/android\/widget\/TextView\"><code>android.widget.TextView<\/code><\/a>, which has\n<a href=\"https:\/\/developer.android.com\/reference\/android\/widget\/TextView#getText()\"><code>getText()<\/code><\/a> and <a href=\"https:\/\/developer.android.com\/reference\/android\/widget\/TextView#setText(java.lang.CharSequence)\"><code>setText()<\/code><\/a> methods\nwhich have parameter types and return types which are <code>java.lang.CharSequence<\/code>:<\/p>\n<pre><code class=\"language-java\">\/\/ Java\r\nclass TextView extends View {\r\n    public CharSequence getText();\r\n    public final void setText(CharSequence text);\r\n}<\/code><\/pre>\n<p>When bound, this results in <em>two<\/em> properties:<\/p>\n<pre><code class=\"language-csharp\">\/\/ C#\r\nclass TextView : View {\r\n    public Java.Lang.ICharSequence? TextFormatted { get; set; }\r\n    public string? Text { get; set; }\r\n}<\/code><\/pre>\n<p>The &#8220;non-<code>Formatted<\/code> overload&#8221; works by creating a temporary <code>String<\/code> object to\ninvoke the <code>Formatted<\/code> overload, so the actual implementation looks like:<\/p>\n<pre><code class=\"language-csharp\">partial class TextView {\r\n    public string? Text {\r\n        get =&gt; TextFormatted?.ToString ();\r\n        set {\r\n            var jls = value == null ? null : new Java.Lang.String (value);\r\n            TextFormatted = jls;\r\n            jls?.Dispose ();\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p><code>TextView.Text<\/code> is much easer to understand &amp; simpler to consume for .NET\ndevelopers than <code>TextView.TextFormatted<\/code>.<\/p>\n<p>A problem with the this approach is performance: creating a new\n<code>Java.Lang.String<\/code> instance requires:<\/p>\n<ol>\n<li>Creating the managed peer (the <code>Java.Lang.String<\/code> instance),<\/li>\n<li>Creating the native peer (the <code>java.lang.String<\/code> instance),<\/li>\n<li>And <em>registering the mapping<\/em> between (1) and (2)<\/li>\n<\/ol>\n<p>And then immediately use and dispose the value&#8230;<\/p>\n<p>This is particularly noticeable with .NET MAUI apps. Consider a customer sample,\nwhich uses XAML to set data-bound <code>Text<\/code> values in a <code>CollectionView<\/code>, which\neventually hit <code>TextView.Text<\/code>. Profiling shows:<\/p>\n<pre><code class=\"language-csharp\">653.69ms (6.3%) mono.android!Android.Widget.TextView.set_Text(string)\r\n198.05ms (1.9%) mono.android!Java.Lang.String..ctor(string)\r\n121.57ms (1.2%) mono.android!Java.Lang.Object.Dispose()<\/code><\/pre>\n<p><em>6.3%<\/em> of scrolling time is spent in the <code>TextView.Text<\/code> property\nsetter!<\/p>\n<p><em>Partially optimize<\/em> this case: if the <code>*Formatted<\/code> member is\n(1) a property, and (2) <em>not<\/em> <code>virtual<\/code>, then we can directly call\nthe Java setter method. This avoids the need to create a managed\npeer and to register a mapping between the peers:<\/p>\n<pre><code class=\"language-csharp\">partial class TextView {\r\n    public string? Text {\r\n        get =&gt; TextFormatted?.ToString (); \/\/ unchanged\r\n        set {\r\n            const string __id               = \"setText.(Ljava\/lang\/CharSequence;)V\";\r\n            JniObjectReference native_value = JniEnvironment.Strings.NewString (value);\r\n            try {\r\n                JniArgumentValue* __args    = stackalloc JniArgumentValue [1];\r\n                __args [0] = new JniArgumentValue (native_value);\r\n                _members.InstanceMethods.InvokeNonvirtualVoidMethod (__id, this, __args);\r\n            } finally {\r\n                JniObjectReference.Dispose (ref native_value);\r\n            }\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p>With the result being:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right\">Mean<\/th>\n<th style=\"text-align: right\">Error<\/th>\n<th style=\"text-align: right\">StdDev<\/th>\n<th style=\"text-align: right\">Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Before SetFinalText<\/td>\n<td style=\"text-align: right\">6.632 us<\/td>\n<td style=\"text-align: right\">0.0101 us<\/td>\n<td style=\"text-align: right\">0.0079 us<\/td>\n<td style=\"text-align: right\">112 B<\/td>\n<\/tr>\n<tr>\n<td>After SetFinalText<\/td>\n<td style=\"text-align: right\">1.361 us<\/td>\n<td style=\"text-align: right\">0.0022 us<\/td>\n<td style=\"text-align: right\">0.0019 us<\/td>\n<td style=\"text-align: right\">&#8211;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The <code>TextView.Text<\/code> property setter invocation time is reduced to 20%\nof the previous average invocation time.<\/p>\n<p>Note that the virtual case is problematic for other reasons, but luckily enough\n<code>TextView.setText()<\/code> is non-virtual and likely one of the more commonly used\nAndroid APIs.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1101\">java-interop#1101<\/a> for details about this improvement.<\/p>\n<h3>Faster Java interop for C# events on Android<\/h3>\n<p>Profiling a .NET MAUI customer sample while scrolling on a Pixel 5, We saw ~2.2%\nof the time spent in the <code>IOnFocusChangeListenerImplementor<\/code> constructor, due to\na subscription to the <code>View.FocusChange<\/code> event:<\/p>\n<pre><code class=\"language-csharp\">(2.2%) mono.android!Android.Views.View.IOnFocusChangeListenerImplementor..ctor()<\/code><\/pre>\n<p>MAUI subscribes to <code>Android.Views.View.FocusChange<\/code> for every view placed on the\nscreen, which happens while scrolling in this sample.<\/p>\n<p>Reviewing the generated code for the <code>IOnFocusChangeListenerImplementor<\/code>\nconstructor, we see it still uses outdated <code>JNIEnv<\/code> APIs:<\/p>\n<pre><code class=\"language-csharp\">public IOnFocusChangeListenerImplementor () : base (\r\n        Android.Runtime.JNIEnv.StartCreateInstance (\"mono\/android\/view\/View_OnFocusChangeListenerImplementor\", \"()V\"),\r\n        JniHandleOwnership.TransferLocalRef\r\n    )\r\n{\r\n    Android.Runtime.JNIEnv.FinishCreateInstance (((Java.Lang.Object) this).Handle, \"()V\");\r\n}<\/code><\/pre>\n<p>Which we can change to use the newer\/faster Java.Interop APIs:<\/p>\n<pre><code class=\"language-csharp\">public unsafe IOnFocusChangeListenerImplementor ()\r\n    : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer)\r\n{\r\n    const string __id = \"()V\";\r\n    if (((Java.Lang.Object) this).Handle != IntPtr.Zero)\r\n        return;\r\n    var h = JniPeerMembers.InstanceMethods.StartCreateInstance (__id, ((object) this).GetType (), null);\r\n    SetHandle (h.Handle, JniHandleOwnership.TransferLocalRef);\r\n    JniPeerMembers.InstanceMethods.FinishCreateInstance (__id, this, null);\r\n}<\/code><\/pre>\n<p>These are better because the equivalent call to <code>JNIEnv.FindClass()<\/code> is cached,\namong other things. This was just one of the cases that was accidentally missed\nwhen we implemented the new Java.Interop APIs in the Xamarin timeframe. We\nsimply needed to update our code generator to emit a better C# binding for this\ncase.<\/p>\n<p>After these changes, we saw instead results in <code>dotnet-trace<\/code>:<\/p>\n<pre><code class=\"language-csharp\">(0.81%) mono.android!Android.Views.View.IOnFocusChangeListenerImplementor..ctor()<\/code><\/pre>\n<p>This should improve the performance of all C# events that wrap Java\n<a href=\"https:\/\/docs.oracle.com\/javase\/tutorial\/uiswing\/events\/intro.html\">listeners<\/a>, a design-pattern commonly used in Java and Android\napplications. This includes the <code>FocusedChanged<\/code> event used by all .NET MAUI\nviews on Android.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/1105\">java-interop#1105<\/a> for details about this improvement.<\/p>\n<h3>Use Function Pointers for JNI<\/h3>\n<p>There is various machinery and generated code that makes Java interop possible\nfrom C#. Take, for example, the following instance method <code>foo()<\/code> in Java:<\/p>\n<pre><code class=\"language-java\">\/\/ Java\r\nobject foo(object bar) {\r\n    \/\/ returns some value\r\n}<\/code><\/pre>\n<p>A C# method named <code>CallObjectMethod<\/code> is responsible for calling Java&#8217;s Native\nInterface (JNI) that calls into the JVM to actually invoke the Java method:<\/p>\n<pre><code class=\"language-csharp\">public static unsafe JniObjectReference CallObjectMethod (JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args)\r\n{\r\n    \/\/...\r\n    IntPtr thrown;\r\n    var tmp = NativeMethods.java_interop_jnienv_call_object_method_a (JniEnvironment.EnvironmentPointer, out thrown, instance.Handle, method.ID, (IntPtr) args);\r\n\r\n    Exception __e = JniEnvironment.GetExceptionForLastThrowable (thrown);\r\n    if (__e != null)\r\n        ExceptionDispatchInfo.Capture (__e).Throw ();\r\n\r\n    JniEnvironment.LogCreateLocalRef (tmp);\r\n    return new JniObjectReference (tmp, JniObjectReferenceType.Local);\r\n}<\/code><\/pre>\n<p>In Xamarin.Android, .NET 6, and .NET 7 all calls into Java went through a\n<code>java_interop_jnienv_call_object_method_a<\/code> p\/invoke, which signature looks like:<\/p>\n<pre><code class=\"language-csharp\">[DllImport (JavaInteropLib, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]\r\ninternal static extern unsafe jobject java_interop_jnienv_call_object_method_a (IntPtr jnienv, out IntPtr thrown, jobject instance, IntPtr method, IntPtr args);<\/code><\/pre>\n<p>Which is implemented in C as:<\/p>\n<pre><code class=\"language-c\">JI_API jobject\r\njava_interop_jnienv_call_object_method_a (JNIEnv *env, jthrowable *_thrown, jobject instance, jmethodID method, jvalue* args)\r\n{\r\n    *_thrown = 0;\r\n    jobject _r_ = (*env)-&gt;CallObjectMethodA (env, instance, method, args);\r\n    *_thrown = (*env)-&gt;ExceptionOccurred (env);\r\n    return _r_;\r\n}<\/code><\/pre>\n<p>C# 9 introduced <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/unsafe-code#function-pointers\">function pointers<\/a> that allowed us a way to\nsimplify things slightly &#8212; and make them faster as a result.<\/p>\n<p>So instead of using p\/invoke in .NET 8, we could instead call a new <code>unsafe<\/code>\nmethod named <code>CallObjectMethodA<\/code>:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Before:\r\nvar tmp = NativeMethods.java_interop_jnienv_call_object_method_a (JniEnvironment.EnvironmentPointer, out thrown, instance.Handle, method.ID, (IntPtr) args);\r\n\/\/ After:\r\nvar tmp = JniNativeMethods.CallObjectMethodA (JniEnvironment.EnvironmentPointer, instance.Handle, method.ID, (IntPtr) args);<\/code><\/pre>\n<p>Which calls a C# function pointer directly:<\/p>\n<pre><code class=\"language-csharp\">[System.Runtime.CompilerServices.MethodImpl (System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]\r\ninternal static unsafe jobject CallObjectMethodA (IntPtr env, jobject instance, IntPtr method, IntPtr args)\r\n{\r\n    return (*((JNIEnv**)env))-&gt;CallObjectMethodA (env, instance, method, args);\r\n}<\/code><\/pre>\n<p>This function pointer declared using the new syntax introduced in C# 9:<\/p>\n<pre><code class=\"language-csharp\">public delegate* unmanaged &lt;IntPtr, jobject, IntPtr, IntPtr, jobject&gt; CallObjectMethodA;<\/code><\/pre>\n<p>Comparing the two implementations with a manual benchmark:<\/p>\n<pre><code class=\"language-log\"># JIPinvokeTiming timing: 00:00:01.6993644\r\n#    Average Invocation: 0.00016993643999999998ms\r\n# JIFunctionPointersTiming timing: 00:00:01.6561349\r\n#    Average Invocation: 0.00016561349ms<\/code><\/pre>\n<p>With a <code>Release<\/code> build, the average invocation time for\n<code>JIFunctionPointersTiming<\/code> takes 97% of the time as <code>JIPinvokeTiming<\/code>, i.e. is\n3% faster. Additionally, using C# 9 function pointers means we can get rid of\nall of the <code>java_interop_jnienv_*()<\/code> C functions, which shrinks\n<code>libmonodroid.so<\/code> by ~55KB for each architecture.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/8234\">xamarin-android#8234<\/a> and\n<a href=\"https:\/\/github.com\/xamarin\/java.interop\/pull\/938\">java-interop#938<\/a> for details about this improvement.<\/p>\n<h3>Removed <code>Xamarin.AndroidX.Legacy.Support.V4<\/code><\/h3>\n<p>Reviewing .NET MAUI&#8217;s Android dependencies, we noticed a suspicious package:<\/p>\n<pre><code class=\"language-csharp\">Xamarin.AndroidX.Legacy.Support.V4<\/code><\/pre>\n<p>If you are familiar with the <a href=\"https:\/\/developer.android.com\/topic\/libraries\/support-library\/\">Android Support Libraries<\/a>, these\nare a set of packages Google provides to &#8220;polyfill&#8221; APIs to past versions of\nAndroid. This gives them a way to bring new APIs to old OS versions, since the\nAndroid ecosystem (OEMs, etc.) are much slower to upgrade as compared to iOS,\nfor example. This particular package, <code>Legacy.Support.V4<\/code>, is actually support\nfor Android as far back as Android API 4! The minimum supported Android version\nin .NET is Android API 21, which was released in 2017.<\/p>\n<p>It turns out this dependency was brought over from Xamarin.Forms and was not\nactually needed. As expected from this change, lots of Java code was removed\nfrom .NET MAUI apps. So much, in fact, that .NET 8 MAUI applications are <a href=\"https:\/\/developer.android.com\/build\/multidex\">now\nunder the multi-dex limit<\/a> &#8212; all <a href=\"https:\/\/source.android.com\/docs\/core\/runtime\/dalvik-bytecode\">Dalvik bytecode<\/a> can fix\ninto a single <code>classes.dex<\/code> file.<\/p>\n<p>A detailed breakdown of the size changes using <a href=\"https:\/\/github.com\/radekdoulik\/apkdiff\"><code>apkdiff<\/code><\/a>:<\/p>\n<pre><code class=\"language-diff\">&gt; apkdiff -f com.companyname.maui_before-Signed.apk com.companyname.maui_after-Signed.apk\r\nSize difference in bytes ([*1] apk1 only, [*2] apk2 only):\r\n+   1,598,040 classes.dex\r\n-           6 META-INF\/androidx.asynclayoutinflater_asynclayoutinflater.version *1\r\n-           6 META-INF\/androidx.legacy_legacy-support-core-ui.version *1\r\n-           6 META-INF\/androidx.legacy_legacy-support-v4.version *1\r\n-           6 META-INF\/androidx.media_media.version *1\r\n-         455 assemblies\/assemblies.blob\r\n-         564 res\/layout\/notification_media_action.xml *1\r\n-         744 res\/layout\/notification_media_cancel_action.xml *1\r\n-       1,292 res\/layout\/notification_template_media.xml *1\r\n-       1,584 META-INF\/BNDLTOOL.SF\r\n-       1,584 META-INF\/MANIFEST.MF\r\n-       1,696 res\/layout\/notification_template_big_media.xml *1\r\n-       1,824 res\/layout\/notification_template_big_media_narrow.xml *1\r\n-       2,456 resources.arsc\r\n-       2,756 res\/layout\/notification_template_media_custom.xml *1\r\n-       2,872 res\/layout\/notification_template_lines_media.xml *1\r\n-       3,044 res\/layout\/notification_template_big_media_custom.xml *1\r\n-       3,216 res\/layout\/notification_template_big_media_narrow_custom.xml *1\r\n-   2,030,636 classes2.dex\r\nSummary:\r\n-      24,111 Other entries -0.35% (of 6,880,759)\r\n-     432,596 Dalvik executables -3.46% (of 12,515,440)\r\n+           0 Shared libraries 0.00% (of 12,235,904)\r\n-     169,179 Package size difference -1.12% (of 15,123,185)<\/code><\/pre>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/12232\">dotnet\/maui#12232<\/a> for details about this improvement.<\/p>\n<h3>Deduplication of generics on iOS and macOS<\/h3>\n<p>In .NET 7, iOS applications experienced app size increases due to C# generics\nusage across multiple .NET assemblies. When the .NET 7 Mono AOT compiler\nencounters a generic instance that is not handled by generic sharing, it will\nemit code for the instance. If the same instance is encountered during AOT\ncompilation in multiple assemblies, the code will be emitted multiple times,\nincreasing code size.<\/p>\n<p>In .NET 8, new <code>dedup-skip<\/code> and <code>dedup-include<\/code> command-line options are passed\nto the Mono AOT compiler. A new <code>aot-instances.dll<\/code> assembly is created for\nsharing this information in one place throughout the application.<\/p>\n<p>The change was tested on <code>MySingleView<\/code> app and <code>Monotouch<\/code> tests in the\nxamarin\/xamarin-macios codebase:<\/p>\n<table>\n<thead>\n<tr>\n<th>App<\/th>\n<th>Baseline size on disk .ipa (MB)<\/th>\n<th>Target size on disk .ipa (MB)<\/th>\n<th>Baseline size on disk .app (MB)<\/th>\n<th>Target size on disk .app (MB)<\/th>\n<th>Baseline build time (s)<\/th>\n<th>Target build time (s)<\/th>\n<th>.app diff (%)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>MySingleView Release iOS<\/td>\n<td>5.4<\/td>\n<td>5.4<\/td>\n<td>29.2<\/td>\n<td>15.2<\/td>\n<td>29.2<\/td>\n<td>16.8<\/td>\n<td>47.9<\/td>\n<\/tr>\n<tr>\n<td>MySingleView Release  iOSSimulator-arm64<\/td>\n<td>N\/A<\/td>\n<td>N\/A<\/td>\n<td>469.5<\/td>\n<td>341.8<\/td>\n<td>468.0<\/td>\n<td>330.0<\/td>\n<td>27.2<\/td>\n<\/tr>\n<tr>\n<td>Monotouch Release llvm iOS<\/td>\n<td>49.0<\/td>\n<td>38.8<\/td>\n<td>209.6<\/td>\n<td>157.4<\/td>\n<td>115.0<\/td>\n<td>130.0<\/td>\n<td>24.9<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/17766\">xamarin-macios#17766<\/a> for details about this\nimprovement.<\/p>\n<h3>Fix <code>System.Linq.Expressions<\/code> implementation on iOS-like platforms<\/h3>\n<p>In .NET 7, codepaths in System.Linq.Expressions were controlled by various flags\nsuch as:<\/p>\n<ul>\n<li><code>CanCompileToIL<\/code><\/li>\n<li><code>CanEmitObjectArrayDelegate<\/code><\/li>\n<li><code>CanCreateArbitraryDelegates<\/code><\/li>\n<\/ul>\n<p>These flags were controlling codepaths which are &#8220;AOT friendly&#8221; and those that\nare not. For desktop platforms, NativeAOT specifies the following configuration\nfor AOT-compatible code:<\/p>\n<pre><code class=\"language-xml\">&lt;IlcArg Include=\"--feature:System.Linq.Expressions.CanCompileToIL=false\" \/&gt; \r\n&lt;IlcArg Include=\"--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false\" \/&gt; \r\n&lt;IlcArg Include=\"--feature:System.Linq.Expressions.CanCreateArbitraryDelegates=false\" \/&gt; <\/code><\/pre>\n<p>When it comes to iOS-like platforms, System.Linq.Expressions library was built\nwith constant propagation enabled and control variables were removed. This\nfurther caused above-listed NativeAOT feature switches not to have any effect\n(fail to trim during app build), potentially causing the AOT compilation to\nfollow unsupported code paths on these platforms.<\/p>\n<p>In .NET8, we have unified the build of <code>System.Linq.Expressions.dll<\/code> shipping the same assembly for all supported platforms and runtimes, and simplified these switches to respect <code>IsDynamicCodeSupported<\/code> so that the .NET\ntrimmer can remove the appropriate IL in <code>System.Linq.Expressions.dll<\/code> at\napplication build time.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/87924\">dotnet\/runtime#87924<\/a> and\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/89308\">dotnet\/runtime#89308<\/a> for details about this improvement.<\/p>\n<h3>Set <code>DynamicCodeSupport=false<\/code> for iOS and Catalyst<\/h3>\n<p>In .NET 8, the feature switch <code>$(DynamicCodeSupport)<\/code> is set to false for\nplatforms:<\/p>\n<ul>\n<li>\n<p>Where it is not possible to publish without the AOT compiler.<\/p>\n<\/li>\n<li>\n<p>When interpreter is not enabled.<\/p>\n<\/li>\n<\/ul>\n<p>Which boils down to applications running on iOS, tvOS, MacCatalyst, etc.<\/p>\n<p><code>DynamicCodeSupport=false<\/code> enables the .NET trimmer to remove code paths\ndepending on <code>RuntimeFeature.IsDynamicCodeSupported<\/code> such as <a href=\"https:\/\/github.com\/dotnet\/runtime\/blob\/dedaf46154a8938848fc9a7e0bd6e5ccebe7809c\/src\/libraries\/System.Linq.Expressions\/src\/System\/Linq\/Expressions\/Interpreter\/CallInstruction.cs#L19\">this example in\nSystem.Linq.Expressions<\/a>.<\/p>\n<p>Estimated size savings are:<\/p>\n<table>\n<thead>\n<tr>\n<th>dotnet new maui (ios)<\/th>\n<th>old SLE.dll<\/th>\n<th>new SLE.dll + DynamicCodeSupported=false<\/th>\n<th>diff (%)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Size on disk (Mb)<\/td>\n<td>40,53<\/td>\n<td>38,78<\/td>\n<td>-4,31%<\/td>\n<\/tr>\n<tr>\n<td>.pkg (Mb)<\/td>\n<td>14,83<\/td>\n<td>14,20<\/td>\n<td>-4,21%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>When combined with the <a href=\"#fix-systemlinqexpressions-implementation-on-ios-like-platforms\"><code>System.Linq.Expressions<\/code> improvements on iOS-like\nplatforms<\/a>,\nthis showed a nice overall improvement to application size:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/MonoVsNativeAOT.png\" alt=\"Maui Mono vs NativeAOT\" \/><\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/18555\">xamarin-macios#18555<\/a> for details about this\nimprovement.<\/p>\n<h2>Memory Leaks<\/h2>\n<h3>Memory Leaks and Quality<\/h3>\n<p>Given that the major theme for .NET MAUI in .NET 8 is <em>quality<\/em>, memory-related\nissues became a focal point for this release. Some of the problems found existed\neven in the Xamarin.Forms codebase, so we are happy to work towards a framework\nthat developers can rely on for their cross-platform .NET applications.<\/p>\n<p>For full details on the work completed in .NET 8, we&#8217;ve various PRs and Issues\nrelated to memory issues at:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/pulls?q=is%3Apr+label%3A%22memory-leak+%F0%9F%92%A6%22+\">Pull Requests<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/maui\/issues?q=is%3Aissue+label%3A%22memory-leak+%F0%9F%92%A6%22+\">Issues<\/a><\/li>\n<\/ul>\n<p>You can see that considerable progress was made in .NET 8 in this area.<\/p>\n<p>If we compare .NET 7 MAUI versus .NET 8 MAUI in a sample application running on\nWindows, displaying the results of <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.gc.gettotalmemory\"><code>GC.GetTotalMemory()<\/code><\/a> on\nscreen:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/windows-side-by-side.gif\" alt=\"Comparison on Windows: .NET 7 vs .NET 8\" \/><\/p>\n<p>Then compare the sample application running on macOS, but with many more pages\npushed onto the navigation stack:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/mac-side-by-side.gif\" alt=\"Comparison on Mac: .NET 7 vs .NET 8\" \/><\/p>\n<p>See the <a href=\"https:\/\/github.com\/jonathanpeppers\/maui-memory-comparison\">sample code for this project on GitHub<\/a> for further details.<\/p>\n<h3>Diagnosing leaks in .NET MAUI<\/h3>\n<p>The symptom of a memory leak in a .NET MAUI application, could be something like:<\/p>\n<ul>\n<li>\n<p>Navigate from the landing page to a sub page.<\/p>\n<\/li>\n<li>\n<p>Go back.<\/p>\n<\/li>\n<li>\n<p>Navigate to the sub page again.<\/p>\n<\/li>\n<li>\n<p>Repeat.<\/p>\n<\/li>\n<li>\n<p>Memory grows consistently until the OS closes the application due to lack of\nmemory.<\/p>\n<\/li>\n<\/ul>\n<p>In the case of Android, you may see log messages such as:<\/p>\n<pre><code class=\"language-log\">07-07 18:51:39.090 17079 17079 D Mono : GC_MAJOR: (user request) time 137.21ms, stw 140.60ms los size: 10984K in use: 3434K\r\n07-07 18:51:39.090 17079 17079 D Mono : GC_MAJOR_SWEEP: major size: 116192K in use: 108493K\r\n07-07 18:51:39.092 17079 17079 I monodroid-gc: 46204 outstanding GREFs. Performing a full GC!<\/code><\/pre>\n<p>In this example, a 116MB heap is quite large for a mobile application, as well\nas over 46,000 C# &lt;-&gt; Java wrapper objects!<\/p>\n<p>To truly determine if the sub page is leaking, we can make a couple\nmodifications to a .NET MAUI application:<\/p>\n<ol>\n<li>Add logging in a finalizer. For example:<\/li>\n<\/ol>\n<pre><code class=\"language-csharp\">~MyPage() =&gt; Console.WriteLine(\"Finalizer for ~MyPage()\");<\/code><\/pre>\n<p>While navigating through your app, you can find out if entire pages are living\nforever if the log message is never displayed. This is a common symptom of a leak,\nbecause any <code>View<\/code> holds <code>.Parent.Parent.Parent<\/code>, etc. all the way up to the\n<code>Page<\/code> object.<\/p>\n<ol start=\"2\">\n<li>Call <code>GC.Collect()<\/code> somewhere in the app, such as the sub page&#8217;s constructor:<\/li>\n<\/ol>\n<pre><code class=\"language-csharp\">public MyPage()\r\n{\r\n    GC.Collect(); \/\/ For debugging purposes only, remove later\r\n    InitializeComponent();\r\n}<\/code><\/pre>\n<p>This makes the GC more deterministic, in that we are forcing it to run more\nfrequently. Each time we navigate to the sub page, we are more likely causing\nthe old sub page&#8217;s to go away. If things are working properly, we should see the\nlog message from the finalizer.<\/p>\n<blockquote>\n<p><strong>Note<\/strong>\n<code>GC.Collect()<\/code> is for debugging purposes only. You should not need this in\nyour app after investigation is complete, so be sure to remove it afterward.<\/p>\n<\/blockquote>\n<ol start=\"3\">\n<li>With these changes in place, test a <code>Release<\/code> build of your app.<\/li>\n<\/ol>\n<p>On iOS, Android, macOS, etc. you can watch console output of your app to\ndetermine what is actually happening at runtime. <a href=\"https:\/\/learn.microsoft.com\/xamarin\/android\/deploy-test\/debugging\/android-debug-log?tabs=windows#accessing-from-the-command-line\"><code>adb logcat<\/code><\/a>, for\nexample, is a way to view these logs on Android.<\/p>\n<p>If running on Windows, you can also use <code>Debug &gt; Windows &gt; Diagnostic Tools<\/code>\ninside Visual Studio to take memory snapshots inside Visual Studio. In the\nfuture, we would like Visual Studio&#8217;s diagnostic tooling to support .NET MAUI\napplications running on other platforms.<\/p>\n<p>See our <a href=\"https:\/\/github.com\/dotnet\/maui\/wiki\/Memory-Leaks\">memory leaks wiki page<\/a> for more information related to\nmemory leaks in .NET MAUI applications.<\/p>\n<h3>Patterns that cause leaks: C# events<\/h3>\n<p>C# events, just like a field, property, etc. can create strong references\nbetween objects. Let&#8217;s look at a situation where things can go wrong.<\/p>\n<p>Take for example, the cross-platform <code>Grid.ColumnDefinitions<\/code> property:<\/p>\n<pre><code class=\"language-csharp\">public class Grid : Layout, IGridLayout\r\n{\r\n    public static readonly BindableProperty ColumnDefinitionsProperty = BindableProperty.Create(\"ColumnDefinitions\",\r\n        typeof(ColumnDefinitionCollection), typeof(Grid), null, validateValue: (bindable, value) =&gt; value != null,\r\n        propertyChanged: UpdateSizeChangedHandlers, defaultValueCreator: bindable =&gt;\r\n        {\r\n            var colDef = new ColumnDefinitionCollection();\r\n            colDef.ItemSizeChanged += ((Grid)bindable).DefinitionsChanged;\r\n            return colDef;\r\n        });\r\n\r\n    public ColumnDefinitionCollection ColumnDefinitions\r\n    {\r\n        get { return (ColumnDefinitionCollection)GetValue(ColumnDefinitionsProperty); }\r\n        set { SetValue(ColumnDefinitionsProperty, value); }\r\n    }<\/code><\/pre>\n<ul>\n<li>\n<p><code>Grid<\/code> has a strong reference to its <code>ColumnDefinitionCollection<\/code> via the\n<code>BindableProperty<\/code>.<\/p>\n<\/li>\n<li>\n<p><code>ColumnDefinitionCollection<\/code> has a strong reference to <code>Grid<\/code> via the\n<code>ItemSizeChanged<\/code> event.<\/p>\n<\/li>\n<\/ul>\n<p>If you put a breakpoint on the line with <code>ItemSizeChanged +=<\/code>, you can see the\nevent has an <code>EventHandler<\/code> object where the <code>Target<\/code> is a strong reference back\nto the <code>Grid<\/code>.<\/p>\n<p>In some cases, circular references like this are completely OK. The .NET\nruntime(s)&#8217; garbage collectors know how to collect cycles of objects that point\neach other. When there is no &#8220;root&#8221; object holding them both, they can both go\naway.<\/p>\n<p>The problem comes in with object lifetimes: what happens if the\n<code>ColumnDefinitionCollection<\/code> lives for the life of the entire application?<\/p>\n<p>Consider the following <code>Style<\/code> in <code>Application.Resources<\/code> or\n<code>Resources\/Styles\/Styles.xaml<\/code>:<\/p>\n<pre><code class=\"language-xml\">&lt;Style TargetType=\"Grid\" x:Key=\"GridStyleWithColumnDefinitions\"&gt;\r\n    &lt;Setter Property=\"ColumnDefinitions\" Value=\"18,*\"\/&gt;\r\n&lt;\/Style&gt;<\/code><\/pre>\n<p>If you applied this <code>Style<\/code> to a <code>Grid<\/code> on a random <code>Page<\/code>:<\/p>\n<ul>\n<li><code>Application<\/code>&#8216;s main <code>ResourceDictionary<\/code> holds the <code>Style<\/code>.<\/li>\n<li>The <code>Style<\/code> holds a <code>ColumnDefinitionCollection<\/code>.<\/li>\n<li>The <code>ColumnDefinitionCollection<\/code> holds the <code>Grid<\/code>.<\/li>\n<li><code>Grid<\/code> unfortunately holds the <code>Page<\/code> via <code>.Parent.Parent.Parent<\/code>, etc.<\/li>\n<\/ul>\n<p>This situation could cause entire <code>Page<\/code>&#8216;s to live forever!<\/p>\n<blockquote>\n<p><strong>Note<\/strong>\nThe issue with <code>Grid<\/code> is fixed in <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/16145\">maui#16145<\/a>, but is an\nexcellent example of illustrating how C# events can go wrong.<\/p>\n<\/blockquote>\n<h3>Circular references on Apple platforms<\/h3>\n<p>Even since the early days of <a href=\"https:\/\/stackoverflow.com\/questions\/13058521\/is-this-a-bug-in-monotouch-gc\/13059140#13059140\">Xamarin.iOS<\/a>, there has existed an\nissue with &#8220;circular references&#8221; even in a garbage-collected runtime like .NET.\nC# objects co-exist with a reference-counted world on Apple platforms, and so a\nC# object that subclasses <code>NSObject<\/code> can run into situations where they can\naccidentally live forever &#8212; a memory leak. This is not a .NET-specific problem,\nas you can just as easily create the same situation in Objective-C or Swift.\nNote that this does not occur on Android or Windows platforms.<\/p>\n<p>Take for example, the following circular reference:<\/p>\n<pre><code class=\"language-csharp\">class MyViewSubclass : UIView\r\n{\r\n    public UIView? Parent { get; set; }\r\n\r\n    public void Add(MyViewSubclass subview)\r\n    {\r\n        subview.Parent = this;\r\n        AddSubview(subview);\r\n    }\r\n}\r\n\r\n\/\/...\r\n\r\nvar parent = new MyViewSubclass();\r\nvar view = new MyViewSubclass();\r\nparent.Add(view);<\/code><\/pre>\n<p>In this case:<\/p>\n<ul>\n<li><code>parent<\/code> -&gt; <code>view<\/code> via <code>Subviews<\/code><\/li>\n<li><code>view<\/code> -&gt; <code>parent<\/code> via the <code>Parent<\/code> property<\/li>\n<li>The reference count of both objects is non-zero.<\/li>\n<li>Both objects live forever.<\/li>\n<\/ul>\n<p>This problem isn&#8217;t limited to a field or property, you can create similar\nsituations with C# events:<\/p>\n<pre><code class=\"language-csharp\">class MyView : UIView\r\n{\r\n    public MyView()\r\n    {\r\n        var picker = new UIDatePicker();\r\n        AddSubview(picker);\r\n        picker.ValueChanged += OnValueChanged;\r\n    }\r\n\r\n    void OnValueChanged(object? sender, EventArgs e) { }\r\n\r\n    \/\/ Use this instead and it doesn't leak!\r\n    \/\/static void OnValueChanged(object? sender, EventArgs e) { }\r\n}<\/code><\/pre>\n<p>In this case:<\/p>\n<ul>\n<li><code>MyView<\/code> -&gt; <code>UIDatePicker<\/code> via <code>Subviews<\/code><\/li>\n<li><code>UIDatePicker<\/code> -&gt; <code>MyView<\/code> via <code>ValueChanged<\/code> and <code>EventHandler.Target<\/code><\/li>\n<li>Both objects live forever.<\/li>\n<\/ul>\n<p>A solution for this example, is to make <code>OnValueChanged<\/code> method <code>static<\/code>, which\nwould result in a <code>null<\/code> <code>Target<\/code> on the <code>EventHandler<\/code> instance.<\/p>\n<p>Another solution, would be to put <code>OnValueChanged<\/code> in a non-<code>NSObject<\/code> subclass:<\/p>\n<pre><code class=\"language-csharp\">class MyView : UIView\r\n{\r\n    readonly Proxy _proxy = new();\r\n\r\n    public MyView()\r\n    {\r\n        var picker = new UIDatePicker();\r\n        AddSubview(picker);\r\n        picker.ValueChanged += _proxy.OnValueChanged;\r\n    }\r\n\r\n    class Proxy\r\n    {\r\n        public void OnValueChanged(object? sender, EventArgs e) { }\r\n    }\r\n}<\/code><\/pre>\n<p>This is the pattern we&#8217;ve used in most .NET MAUI handlers and other <code>UIView<\/code>\nsubclasses.<\/p>\n<p>See the <a href=\"https:\/\/github.com\/jonathanpeppers\/MemoryLeaksOniOS\">MemoryLeaksOniOS<\/a> sample repo, if you would like to play with\nsome of these scenarios in isolation in an iOS application without .NET MAUI.<\/p>\n<h3>Roslyn analyzer for Apple platforms<\/h3>\n<p>We also have an experimental <a href=\"https:\/\/github.com\/jonathanpeppers\/memory-analyzers\">Roslyn Analyzer<\/a> that can detect\nthese situations at build time. To add it to <code>net7.0-ios<\/code>, <code>net8.0-ios<\/code>, etc.\nprojects, you can simply install a NuGet package:<\/p>\n<pre><code class=\"language-xml\">&lt;PackageReference Include=\"MemoryAnalyzers\" Version=\"0.1.0-beta.3\" PrivateAssets=\"all\" \/&gt;<\/code><\/pre>\n<p>Some examples of a warning would be:<\/p>\n<pre><code class=\"language-csharp\">public class MyView : UIView\r\n{\r\n    public event EventHandler MyEvent;\r\n}<\/code><\/pre>\n<pre><code class=\"language-text\">Event 'MyEvent' could could memory leaks in an NSObject subclass.\r\nRemove the event or add the [UnconditionalSuppressMessage(\"Memory\", \"MA0001\")]\r\nattribute with a justification as to why the event will not leak.<\/code><\/pre>\n<p>Note that the analyzer can warns if there <em>might<\/em> be an issue, so it can be\nquite noisy to enable in a large, existing codebase. Inspecting memory at\nruntime is the best way to determine if there is truly a memory leak.<\/p>\n<h2>Tooling and Documentation<\/h2>\n<h3>Simplified <code>dotnet-trace<\/code> and <code>dotnet-dsrouter<\/code><\/h3>\n<p>In .NET 7, profiling a mobile application was a bit of a challenge. You had to\nrun <code>dotnet-dsrouter<\/code> and <code>dotnet-trace<\/code> together and get all the settings right\nto be able to retrieve a <code>.nettrace<\/code> or <a href=\"https:\/\/www.speedscope.app\/\">speedscope<\/a> file for\nperformance investigations. There was also no built-in support for\n<code>dotnet-gcdump<\/code> to connect to <code>dotnet-dsrouter<\/code> to get memory snapshots of a\nrunning .NET MAUI application.<\/p>\n<p>In .NET 8, we&#8217;ve streamlined this scenario by making new commands for\n<code>dotnet-dsrouter<\/code> that simplifies the workflow.<\/p>\n<p>To verify you have the latest diagnostic tooling, you can install them via:<\/p>\n<pre><code class=\"language-bash\">$ dotnet tool install -g dotnet-dsrouter\r\nYou can invoke the tool using the following command: dotnet-dsrouter\r\nTool 'dotnet-dsrouter' was successfully installed.\r\n$ dotnet tool install -g dotnet-gcdump\r\nYou can invoke the tool using the following command: dotnet-gcdump\r\nTool 'dotnet-gcdump' was successfully installed.\r\n$ dotnet tool install -g dotnet-trace\r\nYou can invoke the tool using the following command: dotnet-trace\r\nTool 'dotnet-trace' was successfully installed.<\/code><\/pre>\n<p>Verify you have at least <a href=\"https:\/\/github.com\/dotnet\/diagnostics\/releases\/tag\/v8.0.452401\">8.x versions<\/a>\nof these tools:<\/p>\n<pre><code class=\"language-bash\">$ dotnet tool list -g\r\nPackage Id                         Version                       Commands\r\n--------------------------------------------------------------------------------------\r\ndotnet-dsrouter                    8.0.452401                    dotnet-dsrouter\r\ndotnet-gcdump                      8.0.452401                    dotnet-gcdump\r\ndotnet-trace                       8.0.452401                    dotnet-trace<\/code><\/pre>\n<p>To profile an Android application on an Android emulator, first build and\ninstall your application in <code>Release<\/code> mode such as:<\/p>\n<pre><code class=\"language-bash\">$ dotnet build -f net8.0-android -t:Install -c Release -p:AndroidEnableProfiler=true\r\nBuild SUCCEEDED.\r\n    0 Warning(s)\r\n    0 Error(s)<\/code><\/pre>\n<p>Next, open a terminal to run <code>dotnet-dsrouter<\/code><\/p>\n<pre><code class=\"language-bash\">$ dotnet-dsrouter android-emu\r\nStart an application on android emulator with one of the following environment variables set:\r\nDOTNET_DiagnosticPorts=10.0.2.2:9000,nosuspend,connect\r\nDOTNET_DiagnosticPorts=10.0.2.2:9000,suspend,connect<\/code><\/pre>\n<p>Then in a second terminal window, we can set the <code>debug.mono.profile<\/code> Android\nsystem property, as the stand-in for <code>$DOTNET_DiagnosticPorts<\/code>:<\/p>\n<pre><code class=\"language-bash\">$ adb shell setprop debug.mono.profile '10.0.2.2:9000,suspend,connect'\r\n$ dotnet-trace ps\r\n3248  dotnet-dsrouter\r\n$ dotnet-trace collect -p 3248 --format speedscope\r\n...\r\n[00:00:00:09]   Recording trace 3.2522   (MB)\r\nPress &lt;Enter&gt; or &lt;Ctrl+C&gt; to exit...<\/code><\/pre>\n<blockquote>\n<p><strong>Note<\/strong>\nAndroid doesn&#8217;t have good support for environment variables like\n<code>$DOTNET_DiagnosticPorts<\/code>. You can create an\n<a href=\"https:\/\/learn.microsoft.com\/xamarin\/android\/deploy-test\/building-apps\/build-items#androidenvironment\"><code>AndroidEnvironment<\/code><\/a> text file for setting environment\nvariables, but Android system properties can be simpler as they would not\nrequire rebuilding the application to set them.<\/p>\n<\/blockquote>\n<p>Upon launching the Android application, it should be able to connect to\n<code>dotnet-dsrouter<\/code> -&gt; <code>dotnet-trace<\/code> and record performance profiling information\nfor investigation. The <code>--format<\/code> argument is optional and it defaults to\n<code>.nettrace<\/code>. However, <code>.nettrace<\/code> files can be viewed only with Perfview on\nWindows, while the speedscope JSON files can be viewed &#8220;on&#8221; macOS or Linux by\nuploading them to <a href=\"https:\/\/www.speedscope.app\/\">https:\/\/speedscope.app<\/a>.<\/p>\n<blockquote>\n<p><strong>Note<\/strong>\nWhen providing a process ID to <code>dotnet-trace<\/code>, it knows how to tell if a\nprocess ID is <code>dotnet-dsrouter<\/code> and connect <em>through<\/em> it appropriately.<\/p>\n<\/blockquote>\n<p><code>dotnet-dsrouter<\/code> has the following new commands to simplify the workflow:<\/p>\n<ul>\n<li><code>dotnet-dsrouter android<\/code>: Android devices<\/li>\n<li><code>dotnet-dsrouter android-emu<\/code>: Android emulators<\/li>\n<li><code>dotnet-dsrouter ios<\/code>: iOS devices<\/li>\n<li><code>dotnet-dsrouter ios-sim<\/code>: iOS simulators<\/li>\n<\/ul>\n<p>See the <a href=\"https:\/\/aka.ms\/profile-maui\">.NET MAUI wiki<\/a> for more information about profiling .NET\nMAUI applications on each platform.<\/p>\n<h3><code>dotnet-gcdump<\/code> Support for Mobile<\/h3>\n<p>In .NET 7, we had a somewhat complex method (<a href=\"https:\/\/aka.ms\/profile-maui\">see wiki<\/a>) for\ngetting a memory snapshot of an application on the Mono runtime (such as iOS or\nAndroid). You had to use a Mono-specific event provider such as:<\/p>\n<pre><code class=\"language-bash\">dotnet-trace collect --diagnostic-port \/tmp\/maui-app --providers Microsoft-DotNETRuntimeMonoProfiler:0xC900001:4<\/code><\/pre>\n<p>And then we relied on Filip Navara&#8217;s <a href=\"https:\/\/github.com\/filipnavara\/mono-gcdump\"><code>mono-gcdump<\/code><\/a> tool (thanks\nFilip!) to convert the <code>.nettrace<\/code> file to <code>.gcdump<\/code> to be opened in Visual\nStudio or PerfView.<\/p>\n<p>In .NET 8, we now have <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/diagnostics\/dotnet-gcdump\"><code>dotnet-gcdump<\/code><\/a> support for mobile\nscenarios. If you want to get a memory snapshot of a running application, you\ncan use <code>dotnet-gcdump<\/code> in a similar fashion as <code>dotnet-trace<\/code>:<\/p>\n<pre><code class=\"language-bash\">$ dotnet-gcdump ps\r\n3248  dotnet-dsrouter\r\n$ dotnet-gcdump collect -p 3248\r\nWriting gcdump to '20231018_115631_29880.gcdump'...<\/code><\/pre>\n<blockquote>\n<p><strong>Note<\/strong>\nThis requires the exact same setup as <code>dotnet-trace<\/code>, such as\n<code>-p:AndroidEnableProfiler=true<\/code>, <code>dotnet-dsrouter<\/code>, <code>adb<\/code> commands, etc.<\/p>\n<\/blockquote>\n<p>This greatly streamlines our workflow for investigating memory leaks in .NET\nMAUI applications. See our <a href=\"https:\/\/github.com\/dotnet\/maui\/wiki\/Memory-Leaks\">memory leaks wiki page<\/a> for more\ninformation.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>What improvements did we bring to .NET MAUI in .NET 8? Click to find out more!<\/p>\n","protected":false},"author":1345,"featured_media":48595,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7233,3009],"tags":[7701,7238,108],"class_list":["post-48594","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-maui","category-performance","tag-dotnet-8","tag-net-maui","tag-performance"],"acf":[],"blog_post_summary":"<p>What improvements did we bring to .NET MAUI in .NET 8? Click to find out more!<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/48594","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=48594"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/48594\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/48595"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=48594"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=48594"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=48594"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}