{"id":40341,"date":"2022-06-07T09:45:17","date_gmt":"2022-06-07T16:45:17","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=40341"},"modified":"2022-06-24T09:09:43","modified_gmt":"2022-06-24T16:09:43","slug":"performance-improvements-in-dotnet-maui","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-dotnet-maui\/","title":{"rendered":"Performance Improvements in .NET MAUI"},"content":{"rendered":"<p>.NET Multi-platform App UI (MAUI) unifies Android, iOS, macOS, and Windows APIs into a single\nAPI so you can write one app that runs natively on many platforms. We are\nfocused on improving both your daily productivity\nas well as performance of your applications. Gains in developer\nproductivity, we believe, should not be at the cost of application\nperformance.<\/p>\n<p>The same could be said about application size &#8212; what overhead is\npresent in a blank .NET MAUI application? When we began optimizing\n.NET MAUI, it was clear that iOS had some work needed to improve\napplication size, while Android was lacking in startup performance.<\/p>\n<p>iOS application size of a <code>dotnet new maui<\/code> project was originally\naround 18MB. Likewise .NET MAUI startup times on Android in earlier\npreviews were not looking too good:<\/p>\n<table>\n<thead>\n<tr>\n<th>Application<\/th>\n<th>Framework<\/th>\n<th style=\"text-align: right;\">Startup Time(ms)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Xamarin.Android<\/td>\n<td>Xamarin<\/td>\n<td style=\"text-align: right;\">306.5<\/td>\n<\/tr>\n<tr>\n<td>Xamarin.Forms<\/td>\n<td>Xamarin<\/td>\n<td style=\"text-align: right;\">498.6<\/td>\n<\/tr>\n<tr>\n<td>Xamarin.Forms (Shell)<\/td>\n<td>Xamarin<\/td>\n<td style=\"text-align: right;\">817.7<\/td>\n<\/tr>\n<tr>\n<td>dotnet new android<\/td>\n<td>.NET 6 (Early Preview)<\/td>\n<td style=\"text-align: right;\">210.5<\/td>\n<\/tr>\n<tr>\n<td>dotnet new maui<\/td>\n<td>.NET 6 (Early Preview)<\/td>\n<td style=\"text-align: right;\">683.9<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast<\/a><\/td>\n<td>.NET 6 (Early Preview)<\/td>\n<td style=\"text-align: right;\">1299.9<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>This is an average of ten runs on a Pixel 5 device. See our\n<a href=\"https:\/\/github.com\/jonathanpeppers\/maui-profiling\">maui-profiling<\/a> repo for details on how these numbers were\nobtained.<\/p>\n<p>Our goal was for .NET MAUI to be <em>faster<\/em> than its predecessor,\nXamarin.Forms, and it was clear that we had some work to do in .NET\nMAUI itself. The <code>dotnet new android<\/code> template was already shaping up\nto launch faster than Xamarin.Android, mostly due to the new BCL and\nMono runtime in .NET 6.<\/p>\n<p>The <code>dotnet new maui<\/code> template was not yet using the <a href=\"https:\/\/docs.microsoft.com\/xamarin\/xamarin-forms\/app-fundamentals\/shell\/\">Shell<\/a>\nnavigation pattern, but plans were in the works for it to be the\ndefault navigation pattern in .NET MAUI. We knew there would be a\nperformance-hit in the template when we adopted this change.<\/p>\n<p>To arrive at where we are today, it was a collaboration of several\ndifferent teams. We improved areas like Microsoft.Extensions and\nDependencyInjection usage, AOT compilation, Java interop, XAML, code\nin .NET MAUI in general, and many more.<\/p>\n<p>After the dust settled, we arrived at a much better place:<\/p>\n<table style=\"width: 44.62669492060568%;\">\n<thead>\n<tr>\n<th style=\"width: 45.70189770013825%;\">Application<\/th>\n<th style=\"width: 28.015864522855026%;\">Framework<\/th>\n<th style=\"text-align: right; width: 54.90053418171998%;\">Startup Time(ms)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"width: 45.70189770013825%;\">Xamarin.Android<\/td>\n<td style=\"width: 28.015864522855026%;\">Xamarin<\/td>\n<td style=\"text-align: right; width: 54.90053418171998%;\">306.5<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 45.70189770013825%;\">Xamarin.Forms<\/td>\n<td style=\"width: 28.015864522855026%;\">Xamarin<\/td>\n<td style=\"text-align: right; width: 54.90053418171998%;\">498.6<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 45.70189770013825%;\">Xamarin.Forms (Shell)<\/td>\n<td style=\"width: 28.015864522855026%;\">Xamarin<\/td>\n<td style=\"text-align: right; width: 54.90053418171998%;\">817.7<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 45.70189770013825%;\">dotnet new android<\/td>\n<td style=\"width: 28.015864522855026%;\">.NET 6 (MAUI GA)<\/td>\n<td style=\"text-align: right; width: 54.90053418171998%;\">182.8<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 45.70189770013825%;\">dotnet new maui (No Shell**)<\/td>\n<td style=\"width: 28.015864522855026%;\">.NET 6 (MAUI GA)<\/td>\n<td style=\"text-align: right; width: 54.90053418171998%;\">464.2<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 45.70189770013825%;\">dotnet new maui (Shell)<\/td>\n<td style=\"width: 28.015864522855026%;\">.NET 6 (MAUI GA)<\/td>\n<td style=\"text-align: right; width: 54.90053418171998%;\">568.1<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 45.70189770013825%;\"><a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast App (Shell)<\/a><\/td>\n<td style=\"width: 28.015864522855026%;\">.NET 6 (MAUI GA)<\/td>\n<td style=\"text-align: right; width: 54.90053418171998%;\">814.2<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>**<\/code> &#8211; this is the original <code>dotnet new maui<\/code> template that is not using <a href=\"https:\/\/docs.microsoft.com\/xamarin\/xamarin-forms\/app-fundamentals\/shell\/\">Shell<\/a>.<\/p>\n<p>Details below, enjoy!<\/p>\n<h2>Table Of Contents<\/h2>\n<p>Startup Performance Improvements<\/p>\n<ul>\n<li><a href=\"#profiling-on-mobile\">Profiling on Mobile<\/a><\/li>\n<li><a href=\"#measuring-over-time\">Measuring Over Time<\/a><\/li>\n<li><a href=\"#profiled-aot\">Profiled AOT<\/a><\/li>\n<li><a href=\"#single-file-assembly-stores\">Single-file Assembly Stores<\/a><\/li>\n<li><a href=\"#spanify-registernativemembers\">Spanify RegisterNativeMembers<\/a><\/li>\n<li><a href=\"#systemreflectionemit-and-constructors\">System.Reflection.Emit and Constructors<\/a><\/li>\n<li><a href=\"#systemreflectionemit-and-methods\">System.Reflection.Emit and Methods<\/a><\/li>\n<li><a href=\"#newer-javainterop-apis\">Newer Java.Interop APIs<\/a><\/li>\n<li><a href=\"#multi-dimensional-java-arrays\">Multi-dimensional Java Arrays<\/a><\/li>\n<li><a href=\"#use-glide-for-android-images\">Use Glide for Android Images<\/a><\/li>\n<li><a href=\"#reduce-java-interop-calls\">Reduce Java Interop Calls<\/a><\/li>\n<li><a href=\"#port-android-xml-to-java\">Port Android XML to Java<\/a><\/li>\n<li><a href=\"#remove-microsoftextensionshosting\">Remove Microsoft.Extensions.Hosting<\/a><\/li>\n<li><a href=\"#less-shell-initialization-on-startup\">Less Shell Initialization on Startup<\/a><\/li>\n<li><a href=\"#fonts-should-not-use-temporary-files\">Fonts Should Not Use Temporary Files<\/a><\/li>\n<li><a href=\"#compute-onplatform-at-compile-time\">Compute OnPlatform at Compile-time<\/a><\/li>\n<li><a href=\"#use-compiled-converters-in-xaml\">Use Compiled Converters in XAML<\/a><\/li>\n<li><a href=\"#optimize-color-parsing\">Optimize Color Parsing<\/a><\/li>\n<li><a href=\"#dont-use-culture-aware-string-comparisons\">Don&#8217;t Use Culture-aware String Comparisons<\/a><\/li>\n<li><a href=\"#create-loggers-lazily\">Create Loggers Lazily<\/a><\/li>\n<li><a href=\"#use-factory-methods-for-dependency-injection\">Use Factory Methods for Dependency Injection<\/a><\/li>\n<li><a href=\"#load-configurationmanager-lazily\">Load ConfigurationManager Lazily<\/a><\/li>\n<li><a href=\"#default-verifydependencyinjectionopengenericservicetrimmability\">Default VerifyDependencyInjectionOpenGenericServiceTrimmability<\/a><\/li>\n<li><a href=\"#improve-the-built-in-aot-profile\">Improve the Built-in AOT Profile<\/a><\/li>\n<li><a href=\"#enable-lazy-loading-of-aot-images\">Enable Lazy-loading of AOT Images<\/a><\/li>\n<li><a href=\"#remove-unused-encoding-object-in-systemuri\">Remove Unused Encoding Object in System.Uri<\/a><\/li>\n<\/ul>\n<p>App Size Improvements<\/p>\n<ul>\n<li><a href=\"#fix-defaults-for-mauiimage-sizes\">Fix defaults for MauiImage Sizes<\/a><\/li>\n<li><a href=\"#remove-applicationproperties-and-datacontractserializer\">Remove Application.Properties and DataContractSerializer<\/a><\/li>\n<li><a href=\"#trim-unused-http-implementations\">Trim Unused HTTP Implementations<\/a><\/li>\n<\/ul>\n<p>Improvements in the <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast<\/a> Sample<\/p>\n<ul>\n<li><a href=\"#remove-microsoftextensionshttp-usage\">Remove Microsoft.Extensions.Http Usage<\/a><\/li>\n<li><a href=\"#remove-newtonsoftjson-usage\">Remove Newtonsoft.Json Usage<\/a><\/li>\n<li><a href=\"#run-first-network-request-in-background\">Run First Network Request in Background<\/a><\/li>\n<\/ul>\n<p>Experimental or Advanced Options<\/p>\n<ul>\n<li><a href=\"#trimming-resourcedesignercs\">Trimming Resource.designer.cs<\/a><\/li>\n<li><a href=\"#r8-java-code-shrinker\">R8 Java Code Shrinker<\/a><\/li>\n<li><a href=\"#aot-everything\">AOT Everything<\/a><\/li>\n<li><a href=\"#aot-and-llvm\">AOT and LLVM<\/a><\/li>\n<li><a href=\"#record-a-custom-aot-profile\">Record a Custom AOT Profile<\/a><\/li>\n<\/ul>\n<h2>Startup Performance Improvements<\/h2>\n<h3>Profiling on Mobile<\/h3>\n<p>I have to mention the .NET diagnostic tooling available for mobile\nplatforms, as it was our step number 0 for making .NET MAUI faster.<\/p>\n<p>Profiling .NET 6 Android applications requires usage of a tool called\n<a href=\"https:\/\/docs.microsoft.com\/dotnet\/core\/diagnostics\/dotnet-dsrouter\"><code>dotnet-dsrouter<\/code><\/a>. This tool enables <code>dotnet trace<\/code> to connect to\na running mobile application on Android, iOS, etc. This was probably\nthe most impactful tool we used for profiling .NET MAUI.<\/p>\n<p>To get started with <code>dotnet trace<\/code> and <code>dsrouter<\/code>, begin by\nconfiguring some settings via <code>adb<\/code> and launching <code>dsrouter<\/code>:<\/p>\n<pre><code class=\"language-dotnetcli\">adb reverse tcp:9000 tcp:9001\r\nadb shell setprop debug.mono.profile '127.0.0.1:9000,suspend'\r\ndotnet-dsrouter client-server -tcps 127.0.0.1:9001 -ipcc \/tmp\/maui-app --verbose debug<\/code><\/pre>\n<p>Next launch <code>dotnet trace<\/code>, such as:<\/p>\n<pre><code class=\"language-dotnetcli\">dotnet-trace collect --diagnostic-port \/tmp\/maui-app --format speedscope<\/code><\/pre>\n<p>After launching an Android app built with <code>-c Release<\/code> and\n<code>-p:AndroidEnableProfiler=true<\/code>, you&#8217;ll notice the connection when\n<code>dotnet trace<\/code> outputs:<\/p>\n<pre><code class=\"language-dotnetcli\">Press &lt;Enter&gt; or &lt;Ctrl+C&gt; to exit...812  (KB)<\/code><\/pre>\n<p>Simply press enter after your application is fully launched to get a\n<code>*.speedscope<\/code> saved in the current directory. You can open this file\nat <a href=\"https:\/\/speedscope.app\">https:\/\/speedscope.app<\/a>, for an in-depth look into the time each\nmethod takes during application startup:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/speedscope-1.png\" alt=\"speedscope view\" \/><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/speedscope-2.png\" alt=\"speedscope view\" \/><\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/blob\/main\/Documentation\/guides\/tracing.md\">our documentation<\/a> for further details using <code>dotnet trace<\/code> in\nAndroid applications. I would recommend profiling <code>Release<\/code> builds on\na physical Android device to get the best picture of the real-world\nperformance of your app.<\/p>\n<h3>Measuring Over Time<\/h3>\n<p>Our friends in the .NET fundamentals team setup a pipeline to track\n.NET MAUI performance scenarios, such as:<\/p>\n<ul>\n<li>Package size<\/li>\n<li>On disk size (uncompressed)<\/li>\n<li>Individual file breakdown<\/li>\n<li>Application startup<\/li>\n<\/ul>\n<p>This allowed us to see the impact of improvements or regressions over\ntime, seeing numbers for each commit of the dotnet\/maui repo. We could\nalso determine if the difference was caused by changes in\nxamarin-android, xamarin-macios, or dotnet\/runtime.<\/p>\n<p>So for example, a graph of startup time (in milliseconds) of the\n<code>dotnet new maui<\/code> template, running on a physical Pixel 4a device:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/graph-1.png\" alt=\"graph of startup .NET MAUI template\" \/><\/p>\n<p><em>Note that the Pixel 4a is considerably slower than the Pixel 5.<\/em><\/p>\n<p>We could pinpoint the commit in dotnet\/maui where regressions and\nimprovements occurred. It can&#8217;t be understated how useful this was for\ntracking our goals.<\/p>\n<p>Likewise, we could see our progress in the <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast<\/a> app over\ntime on the same Pixel 4a device(s):<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/graph-2.png\" alt=\"graph of startup .NET Podcast sample\" \/><\/p>\n<p>This graph was our true focus, as it was a &#8220;real application&#8221; close to\nwhat developers would see in their own mobile apps.<\/p>\n<p>As for application size, it is a much more stable number &#8212; making it\nvery easy to zero in when things got worse or better:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2022\/06\/graph-3.png\" alt=\"graph of app size .NET Podcast sample\" \/><\/p>\n<p>See <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\/pull\/58\">dotnet-podcasts#58<\/a>, <a href=\"https:\/\/github.com\/xamarin\/AndroidX\/pull\/520\">AndroidX#520<\/a>, and\n<a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/6419\">dotnet\/maui#6419<\/a> for details on these improvements.<\/p>\n<h3>Profiled AOT<\/h3>\n<p>During our initial performance tests of .NET MAUI, we saw how JIT\n(just in time) vs AOT (ahead of time) compiled code can perform:<\/p>\n<table>\n<thead>\n<tr>\n<th>Application<\/th>\n<th style=\"text-align: right;\">JIT Time(ms)<\/th>\n<th style=\"text-align: right;\">AOT Time(ms)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>dotnet new maui<\/td>\n<td style=\"text-align: right;\">1078.0ms<\/td>\n<td style=\"text-align: right;\">683.9ms<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>JIT-ing happens the first time each C# method is called, which\nimplicitly impacts startup performance in mobile applications.<\/p>\n<p>Also problematic is the app size increase caused by AOT. An Android\nnative library is added to the final app for every .NET assembly. To\ngive the best of both worlds, <a href=\"https:\/\/devblogs.microsoft.com\/xamarin\/faster-startup-times-with-startup-tracing-on-android\/\">startup tracing or Profiled AOT<\/a> is\na current feature of Xamarin.Android. This is a mechanism for AOT&#8217;ing\nthe startup path of applications, which improves launch times\nsignificantly with only a modest app size increase.<\/p>\n<p>It made complete sense for this to be the default option for <code>Release<\/code>\nbuilds in .NET 6. In the past, the Android NDK was required (a\nmultiple gigabyte download) for doing AOT of any kind with\nXamarin.Android. We did the legwork for building AOT&#8217;d applications\nwithout an Android NDK installed, making it possible to be the default\ngoing forward.<\/p>\n<p>We recorded built-in profiles for <code>dotnet new android<\/code>, <code>maui<\/code>, and\n<code>maui-blazor<\/code> templates that benefit most applications. If you would\nlike to record a custom profile in .NET 6, you can try our\nexperimental <a href=\"https:\/\/github.com\/jonathanpeppers\/Mono.Profiler.Android\">Mono.Profiler.Android<\/a> package. We are working on\nfull support for recording custom profiles in a future .NET release.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6547\">xamarin-android#6547<\/a> and <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4859\">dotnet\/maui#4859<\/a> for\ndetails about this improvement.<\/p>\n<h3>Single-file Assembly Stores<\/h3>\n<p>Previously, if you reviewed a <code>Release<\/code> Android <code>.apk<\/code> contents in\nyour favorite zip-file utility, you can see .NET assemblies located at:<\/p>\n<pre><code class=\"language-dotnetcli\">assemblies\/Java.Interop.dll\r\nassemblies\/Mono.Android.dll\r\nassemblies\/System.Runtime.dll\r\nassemblies\/arm64-v8a\/System.Private.CoreLib.dll\r\nassemblies\/armeabi-v7a\/System.Private.CoreLib.dll\r\nassemblies\/x86\/System.Private.CoreLib.dll\r\nassemblies\/x86_64\/System.Private.CoreLib.dll<\/code><\/pre>\n<p>These files were loaded individually with the <a href=\"https:\/\/man7.org\/linux\/man-pages\/man2\/mmap.2.html\"><code>mmap<\/code> system\ncall<\/a>, which was a cost per .NET assembly inside the app. This is\nimplemented in C\/C++ in the Android workload, using a callback that\nthe Mono runtime provides for assembly loading. MAUI applications have\na lot of assemblies, so we introduced a new\n<code>$(AndroidUseAssemblyStore)<\/code> feature that is enabled by default for\n<code>Release<\/code> builds.<\/p>\n<p>After this change, you end up with:<\/p>\n<pre><code class=\"language-dotnetcli\">assemblies\/assemblies.manifest\r\nassemblies\/assemblies.blob\r\nassemblies\/assemblies.arm64_v8a.blob\r\nassemblies\/assemblies.armeabi_v7a.blob\r\nassemblies\/assemblies.x86.blob\r\nassemblies\/assemblies.x86_64.blob<\/code><\/pre>\n<p>Now Android startup only has to call <a href=\"https:\/\/man7.org\/linux\/man-pages\/man2\/mmap.2.html\"><code>mmap<\/code><\/a> twice: once for\n<code>assemblies.blob<\/code>, and a second time for the architecture-specific\nblob. This had a noticeable impact on applications with many .NET\nassemblies.<\/p>\n<p>If you need to inspect the IL, of these assemblies from a compiled\nAndroid application, we created an <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/tree\/main\/tools\/assembly-store-reader\">assembly-store-reader<\/a> tool\nfor &#8220;unpacking&#8221; these files.<\/p>\n<p>Another option is to build your application with these settings\ndisabled:<\/p>\n<pre><code class=\"language-dotnetcli\">dotnet build -c Release -p:AndroidUseAssemblyStore=false -p:AndroidEnableAssemblyCompression=false<\/code><\/pre>\n<p>This allows you to be able to unzip the resulting <code>.apk<\/code> with your\nfavorite zip utility and inspect the .NET assemblies with a tool like\n<a href=\"https:\/\/github.com\/icsharpcode\/ILSpy\">ILSpy<\/a>. This is a good way to diagnose trimmer\/linker issues.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6311\">xamarin-android#6311<\/a> for details about this improvement.<\/p>\n<h3>Spanify RegisterNativeMembers<\/h3>\n<p>When a C# object is created from Java, a small Java wrapper is\ninvoked, such as:<\/p>\n<pre><code class=\"language-java\">public class MainActivity extends android.app.Activity\r\n{\r\n    public static final String methods;\r\n    static {\r\n        methods = \"n_onCreate:(Landroid\/os\/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\\n\";\r\n        mono.android.Runtime.register (\"foo.MainActivity, foo\", MainActivity.class, methods);\r\n    }<\/code><\/pre>\n<p>The list of <code>methods<\/code> is a <code>\\n<\/code> and <code>:<\/code>-delimited list of <a href=\"https:\/\/en.wikipedia.org\/wiki\/Java_Native_Interface\">Java Native\nInterface (JNI)<\/a> signatures that are overridden in managed C# code.\nYou get one of these for each Java method that is overridden in C#.<\/p>\n<p>When the actual Java <code>onCreate()<\/code> method is called for an Android\n<code>Activity<\/code>:<\/p>\n<pre><code class=\"language-java\">public void onCreate (android.os.Bundle p0)\r\n{\r\n    n_onCreate (p0);\r\n}\r\n\r\nprivate native void n_onCreate (android.os.Bundle p0);<\/code><\/pre>\n<p>Through various magic and hand waving, <code>n_onCreate<\/code> calls into the\nMono runtime and invokes our <code>OnCreate()<\/code> method in C#.<\/p>\n<p>The code splitting the <code>\\n<\/code> and <code>:<\/code>-delimited list of methods was\nwritten in the early days of Xamarin using <code>string.Split()<\/code>. Suffice\nto say <a href=\"https:\/\/docs.microsoft.com\/archive\/msdn-magazine\/2018\/january\/csharp-all-about-span-exploring-a-new-net-mainstay\"><code>Span&lt;T&gt;<\/code><\/a> didn&#8217;t exist back then, but we can use it now!\nThis improves the cost of any C# class that subclasses a Java class,\nso it is a wider reaching improvement than just .NET MAUI.<\/p>\n<p>You might ask, &#8220;why use strings at all?&#8221; Using Java arrays appears to\nhave a worse performance impact than delimited strings. In our testing\ncalling into JNI to get Java array elements, performs worse than\n<code>string.Split<\/code> <em>and<\/em> our new usage of <code>Span<\/code>. We have some ideas on\nhow we can re-architect this in future .NET releases.<\/p>\n<p>In addition to .NET 6, this change shipped in the latest version of\nXamarin.Android for current customers.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6708\">xamarin-android#6708<\/a> for details about this improvement.<\/p>\n<h3>System.Reflection.Emit and Constructors<\/h3>\n<p>Since the early days of Xamarin, we had a somewhat complicated method\nfor invoking C# constructors from Java.<\/p>\n<p>First, we had some reflection calls that happen once on startup:<\/p>\n<pre><code class=\"language-csharp\">static MethodInfo newobject = typeof (System.Runtime.CompilerServices.RuntimeHelpers).GetMethod (\"GetUninitializedObject\", BindingFlags.Public | BindingFlags.Static)!;\r\nstatic MethodInfo gettype = typeof (System.Type).GetMethod (\"GetTypeFromHandle\", BindingFlags.Public | BindingFlags.Static)!;\r\nstatic FieldInfo handle = typeof (Java.Lang.Object).GetField (\"handle\", BindingFlags.NonPublic | BindingFlags.Instance)!;<\/code><\/pre>\n<p>This appears to be leftover from very early versions of Mono and has\njust persisted to this day. <code>RuntimeHelpers.GetUninitializedObject()<\/code>,\nfor example can be called directly.<\/p>\n<p>Followed by some complex System.Reflection.Emit usage with a passed in\n<code>System.Reflection.ConstructorInfo cinfo<\/code> instance:<\/p>\n<pre><code class=\"language-csharp\">DynamicMethod method = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), typeof (void), new Type [] {typeof (IntPtr), typeof (object []) }, typeof (DynamicMethodNameCounter), true);\r\nILGenerator il = method.GetILGenerator ();\r\n\r\nil.DeclareLocal (typeof (object));\r\n\r\nil.Emit (OpCodes.Ldtoken, type);\r\nil.Emit (OpCodes.Call, gettype);\r\nil.Emit (OpCodes.Call, newobject);\r\nil.Emit (OpCodes.Stloc_0);\r\nil.Emit (OpCodes.Ldloc_0);\r\nil.Emit (OpCodes.Ldarg_0);\r\nil.Emit (OpCodes.Stfld, handle);\r\n\r\nil.Emit (OpCodes.Ldloc_0);\r\n\r\nvar len = cinfo.GetParameters ().Length;\r\nfor (int i = 0; i &lt; len; i++) {\r\n    il.Emit (OpCodes.Ldarg, 1);\r\n    il.Emit (OpCodes.Ldc_I4, i);\r\n    il.Emit (OpCodes.Ldelem_Ref);\r\n}\r\nil.Emit (OpCodes.Call, cinfo);\r\n\r\nil.Emit (OpCodes.Ret);\r\n\r\nreturn (Action&lt;IntPtr, object?[]?&gt;) method.CreateDelegate (typeof (Action &lt;IntPtr, object []&gt;));<\/code><\/pre>\n<p>We call the delegate returned, such that the <code>IntPtr<\/code> is the <code>Handle<\/code>\nof the <code>Java.Lang.Object<\/code> subclass and the <code>object[]<\/code> are any\nparameters for that particular C# constructor. System.Reflection.Emit\nhas a significant cost for both the first use of it on startup, as\nwell as each future call.<\/p>\n<p>After some careful review, we could make the <code>handle<\/code> field\n<code>internal<\/code>, and simplify this code to:<\/p>\n<pre><code class=\"language-csharp\">var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType);\r\nif (newobj is Java.Lang.Object o) {\r\n    o.handle = jobject;\r\n} else if (newobj is Java.Lang.Throwable throwable) {\r\n    throwable.handle = jobject;\r\n} else {\r\n    throw new InvalidOperationException ($\"Unsupported type: '{newobj}'\");\r\n}\r\ncinfo.Invoke (newobj, parms);<\/code><\/pre>\n<p>What this code does is create an object <em>without calling the\nconstructor<\/em> (ok weird?), set the <code>handle<\/code> field, and then invoke the\nconstructor. This is done so that the <code>Handle<\/code> is valid on any\n<code>Java.Lang.Object<\/code> when the C# constructor begins. The <code>Handle<\/code> would\nbe needed for any Java interop inside the constructor (like calling\nother Java methods on the class) as well as calling any base Java\nconstructors.<\/p>\n<p>The new code significantly improved any C# constructor called from\nJava, so this particular change improves more than just .NET MAUI. In\naddition to .NET 6, this change shipped in the latest version of\nXamarin.Android for current customers.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6766\">xamarin-android#6766<\/a> for details about this improvement.<\/p>\n<h3>System.Reflection.Emit and Methods<\/h3>\n<p>When you override a Java method in C#, such as:<\/p>\n<pre><code class=\"language-csharp\">public class MainActivity : Activity\r\n{\r\n    protected override void OnCreate(Bundle savedInstanceState)\r\n    {\r\n         base.OnCreate(savedInstanceState);\r\n         \/\/...\r\n    }\r\n}<\/code><\/pre>\n<p>In the transition from Java to C#, we have to wrap the C# method to\nhandle exceptions such as:<\/p>\n<pre><code class=\"language-csharp\">try\r\n{\r\n    \/\/ Call the actual C# method here\r\n}\r\ncatch (Exception e) when (_unhandled_exception (e))\r\n{\r\n    AndroidEnvironment.UnhandledException (e);\r\n    if (Debugger.IsAttached || !JNIEnv.PropagateExceptions)\r\n        throw;\r\n}<\/code><\/pre>\n<p>If a managed exception is unhandled in <code>OnCreate()<\/code>, for example, then\nyou actually end up with a native crash (and no managed C# stack\ntrace). We need to make sure the debugger can break on the exception\nif it is attached, and log the C# stack trace otherwise.<\/p>\n<p>Since the beginning of Xamarin, the above code was generated via\nSystem.Reflection.Emit:<\/p>\n<pre><code class=\"language-csharp\">var dynamic = new DynamicMethod (DynamicMethodNameCounter.GetUniqueName (), ret_type, param_types, typeof (DynamicMethodNameCounter), true);\r\nvar ig = dynamic.GetILGenerator ();\r\n\r\nLocalBuilder? retval = null;\r\nif (ret_type != typeof (void))\r\n    retval = ig.DeclareLocal (ret_type);\r\n\r\nig.Emit (OpCodes.Call, wait_for_bridge_processing_method!);\r\n\r\nvar label = ig.BeginExceptionBlock ();\r\n\r\nfor (int i = 0; i &lt; param_types.Length; i++)\r\n    ig.Emit (OpCodes.Ldarg, i);\r\nig.Emit (OpCodes.Call, dlg.Method);\r\n\r\nif (retval != null)\r\n    ig.Emit (OpCodes.Stloc, retval);\r\n\r\nig.Emit (OpCodes.Leave, label);\r\n\r\nbool  filter = Debugger.IsAttached || !JNIEnv.PropagateExceptions;\r\nif (filter &amp;&amp; JNIEnv.mono_unhandled_exception_method != null) {\r\n    ig.BeginExceptFilterBlock ();\r\n\r\n    ig.Emit (OpCodes.Call, JNIEnv.mono_unhandled_exception_method);\r\n    ig.Emit (OpCodes.Ldc_I4_1);\r\n    ig.BeginCatchBlock (null!);\r\n} else {\r\n    ig.BeginCatchBlock (typeof (Exception));\r\n}\r\n\r\nig.Emit (OpCodes.Dup);\r\nig.Emit (OpCodes.Call, exception_handler_method!);\r\n\r\nif (filter)\r\n    ig.Emit (OpCodes.Throw);\r\n\r\nig.EndExceptionBlock ();\r\n\r\nif (retval != null)\r\n    ig.Emit (OpCodes.Ldloc, retval);\r\n\r\nig.Emit (OpCodes.Ret);<\/code><\/pre>\n<p>This code is called twice for a <code>dotnet new android<\/code> app, but ~58\ntimes for a <code>dotnet new maui<\/code> app!<\/p>\n<p>Instead using System.Reflection.Emit, we realized we could actually\nwrite a strongly-typed &#8220;fast path&#8221; for each common delegate type.\nThere is a generated <code>delegate<\/code> that matches each signature:<\/p>\n<pre><code class=\"language-csharp\">void OnCreate(Bundle savedInstanceState);\r\n\r\n\/\/ Maps to *JNIEnv, JavaClass, Bundle\r\n\/\/ Internal to each assembly\r\ninternal delegate void _JniMarshal_PPL_V(IntPtr, IntPtr, IntPtr);<\/code><\/pre>\n<p>So we could list every signature used by <code>dotnet maui<\/code> apps, such as:<\/p>\n<pre><code class=\"language-csharp\">class JNINativeWrapper\r\n{\r\n    static Delegate? CreateBuiltInDelegate (Delegate dlg, Type delegateType)\r\n    {\r\n        switch (delegateType.Name)\r\n        {\r\n            \/\/ Unsafe.As&lt;T&gt;() is used, because _JniMarshal_PPL_V is generated internal in each assembly\r\n            case nameof (_JniMarshal_PPL_V):\r\n                return new _JniMarshal_PPL_V (Unsafe.As&lt;_JniMarshal_PPL_V&gt; (dlg).Wrap_JniMarshal_PPL_V);\r\n            \/\/ etc.\r\n        }\r\n        return null;\r\n    }\r\n\r\n    \/\/ Static extension method is generated to avoid capturing variables in anonymous methods\r\n    internal static void Wrap_JniMarshal_PPL_V (this _JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0)\r\n    {\r\n        \/\/ ...\r\n    }\r\n}<\/code><\/pre>\n<p>The drawback to this approach is that we have to list more cases when\na new signature is used. We don&#8217;t want to exhaustively list every\ncombination, as this will cause IL-size to grow. We are investigating\nhow to improve this in future .NET releases.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6657\">xamarin-android#6657<\/a> and <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6707\">xamarin-android#6707<\/a> for\ndetails about this improvement.<\/p>\n<h3>Newer Java.Interop APIs<\/h3>\n<p>The original Xamarin APIs in <code>Java.Interop.dll<\/code>, are APIs such as:<\/p>\n<ul>\n<li><code>JNIEnv.CallStaticObjectMethod<\/code><\/li>\n<\/ul>\n<p>Where the &#8220;new way&#8221; to call into Java makes fewer memory allocations\nper call:<\/p>\n<ul>\n<li><code>JniEnvironment.StaticMethods.CallStaticObjectMethod<\/code><\/li>\n<\/ul>\n<p>When C# bindings are generated for Java methods at build time, the\nnewer\/faster methods are used by default &#8212; and have been for some\ntime in Xamarin.Android. Previously, Java binding projects could set\n<a href=\"https:\/\/docs.microsoft.com\/xamarin\/android\/deploy-test\/building-apps\/build-properties#androidcodegentarget\"><code>$(AndroidCodegenTarget)<\/code><\/a> to <code>XAJavaInterop1<\/code>, which caches and\nreuses <code>jmethodID<\/code> instances on each call. See the <a href=\"https:\/\/github.com\/xamarin\/Java.Interop\/commit\/d9b43b52a2904e00b74b96c82a7c62c6a0c214ca\">java.interop<\/a>\nrepo for the history on this feature.<\/p>\n<p>The remaining places that this is an issue, are anywhere we have\n&#8220;manual&#8221; bindings. These tend to also be frequently used methods, so\nit was worthwhile to fix these!<\/p>\n<p>Some examples of improving this situation:<\/p>\n<ul>\n<li><code>JNIEnv.FindClass()<\/code> in <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6805\">xamarin-android#6805<\/a><\/li>\n<li><code>JavaList<\/code> and <code>JavaList&lt;T&gt;<\/code> in <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6812\">xamarin-android#6812<\/a><\/li>\n<\/ul>\n<h3>Multi-dimensional Java Arrays<\/h3>\n<p>When passing C# arrays back and forth to Java, an intermediate step\nhas to copy the array so the appropriate runtime can access it. This\nis really a developer experience situation, as C# developers expect to\nwrite something like:<\/p>\n<pre><code class=\"language-csharp\">var array = new int[] { 1, 2, 3, 4};\r\nMyJavaMethod (array);<\/code><\/pre>\n<p>Where inside <code>MyJavaMethod<\/code> would do:<\/p>\n<pre><code class=\"language-csharp\">IntPtr native_items = JNIEnv.NewArray (items);\r\ntry\r\n{\r\n    \/\/ p\/invoke here, actually calls into Java\r\n}\r\nfinally\r\n{\r\n    if (items != null)\r\n    {\r\n        JNIEnv.CopyArray (native_items, items); \/\/ If the calling method mutates the array\r\n        JNIEnv.DeleteLocalRef (native_items); \/\/ Delete our Java local reference\r\n    }\r\n}<\/code><\/pre>\n<p><code>JNIEnv.NewArray()<\/code> accesses a &#8220;type map&#8221; to know which Java class needs\nto be used for the elements of the array.<\/p>\n<p>A particular Android API used by <code>dotnet new maui<\/code> projects was problematic:<\/p>\n<pre><code class=\"language-csharp\">public ColorStateList (int[][]? states, int[]? colors)<\/code><\/pre>\n<p>A multi-dimensional <code>int[][]<\/code> array was found to access the &#8220;type map&#8221;\nfor each element. We could see this when enabling additional logging,\nmany instances of:<\/p>\n<pre><code class=\"language-dotnetcli\">monodroid: typemap: failed to map managed type to Java type: System.Int32, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e (Module ID: 8e4cd939-3275-41c4-968d-d5a4376b35f5; Type token: 33554653)\r\nmonodroid-assembly: typemap: called from\r\nmonodroid-assembly: at Android.Runtime.JNIEnv.TypemapManagedToJava(Type )\r\nmonodroid-assembly: at Android.Runtime.JNIEnv.GetJniName(Type )\r\nmonodroid-assembly: at Android.Runtime.JNIEnv.FindClass(Type )\r\nmonodroid-assembly: at Android.Runtime.JNIEnv.NewArray(Array , Type )\r\nmonodroid-assembly: at Android.Runtime.JNIEnv.NewArray[Int32[]](Int32[][] )\r\nmonodroid-assembly: at Android.Content.Res.ColorStateList..ctor(Int32[][] , Int32[] )\r\nmonodroid-assembly: at Microsoft.Maui.Platform.ColorStateListExtensions.CreateButton(Int32 enabled, Int32 disabled, Int32 off, Int32 pressed)<\/code><\/pre>\n<p>For this case, we should be able to call <code>JNIEnv.FindClass()<\/code> once and\nreuse this value for each item in the array!<\/p>\n<p>We are investigating how to improve this further in future .NET\nreleases. One such example would be <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5654\">dotnet\/maui#5654<\/a>, where we\nare simply looking into creating the arrays completely in Java instead.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6870\">xamarin-android#6870<\/a> for details about this improvement.<\/p>\n<h3>Use Glide for Android Images<\/h3>\n<p><a href=\"https:\/\/github.com\/bumptech\/glide\"><code>Glide<\/code><\/a> is the recommended\nimage-loading library for modern Android applications. Google\ndocumentation even recommends using it, because the built-in Android\n<code>Bitmap<\/code> class can be painfully difficult to use correctly.\n<a href=\"https:\/\/github.com\/jonathanpeppers\/glidex\"><code>glidex.forms<\/code><\/a> was a\nprototype for using Glide in Xamarin.Forms, but we promoted Glide to\nbe &#8220;the way&#8221; to load images in .NET MAUI going forward.<\/p>\n<p>To reduce the overhead of JNI interop, .NET MAUI&#8217;s Glide\nimplementation is mostly written in Java, such as:<\/p>\n<pre><code class=\"language-java\">import com.bumptech.glide.Glide;\r\n\/\/...\r\npublic static void loadImageFromUri(ImageView imageView, String uri, Boolean cachingEnabled, ImageLoaderCallback callback) {\r\n    \/\/...\r\n    RequestBuilder&lt;Drawable&gt; builder = Glide\r\n        .with(imageView)\r\n        .load(androidUri);\r\n    loadInto(builder, imageView, cachingEnabled, callback);\r\n}<\/code><\/pre>\n<p>Where <code>ImageLoaderCallback<\/code> is subclassed in C# to handle completion\nin managed code. The result is that the performance of images from the\nweb should be significantly improved from what you got previously in\nXamarin.Forms.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/759\">dotnet\/maui#759<\/a> and <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5198\">dotnet\/maui#5198<\/a> for details\nabout this improvement.<\/p>\n<h3>Reduce Java Interop Calls<\/h3>\n<p>Let&#8217;s say you have the following Java APIs:<\/p>\n<pre><code class=\"language-java\">public void setFoo(int foo);\r\npublic void setBar(int bar);<\/code><\/pre>\n<p>The interop for these methods look something like:<\/p>\n<pre><code class=\"language-csharp\">public unsafe static void SetFoo(int foo)\r\n{\r\n    JniArgumentValue* __args = stackalloc JniArgumentValue[1];\r\n    __args[0] = new JniArgumentValue(foo);\r\n    return _members.StaticMethods.InvokeInt32Method(\"setFoo.(I)V\", __args);\r\n}\r\n\r\npublic unsafe static void SetBar(int bar)\r\n{\r\n    JniArgumentValue* __args = stackalloc JniArgumentValue[1];\r\n    __args[0] = new JniArgumentValue(bar);\r\n    return _members.StaticMethods.InvokeInt32Method(\"setBar.(I)V\", __args);\r\n}<\/code><\/pre>\n<p>So calling both of these methods would <code>stackalloc<\/code> twice and p\/invoke\ntwice. It would be more performant to create a small Java wrapper,\nsuch as:<\/p>\n<pre><code class=\"language-java\">public void setFooAndBar(int foo, int bar)\r\n{\r\n    setFoo(foo);\r\n    setBar(bar);\r\n}<\/code><\/pre>\n<p>Which translates to:<\/p>\n<pre><code class=\"language-csharp\">public unsafe static void SetFooAndBar(int foo, int bar)\r\n{\r\n    JniArgumentValue* __args = stackalloc JniArgumentValue[2];\r\n    __args[0] = new JniArgumentValue(foo);\r\n    __args[1] = new JniArgumentValue(bar);\r\n    return _members.StaticMethods.InvokeInt32Method(\"setFooAndBar.(II)V\", __args);\r\n}<\/code><\/pre>\n<p>.NET MAUI views are essentially C# objects with lots of properties\nthat need to be set in Java in this exact same way. If we apply this\nconcept to every Android <code>View<\/code> in .NET MAUI, we can create an ~18\nargument method to be used on <code>View<\/code> creation. Subsequent property\nchanges can can call the standard Android APIs directly.<\/p>\n<p>This had a dramatic increase in performance, for even very simple .NET\nMAUI controls:<\/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;\">Gen 0<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Border (before)<\/td>\n<td style=\"text-align: right;\">323.2 \u00b5s<\/td>\n<td style=\"text-align: right;\">0.82 \u00b5s<\/td>\n<td style=\"text-align: right;\">0.68 \u00b5s<\/td>\n<td style=\"text-align: right;\">0.9766<\/td>\n<td style=\"text-align: right;\">5 KB<\/td>\n<\/tr>\n<tr>\n<td>Border (after)<\/td>\n<td style=\"text-align: right;\">242.3 \u00b5s<\/td>\n<td style=\"text-align: right;\">1.34 \u00b5s<\/td>\n<td style=\"text-align: right;\">1.25 \u00b5s<\/td>\n<td style=\"text-align: right;\">0.9766<\/td>\n<td style=\"text-align: right;\">5 KB<\/td>\n<\/tr>\n<tr>\n<td>ContentView (before)<\/td>\n<td style=\"text-align: right;\">354.6 \u00b5s<\/td>\n<td style=\"text-align: right;\">2.61 \u00b5s<\/td>\n<td style=\"text-align: right;\">2.31 \u00b5s<\/td>\n<td style=\"text-align: right;\">1.4648<\/td>\n<td style=\"text-align: right;\">6 KB<\/td>\n<\/tr>\n<tr>\n<td>ContentView (after)<\/td>\n<td style=\"text-align: right;\">258.3 \u00b5s<\/td>\n<td style=\"text-align: right;\">0.49 \u00b5s<\/td>\n<td style=\"text-align: right;\">0.43 \u00b5s<\/td>\n<td style=\"text-align: right;\">1.4648<\/td>\n<td style=\"text-align: right;\">6 KB<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/3372\">dotnet\/maui#3372<\/a> for details about this improvement.<\/p>\n<h3>Port Android XML to Java<\/h3>\n<p>Reviewing <code>dotnet trace<\/code> output on Android, we can see a reasonable\namount of time spent in:<\/p>\n<pre><code class=\"language-dotnetcli\">20.32.ms mono.android!Android.Views.LayoutInflater.Inflate<\/code><\/pre>\n<p>Reviewing the stack trace, the time is actually spent in Android\/Java\nto inflate the layout, and no work is happening on the .NET side.<\/p>\n<p>If you look at a compiled Android <code>.apk<\/code> and\n<code>res\/layouts\/bottomtablayout.axml<\/code> in Android Studio, the XML is just\nplain XML. Only a few identifiers are transformed to integers. This\nmeans Android has to parse this XML and create Java objects through\nJava&#8217;s reflection APIs &#8212; it seemed like we could get faster\nperformance by <em>not<\/em> using XML?<\/p>\n<p>Testing a standard BenchmarkDotNet comparison, we found that the use\nof Android layouts performed worse even than C# when interop is\ninvolved:<\/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>Java<\/td>\n<td style=\"text-align: right;\">338.4 \u00b5s<\/td>\n<td style=\"text-align: right;\">4.21 \u00b5s<\/td>\n<td style=\"text-align: right;\">3.52 \u00b5s<\/td>\n<td style=\"text-align: right;\">744 B<\/td>\n<\/tr>\n<tr>\n<td>CSharp<\/td>\n<td style=\"text-align: right;\">410.2 \u00b5s<\/td>\n<td style=\"text-align: right;\">7.92 \u00b5s<\/td>\n<td style=\"text-align: right;\">6.61 \u00b5s<\/td>\n<td style=\"text-align: right;\">1,336 B<\/td>\n<\/tr>\n<tr>\n<td>XML<\/td>\n<td style=\"text-align: right;\">490.0 \u00b5s<\/td>\n<td style=\"text-align: right;\">7.77 \u00b5s<\/td>\n<td style=\"text-align: right;\">7.27 \u00b5s<\/td>\n<td style=\"text-align: right;\">2,321 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Next, we configured BenchmarkDotNet to do a single run, to better\nsimulate what would happen on startup:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Java<\/td>\n<td style=\"text-align: right;\">4.619 ms<\/td>\n<\/tr>\n<tr>\n<td>CSharp<\/td>\n<td style=\"text-align: right;\">37.337 ms<\/td>\n<\/tr>\n<tr>\n<td>XML<\/td>\n<td style=\"text-align: right;\">39.364 ms<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>We looked at one of the simpler layouts in .NET MAUI, bottom tab\nnavigation:<\/p>\n<pre><code class=\"language-xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;LinearLayout\r\n  xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"\r\n  android:orientation=\"vertical\"\r\n  android:layout_width=\"match_parent\"\r\n  android:layout_height=\"match_parent\"&gt;\r\n  &lt;FrameLayout\r\n    android:id=\"@+id\/bottomtab.navarea\"\r\n    android:layout_width=\"match_parent\"\r\n    android:layout_height=\"0dp\"\r\n    android:layout_gravity=\"fill\"\r\n    android:layout_weight=\"1\" \/&gt;\r\n  &lt;com.google.android.material.bottomnavigation.BottomNavigationView\r\n    android:id=\"@+id\/bottomtab.tabbar\"\r\n    android:theme=\"@style\/Widget.Design.BottomNavigationView\"\r\n    android:layout_width=\"match_parent\"\r\n    android:layout_height=\"wrap_content\" \/&gt;\r\n&lt;\/LinearLayout&gt;<\/code><\/pre>\n<p>We could port this to four Java methods, such as:<\/p>\n<pre><code class=\"language-java\">@NonNull\r\npublic static List&lt;View&gt; createBottomTabLayout(Context context, int navigationStyle);\r\n@NonNull\r\npublic static LinearLayout createLinearLayout(Context context);\r\n@NonNull\r\npublic static FrameLayout createFrameLayout(Context context, LinearLayout layout);\r\n@NonNull\r\npublic static BottomNavigationView createNavigationBar(Context context, int navigationStyle, FrameLayout bottom)<\/code><\/pre>\n<p>This allows us to only cross from C# into Java four times when\ncreating bottom tab navigation on Android. It also allows the Android\nOS to skip loading and parsing an <code>.xml<\/code> to &#8220;inflate&#8221; Java objects. We\ncarried this idea throughout dotnet\/maui removing all\n<code>LayoutInflater.Inflate()<\/code> calls on startup.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5424\">dotnet\/maui#5424<\/a>, <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5493\">dotnet\/maui#5493<\/a>, and\n<a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5528\">dotnet\/maui#5528<\/a> for details about these improvements.<\/p>\n<h3>Remove Microsoft.Extensions.Hosting<\/h3>\n<p><code>Microsoft.Extensions.Hosting<\/code> provides a <a href=\"https:\/\/docs.microsoft.com\/dotnet\/core\/extensions\/generic-host\">.NET Generic Host<\/a> for\nmanaging dependency injection, logging, configuration, and application\nlifecycle within a .NET application. This has an impact to startup\ntimes that did not seem appropriate for mobile applications.<\/p>\n<p>It made sense to remove <code>Microsoft.Extensions.Hosting<\/code> usage from .NET\nMAUI. Instead of trying to interoperate with the &#8220;Generic Host&#8221; to\nbuild the DI container, .NET MAUI has its own simple implementation\nthat is optimized for mobile startup. Additionally, .NET MAUI no\nlonger adds logging providers by default.<\/p>\n<p>With this change, we saw reduction in startup time of a <code>dotnet new maui<\/code> Android app between 5-10%. And it reduces the size of the same\napp on iOS from <code>19.2 MB<\/code> =&gt; <code>18.0 MB<\/code>.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4505\">dotnet\/maui#4505<\/a> and <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4545\">dotnet\/maui#4545<\/a> for details\nabout this improvement.<\/p>\n<h3>Less Shell Initialization on Startup<\/h3>\n<p><a href=\"https:\/\/docs.microsoft.com\/xamarin\/xamarin-forms\/app-fundamentals\/shell\/\">Xamarin.Forms Shell<\/a> is a pattern for navigation in\ncross-platform applications. This pattern was brought forward to .NET\nMAUI, where it is recommended as the default way for building\napplications.<\/p>\n<p>As we discovered the cost of using Shell in startup (for both\nXamarin.Forms and .NET MAUI), we found a couple places to optimize:<\/p>\n<ul>\n<li>Don&#8217;t parse routes on startup &#8212; wait until a navigation occurs that\nwould need them.<\/li>\n<li>If no query strings have been supplied for navigation then just skip\nthe code that processes query strings. This removes a code path that\nuses System.Reflection heavily.<\/li>\n<li>If the page doesn&#8217;t have a visible <code>BottomNavigationView<\/code>, then\ndon&#8217;t setup the menu items or any of the appearance elements.<\/li>\n<\/ul>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5262\">dotnet\/maui#5262<\/a> for details about this improvement.<\/p>\n<h3>Fonts Should Not Use Temporary Files<\/h3>\n<p>A significant amount of time was spend in .NET MAUI apps loading fonts:<\/p>\n<pre><code class=\"language-dotnetcli\">32.19ms Microsoft.Maui!Microsoft.Maui.FontManager.CreateTypeface(System.ValueTuple`3&lt;string, Microsoft.Maui.FontWeight, bool&gt;)<\/code><\/pre>\n<p>Reviewing the code, it was doing more work than needed:<\/p>\n<ol>\n<li>Saving <code>AndroidAsset<\/code> files to a temp folder.<\/li>\n<li>Use the Android API, <code>Typeface.CreateFromFile()<\/code> to load the file.<\/li>\n<\/ol>\n<p>We can actually use the <code>Typeface.CreateFromAsset()<\/code> Android API\ndirectly and not use a temporary file at all.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4933\">dotnet\/maui#4933<\/a> for details about this improvement.<\/p>\n<h2>Compute OnPlatform at Compile-time<\/h2>\n<p>Usage of the <code>{OnPlatform}<\/code> markup extension:<\/p>\n<pre><code class=\"language-xml\">&lt;Label Text=\"Platform: \" \/&gt;\r\n&lt;Label Text=\"{OnPlatform Default=Unknown, Android=Android, iOS=iOS\" \/&gt;<\/code><\/pre>\n<p>&#8230;can actually be computed at compile-time, where the\n<code>net6.0-android<\/code> and <code>net6.0-ios<\/code> get the appropriate value. In a\nfuture .NET release, we will look into the same optimization for the\n<code>&lt;OnPlatform\/&gt;<\/code> XML element.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4829\">dotnet\/maui#4829<\/a> and <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5611\">dotnet\/maui#5611<\/a> for details\nabout this improvement.<\/p>\n<h2>Use Compiled Converters in XAML<\/h2>\n<p>The following types are now converted at XAML compile time, instead of\nat runtime:<\/p>\n<ul>\n<li><code>Color<\/code>: <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4687\">dotnet\/maui#4687<\/a><\/li>\n<li><code>CornerRadius<\/code>: <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5192\">dotnet\/maui#5192<\/a><\/li>\n<li><code>FontSize<\/code>: <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5338\">dotnet\/maui#5338<\/a><\/li>\n<li><code>GridLength<\/code>, <code>RowDefinition<\/code>, <code>ColumnDefinition<\/code>: <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5489\">dotnet\/maui#5489<\/a><\/li>\n<\/ul>\n<p>This results in better\/faster generated IL from <code>.xaml<\/code> files.<\/p>\n<h3>Optimize Color Parsing<\/h3>\n<p>The original code for <code>Microsoft.Maui.Graphics.Color.Parse()<\/code> could be\nrewritten to make better use of <code>Span&lt;T&gt;<\/code> and avoid string allocations.<\/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;\">Gen 0<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Parse (before)<\/td>\n<td style=\"text-align: right;\">99.13 ns<\/td>\n<td style=\"text-align: right;\">0.281 ns<\/td>\n<td style=\"text-align: right;\">0.235 ns<\/td>\n<td style=\"text-align: right;\">0.0267<\/td>\n<td style=\"text-align: right;\">168 B<\/td>\n<\/tr>\n<tr>\n<td>Parse (after)<\/td>\n<td style=\"text-align: right;\">52.54 ns<\/td>\n<td style=\"text-align: right;\">0.292 ns<\/td>\n<td style=\"text-align: right;\">0.259 ns<\/td>\n<td style=\"text-align: right;\">0.0051<\/td>\n<td style=\"text-align: right;\">32 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Being able to use a <code>switch<\/code>-statement on <code>ReadonlySpan&lt;char&gt;<\/code>\n<a href=\"https:\/\/github.com\/dotnet\/csharplang\/issues\/1881\">dotnet\/csharplang#1881<\/a> will improve this case even further in a\nfuture .NET release.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/Microsoft.Maui.Graphics\/pull\/343\">dotnet\/Microsoft.Maui.Graphics#343<\/a> and\n<a href=\"https:\/\/github.com\/dotnet\/Microsoft.Maui.Graphics\/pull\/345\">dotnet\/Microsoft.Maui.Graphics#345<\/a> for details about this\nimprovement.<\/p>\n<h3>Don&#8217;t Use Culture-aware String Comparisons<\/h3>\n<p>Reviewing <code>dotnet trace<\/code> output of a <code>dotnet new maui<\/code> project showed\nthe real cost of the <em>first<\/em> culture-aware string comparison on\nAndroid:<\/p>\n<pre><code class=\"language-dotnetcli\">6.32ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellNavigationManager.GetNavigationState\r\n3.82ms Microsoft.Maui.Controls!Microsoft.Maui.Controls.ShellUriHandler.FormatUri\r\n3.82ms System.Private.CoreLib!System.String.StartsWith\r\n2.57ms System.Private.CoreLib!System.Globalization.CultureInfo.get_CurrentCulture<\/code><\/pre>\n<p>Really, we did not even <em>want<\/em> to use a culture-aware comparison in\nthis case &#8212; it was simply code brought over from Xamarin.Forms.<\/p>\n<p>So for example if you have:<\/p>\n<pre><code class=\"language-csharp\">if (text.StartsWith(\"f\"))\r\n{\r\n    \/\/ do something\r\n}<\/code><\/pre>\n<p>In this case you can simply do this instead:<\/p>\n<pre><code class=\"language-csharp\">if (text.StartsWith(\"f\", StringComparision.Ordinal))\r\n{\r\n    \/\/ do something\r\n}<\/code><\/pre>\n<p>If done across an entire application,\n<code>System.Globalization.CultureInfo.CurrentCulture<\/code> can avoid being\ncalled, as well as improving the overall speed of this <code>if<\/code>-statement\nby a small amount.<\/p>\n<p>To fix this situation across the entire dotnet\/maui repo, we\nintroduced code analysis rules to catch these:<\/p>\n<pre><code class=\"language-yaml\">dotnet_diagnostic.CA1307.severity = error\r\ndotnet_diagnostic.CA1309.severity = error<\/code><\/pre>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4988\">dotnet\/maui#4988<\/a> for details about this improvement.<\/p>\n<h3>Create Loggers Lazily<\/h3>\n<p>The <code>ConfigureFonts()<\/code> API was spending a bit of time on startup doing\nwork that could be deferred until later. We could also improve the\ngeneral usage of the logging infrastructure in Microsoft.Extensions.<\/p>\n<p>Some improvements we made were:<\/p>\n<ul>\n<li>Defer creating &#8220;logger&#8221; classes until they are needed.<\/li>\n<li>The built-in logging infrastructure is disabled by default, and has\nto be enabled explicitly.<\/li>\n<li>Delay calling <code>Path.GetTempPath()<\/code> in Android&#8217;s <code>EmbeddedFontLoader<\/code>\nuntil it is needed.<\/li>\n<li>Don&#8217;t use <code>ILoggerFactory<\/code> to create a generic logger. Instead get\nthe <code>ILogger<\/code> service directly, so it is cached.<\/li>\n<\/ul>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5103\">dotnet\/maui#5103<\/a> for details about this improvement.<\/p>\n<h3>Use Factory Methods for Dependency Injection<\/h3>\n<p>When using <code>Microsoft.Extensions.DependencyInjection<\/code>, registering\nservices such as:<\/p>\n<pre><code class=\"language-csharp\">IServiceCollection services \/* ... *\/;\r\nservices.TryAddSingleton&lt;IFooService, FooService&gt;();<\/code><\/pre>\n<p>Microsoft.Extensions has to do a bit of System.Reflection to create\nthe first instance of <code>FooService<\/code>. This was noticeable in <code>dotnet trace<\/code> output on Android.<\/p>\n<p>Instead if you do:<\/p>\n<pre><code class=\"language-csharp\">\/\/ If FooService has no dependencies\r\nservices.TryAddSingleton&lt;IFooService&gt;(sp =&gt; new FooService());\r\n\/\/ Or if you need to retrieve some dependencies\r\nservices.TryAddSingleton&lt;IFooService&gt;(sp =&gt; new FooService(sp.GetService&lt;IBar&gt;()));<\/code><\/pre>\n<p>In this case, Microsoft.Extensions can simply call your\nlamdba\/anonymous method and no System.Reflection is involved.<\/p>\n<p>We made this improvement across all of dotnet\/maui, as well as making\nuse of <a href=\"https:\/\/github.com\/dotnet\/roslyn-analyzers\/blob\/main\/src\/Microsoft.CodeAnalysis.BannedApiAnalyzers\/BannedApiAnalyzers.Help.md\"><code>BannedApiAnalyzers<\/code><\/a>, so that no one would accidentally\nuse the slower overload of <code>TryAddSingleton()<\/code> going forward.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5290\">dotnet\/maui#5290<\/a> for details about this improvement.<\/p>\n<h3>Default VerifyDependencyInjectionOpenGenericServiceTrimmability<\/h3>\n<p>The <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast<\/a> sample was spending 4-7ms worth of time in:<\/p>\n<pre><code class=\"language-csharp\">Microsoft.Extensions.DependencyInjection.ServiceLookup.CallsiteFactory.ValidateTrimmingAnnotations()<\/code><\/pre>\n<p>The <code>$(VerifyDependencyInjectionOpenGenericServiceTrimmability)<\/code>\nMSBuild property triggers this method to run. This feature switch\nensures that <code>DynamicallyAccessedMembers<\/code> are applied correctly to\nopen generic types used in Dependency Injection.<\/p>\n<p>In the base .NET SDK, this switch is enabled when\n<code>PublishTrimmed=true<\/code>. However, Android apps don&#8217;t set\n<code>PublishTrimmed=true<\/code> in <code>Debug<\/code> builds, so developers are missing out\non this validation.<\/p>\n<p>Conversely, in published apps, we don&#8217;t want to pay the cost of doing\nthis validation. So this feature switch should be <em>off<\/em> in <code>Release<\/code>\nbuilds.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6727\">xamarin-android#6727<\/a> and <a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/14130\">xamarin-macios#14130<\/a> for\ndetails about this improvement.<\/p>\n<h3>Load ConfigurationManager Lazily<\/h3>\n<p><code>System.Configuration.ConfigurationManager<\/code> isn&#8217;t used by many mobile\napplications, and it turns out to be quite expensive to create one!\n(~7.59ms on Android, for example)<\/p>\n<p>One <code>ConfigurationManager<\/code> was being created by default at startup in\n.NET MAUI, we can defer its creation using <code>Lazy&lt;T&gt;<\/code>, so it will not\nbe created unless requested.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5348\">dotnet\/maui#5348<\/a> for details about this improvement.<\/p>\n<h3>Improve the Built-in AOT Profile<\/h3>\n<p>The Mono runtime has a report (see <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/blob\/main\/Documentation\/guides\/profiling.md#profiling-the-jit-compiler\">our documentation<\/a>) for the\nJIT times of each method, such as:<\/p>\n<pre><code class=\"language-dotnetcli\">Total(ms) | Self(ms) | Method\r\n     3.51 |     3.51 | Microsoft.Maui.Layouts.GridLayoutManager\/GridStructure:.ctor (Microsoft.Maui.IGridLayout,double,double)\r\n     1.88 |     1.88 | Microsoft.Maui.Controls.Xaml.AppThemeBindingExtension\/&lt;&gt;c__DisplayClass20_0:&lt;Microsoft.Maui.Controls.Xaml.IMarkupExtension&lt;Microsoft.Maui.Controls.BindingBase&gt;.ProvideValue&gt;g__minforetriever|0 ()\r\n     1.66 |     1.66 | Microsoft.Maui.Controls.Xaml.OnIdiomExtension\/&lt;&gt;c__DisplayClass32_0:&lt;ProvideValue&gt;g__minforetriever|0 ()\r\n     1.54 |     1.54 | Microsoft.Maui.Converters.ThicknessTypeConverter:ConvertFrom (System.ComponentModel.ITypeDescriptorContext,System.Globalization.CultureInfo,object)<\/code><\/pre>\n<p>This was a selection of the top JIT-times in the <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast<\/a>\nsample in a <code>Release<\/code> build using Profiled AOT. These seemed like\ncommonly used APIs that developers would want to use in .NET MAUI\napplications.<\/p>\n<p>To make sure these methods are in the AOT profile, we used these APIs\nin the &#8220;recorded app&#8221; we use in dotnet\/maui:<\/p>\n<pre><code class=\"language-csharp\"> _ = new Microsoft.Maui.Layouts.GridLayoutManager(new Grid()).Measure(100, 100);<\/code><\/pre>\n<pre><code class=\"language-xml\">&lt;SolidColorBrush x:Key=\"ProfiledAot_AppThemeBinding_Color\" Color=\"{AppThemeBinding Default=Black}\"\/&gt;\r\n&lt;CollectionView x:Key=\"ProfiledAot_CollectionView_OnIdiom_Thickness\" Margin=\"{OnIdiom Default=1,1,1,1}\" \/&gt;<\/code><\/pre>\n<p>Calling these methods within this test application ensured they would\nbe in the built-in .NET MAUI AOT profile.<\/p>\n<p>After this change, we looked at an updated JIT report:<\/p>\n<pre><code class=\"language-dotnetcli\">Total (ms) |  Self (ms) | Method\r\n      2.61 |       2.61 | string:SplitInternal (string,string[],int,System.StringSplitOptions)\r\n      1.57 |       1.57 | System.Number:NumberToString (System.Text.ValueStringBuilder&amp;,System.Number\/NumberBuffer&amp;,char,int,System.Globalization.NumberFormatInfo)\r\n      1.52 |       1.52 | System.Number:TryParseInt32IntegerStyle (System.ReadOnlySpan`1&lt;char&gt;,System.Globalization.NumberStyles,System.Globalization.NumberFormatInfo,int&amp;)<\/code><\/pre>\n<p>Which led to further additions to the profile:<\/p>\n<pre><code class=\"language-csharp\">var split = \"foo;bar\".Split(';');\r\nvar x = int.Parse(\"999\");\r\nx.ToString();<\/code><\/pre>\n<p>We did similar changes for <code>Color.Parse()<\/code>,\n<code>Connectivity.NetworkAccess<\/code>, <code>DeviceInfo.Idiom<\/code>, and\n<code>AppInfo.RequestedTheme<\/code> that should be commonly used in .NET MAUI\napplications.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5559\">dotnet\/maui#5559<\/a>, <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/5682\">dotnet\/maui#5682<\/a>, and\n<a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/6834\">dotnet\/maui#6834<\/a> for details about these improvements.<\/p>\n<p>If you would like to record a custom AOT profile in .NET 6, you can try\nour experimental <a href=\"https:\/\/github.com\/jonathanpeppers\/Mono.Profiler.Android\">Mono.Profiler.Android<\/a> package. We are working on\nfull support for recording custom profiles in a future .NET release.<\/p>\n<h3>Enable Lazy-loading of AOT Images<\/h3>\n<p>Previously, the Mono runtime would load all AOT images at startup to\nverify the MVID of the managed .NET assembly (such as <code>Foo.dll<\/code>)\nmatches the AOT image (<code>libFoo.dll.so<\/code>). In most .NET applications,\nsome AOT images may not need to be loaded until later.<\/p>\n<p>A new <code>--aot-lazy-assembly-load<\/code> or <code>mono_opt_aot_lazy_assembly_load<\/code>\nsetting was introduced in Mono that the Android workload could opt\ninto. We found this improved the startup of a <code>dotnet new maui<\/code>\nproject on a Pixel 6 Pro by around 25ms.<\/p>\n<p>This is enabled by default, but if desired you can disable this\nsetting in your <code>.csproj<\/code> via:<\/p>\n<pre><code class=\"language-xml\">&lt;AndroidAotEnableLazyLoad&gt;false&lt;\/AndroidAotEnableLazyLoad&gt;<\/code><\/pre>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/67024\">dotnet\/runtime#67024<\/a> and <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6940\">xamarin-android#6940<\/a> for\ndetails about these improvements.<\/p>\n<h3>Remove Unused Encoding Object in System.Uri<\/h3>\n<p><code>dotnet trace<\/code> output of a MAUI app, showed that around 7ms were spent\nloading UTF32 and Latin1 encodings the first time <code>System.Uri<\/code> APIs\nare used:<\/p>\n<pre><code class=\"language-csharp\">namespace System\r\n{\r\n    internal static class UriHelper\r\n    {\r\n        internal static readonly Encoding s_noFallbackCharUTF8 = Encoding.GetEncoding(\r\n            Encoding.UTF8.CodePage, new EncoderReplacementFallback(\"\"), new DecoderReplacementFallback(\"\"));<\/code><\/pre>\n<p>This field was left in place accidentally. Simply removing the\n<code>s_noFallbackCharUTF8<\/code> field, improved startup for any .NET\napplication using <code>System.Uri<\/code> or related APIs.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/65326\">dotnet\/runtime#65326<\/a> for details about this improvement.<\/p>\n<h2>App Size Improvements<\/h2>\n<h3>Fix defaults for MauiImage Sizes<\/h3>\n<p>The <code>dotnet new maui<\/code> template displays a friendly &#8220;.NET bot&#8221; image.\nThis is implemented by using an <code>.svg<\/code> file as a <code>MauiImage<\/code> with the\ncontents:<\/p>\n<pre><code class=\"language-xml\">&lt;svg width=\"419\" height=\"519\" viewBox=\"0 0 419 519\" fill=\"none\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"&gt;\r\n&lt;!-- everything else --&gt;<\/code><\/pre>\n<p>By default, <code>MauiImage<\/code> uses the <code>width<\/code> and <code>height<\/code> values in the\n<code>.svg<\/code> as the &#8220;base size&#8221; of the image. Reviewing the build output\nshowed these images are scaled to:<\/p>\n<pre><code class=\"language-dotnetcli\">obj\\Release\\net6.0-android\\resizetizer\\r\\mipmap-xxxhdpi\r\n    appiconfg.png = 1824x1824\r\n    dotnet_bot.png = 1676x2076<\/code><\/pre>\n<p>This seemed to be a bit oversized for Android devices? We can simply\nspecify <code>%(BaseSize)<\/code> in the template, which also gives an example on\nhow to choose an appropriate size for these images:<\/p>\n<pre><code class=\"language-xml\">&lt;!-- Splash Screen --&gt;\r\n&lt;MauiSplashScreen Include=\"Resources\\appiconfg.svg\" Color=\"#512BD4\" BaseSize=\"128,128\" \/&gt;\r\n\r\n&lt;!-- Images --&gt;\r\n&lt;MauiImage Include=\"Resources\\Images\\*\" \/&gt;\r\n&lt;MauiImage Update=\"Resources\\Images\\dotnet_bot.svg\" BaseSize=\"168,208\" \/&gt;<\/code><\/pre>\n<p>Which results in more appropriate sizes:<\/p>\n<pre><code class=\"language-dotnetcli\">obj\\Release\\net6.0-android\\resizetizer\\r\\mipmap-xxxhdpi\\\r\n    appiconfg.png = 512x512\r\n    dotnet_bot.png = 672x832<\/code><\/pre>\n<p>We also could have modified the <code>.svg<\/code> contents, but that may not be\ndesirable depending on how a graphics designer would use this image in\nother design tools.<\/p>\n<p>In another example, a 3008&#215;5340 <code>.jpg<\/code> image:<\/p>\n<pre><code class=\"language-xml\">&lt;MauiImage Include=\"Resources\\Images\\large.jpg\" \/&gt;<\/code><\/pre>\n<p>&#8230;was being upscaled to 21360&#215;12032! Setting <code>Resize=\"false\"<\/code> would\nprevent the image from being resized, but we made this the default\noption for non-vector images. Going forward, developers should be able\nto rely on the default value or specify <code>%(BaseSize)<\/code> and <code>%(Resize)<\/code>\nas needed.<\/p>\n<p>These changes improved startup performance as well as app size. See\n<a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4759\">dotnet\/maui#4759<\/a> and <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/6419\">dotnet\/maui#6419<\/a> for details\nabout these improvements.<\/p>\n<h3>Remove Application.Properties and DataContractSerializer<\/h3>\n<p>Xamarin.Forms had an API for persisting key-value pairs through a\n<code>Application.Properties<\/code> dictionary. This used\n<code>DataContractSerializer<\/code> internally, which is not the best choice for\nmobile applications that are self-contained &amp; trimmed. Parts of\n<code>System.Xml<\/code> from the BCL can be quite large, and we don&#8217;t want to pay\nfor this cost in every .NET MAUI application.<\/p>\n<p>Simply removing this API and all usage of <code>DataContractSerializer<\/code>\nresulted in a ~855KB improvement on Android and a ~1MB improvement on\niOS.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4976\">dotnet\/maui#4976<\/a> for details about this improvement.<\/p>\n<h3>Trim Unused HTTP Implementations<\/h3>\n<p>The linker switch, <code>System.Net.Http.UseNativeHttpHandler<\/code> was not\nappropriately trimming away the underlying managed HTTP handler\n(<code>SocketsHttpHandler<\/code>). By default, <code>AndroidMessageHandler<\/code> and\n<code>NSUrlSessionHandler<\/code> are used to leverage the underlying Android and\niOS networking stacks.<\/p>\n<p>By fixing this, more IL code is able to be trimmed away in any .NET\nMAUI application. In one example, an Android application using HTTP\nwas able to completely trim away several assemblies:<\/p>\n<ul>\n<li><code>Microsoft.Win32.Primitives.dll<\/code><\/li>\n<li><code>System.Formats.Asn1.dll<\/code><\/li>\n<li><code>System.IO.Compression.Brotli.dll<\/code><\/li>\n<li><code>System.Net.NameResolution.dll<\/code><\/li>\n<li><code>System.Net.NetworkInformation.dll<\/code><\/li>\n<li><code>System.Net.Quic.dll<\/code><\/li>\n<li><code>System.Net.Security.dll<\/code><\/li>\n<li><code>System.Net.Sockets.dll<\/code><\/li>\n<li><code>System.Runtime.InteropServices.RuntimeInformation.dll<\/code><\/li>\n<li><code>System.Runtime.Numerics.dll<\/code><\/li>\n<li><code>System.Security.Cryptography.Encoding.dll<\/code><\/li>\n<li><code>System.Security.Cryptography.X509Certificates.dll<\/code><\/li>\n<li><code>System.Threading.Channels.dll<\/code><\/li>\n<\/ul>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/64852\">dotnet\/runtime#64852<\/a>, <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6749\">xamarin-android#6749<\/a>, and\n<a href=\"https:\/\/github.com\/xamarin\/xamarin-macios\/pull\/14297\">xamarin-macios#14297<\/a> for details about this improvement.<\/p>\n<h2>Improvements in the <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast<\/a> Sample<\/h2>\n<p>We made a few adjustments to the sample itself where the change was\nconsidered &#8220;best practice&#8221;.<\/p>\n<h3>Remove Microsoft.Extensions.Http Usage<\/h3>\n<p>Using Microsoft.Extensions.Http is too heavy-weight for mobile applications,\nand doesn&#8217;t provide any real value in this situation.<\/p>\n<p>So instead of using DI for <code>HttpClient<\/code>:<\/p>\n<pre><code class=\"language-csharp\">builder.Services.AddHttpClient&lt;ShowsService&gt;(client =&gt; \r\n{\r\n    client.BaseAddress = new Uri(Config.APIUrl);\r\n});\r\n\r\n\/\/ Then in the service ctor\r\npublic ShowsService(HttpClient httpClient, ListenLaterService listenLaterService)\r\n{\r\n    this.httpClient = httpClient;\r\n    \/\/ ...\r\n}<\/code><\/pre>\n<p>We simply create an <code>HttpClient<\/code> to be used within the service:<\/p>\n<pre><code class=\"language-csharp\">public ShowsService(ListenLaterService listenLaterService)\r\n{\r\n    this.httpClient = new HttpClient() { BaseAddress = new Uri(Config.APIUrl) };\r\n    \/\/ ...\r\n}<\/code><\/pre>\n<p>We would recommend using a single <code>HttpClient<\/code> instance per web\nservice your application needs to interact with.<\/p>\n<p>See <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/66863\">dotnet\/runtime#66863<\/a> and <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\/pull\/44\">dotnet-podcasts#44<\/a> for\ndetails about this improvement.<\/p>\n<h3>Remove Newtonsoft.Json Usage<\/h3>\n<p>The <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\">.NET Podcast<\/a> Sample was using a library called\n<a href=\"https:\/\/github.com\/jamesmontemagno\/monkey-cache\"><code>MonkeyCache<\/code><\/a> that depends on Newtonsoft.Json. This is not a\nproblem in itself, except that .NET MAUI + Blazor applications depend\non a few ASP.NET Core libraries which in turn depend on\nSystem.Text.Json. The application was effectively &#8220;paying twice&#8221; for\nJSON parsing libraries, which had an impact on app size.<\/p>\n<p>We ported <code>MonkeyCache<\/code> 2.0 to use System.Text.Json, eliminating the\nneed for <code>Newtonsoft.Json<\/code> in the app. This reduced the app size on\niOS from 29.3MB to 26.1MB!<\/p>\n<p>See <a href=\"https:\/\/github.com\/jamesmontemagno\/monkey-cache\/pull\/109\">monkey-cache#109<\/a> and <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\/pull\/58\">dotnet-podcasts#58<\/a> for details\nabout this improvement.<\/p>\n<h3>Run First Network Request in Background<\/h3>\n<p>Reviewing <code>dotnet trace<\/code> output, the initial request in <code>ShowsService<\/code>\nwas blocking the UI thread initializing <code>Connectivity.NetworkAccess<\/code>,\n<code>Barrel.Current.Get<\/code>, and <code>HttpClient<\/code>. This work could be done in a\nbackground thread &#8212; resulting in a faster startup time in this case.\nWrapping the first call in <code>Task.Run()<\/code> improves startup of this\nsample by a reasonable amount.<\/p>\n<p>An average of 10 runs on a Pixel 5a device:<\/p>\n<pre><code class=\"language-dotnetcli\">Before\r\nAverage(ms): 843.7\r\nAverage(ms): 847.8\r\nAfter\r\nAverage(ms): 817.2\r\nAverage(ms): 812.8<\/code><\/pre>\n<p>This type of change, it is always recommended to base the decision off\nof <code>dotnet trace<\/code> or other profiling results and measure the changes\nbefore and after.<\/p>\n<p>See <a href=\"https:\/\/github.com\/microsoft\/dotnet-podcasts\/pull\/57\">dotnet-podcasts#57<\/a> for details about this improvement.<\/p>\n<h2>Experimental or Advanced Options<\/h2>\n<p>If you would like to optimize your .NET MAUI application even further\non Android, there are a couple features that are either advanced or\nexperimental, and not enabled by default.<\/p>\n<h3>Trimming Resource.designer.cs<\/h3>\n<p>Since the beginning of Xamarin, Android applications include a\ngenerated <code>Properties\/Resource.designer.cs<\/code> file for accessing integer\nidentifiers for <code>AndroidResource<\/code> files. This is a C#\/managed version\nof the <code>R.java<\/code> class, to allow usage of these identifiers as plain C#\nfields (and sometimes <code>const<\/code>) without any interop into Java.<\/p>\n<p>In an Android Studio &#8220;library&#8221; project, when you include a file like\n<code>res\/drawable\/foo.png<\/code>, you get a field like:<\/p>\n<pre><code class=\"language-java\">package com.yourlibrary;\r\n\r\npublic class R\r\n{\r\n    public class drawable\r\n    {\r\n        \/\/ The actual integer here maps to a table inside the final .apk file\r\n        public final int foo = 1234;\r\n    }\r\n}<\/code><\/pre>\n<p>You can use this value, for example, to display this image in an <code>ImageView<\/code>:<\/p>\n<pre><code class=\"language-java\">ImageView imageView = new ImageView(this);\r\nimageView.setImageResource(R.drawable.foo);<\/code><\/pre>\n<p>When you build <code>com.yourlibrary.aar<\/code>, the Android gradle plugin\ndoesn&#8217;t actually put this class inside the package. Instead, the\nconsuming Android application is what actually <em>knows<\/em> what the\ninteger will be. So the <code>R<\/code> class is generated when the Android\napplication builds, generating an <code>R<\/code> class for every Android library\nconsumed.<\/p>\n<p>Xamarin.Android took a different approach, to do this integer fixup at\nruntime. There wasn&#8217;t really a great precedence of doing something\nlike this with C# and MSBuild? So for example, a C# Android library\nmight have:<\/p>\n<pre><code class=\"language-csharp\">public class Resource\r\n{\r\n    public class Drawable\r\n    {\r\n        \/\/ The actual integer here is *not* final\r\n        public int foo = -1;\r\n    }\r\n}<\/code><\/pre>\n<p>Then the main application would have code like:<\/p>\n<pre><code class=\"language-csharp\">public class Resource\r\n{\r\n    public class Drawable\r\n    {\r\n        public Drawable()\r\n        {\r\n            \/\/ Copy the value at runtime\r\n            global::MyLibrary.Resource.Drawable.foo = foo;\r\n        }\r\n\r\n        \/\/ The actual integer here *is* final\r\n        public const int foo = 1234;\r\n    }\r\n}<\/code><\/pre>\n<p>This situation has been working well for quite some time, but\nunfortunately the number of resources in Google&#8217;s libraries like\nAndroidX, Material, Google Play Services, etc. have really started to\ncompound. In <a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/2606\">dotnet\/maui#2606<\/a>, for example, 21,497 fields were\nset at startup! We created a way to workaround this at the time, but\nwe also have a new custom trimmer step to perform fixups at build time\n(during trimming) instead of at runtime.<\/p>\n<p>To opt into the feature:<\/p>\n<pre><code class=\"language-xml\">&lt;AndroidLinkResources&gt;true&lt;\/AndroidLinkResources&gt;<\/code><\/pre>\n<p>This will make your <code>Release<\/code> builds replace cases like:<\/p>\n<pre><code class=\"language-csharp\">ImageView imageView = new(this);\r\nimageView.SetImageResource(Resource.Drawable.foo);<\/code><\/pre>\n<p>To instead, inline the integer directly:<\/p>\n<pre><code class=\"language-csharp\">ImageView imageView = new(this);\r\nimageView.SetImageResource(1234); \/\/ The actual integer here *is* final<\/code><\/pre>\n<p>The one known issue with this feature are <code>Styleable<\/code> values like:<\/p>\n<pre><code class=\"language-csharp\">public partial class Styleable\r\n{\r\n    public static int[] ActionBarLayout = new int[] { 16842931 };\r\n}<\/code><\/pre>\n<p>Replacing <code>int[]<\/code> values is not currently supported, which made it not\nsomething we can enable by default. Some apps will be able to turn on\nthis feature, the <code>dotnet new maui<\/code> template and perhaps many .NET\nMAUI Android applications would not run into this limitation.<\/p>\n<p>In a future .NET release, we may be able to enable\n<code>$(AndroidLinkResources)<\/code> by default, or perhaps redesign things\nentirely.<\/p>\n<p>See <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/5317\">xamarin-android#5317<\/a>, <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/pull\/6696\">xamarin-android#6696<\/a>, and\n<a href=\"https:\/\/github.com\/dotnet\/maui\/pull\/4912\">dotnet\/maui#4912<\/a> for details about this feature.<\/p>\n<h3>R8 Java Code Shrinker<\/h3>\n<p><a href=\"https:\/\/r8.googlesource.com\/r8\/\">R8<\/a> is whole-program optimization,\nshrinking and minification tool that converts java byte code to\noptimized dex code. <code>R8<\/code> uses the <code>Proguard<\/code> keep rule format for\nspecifying the entry points for an application. As you might expect,\nmany applications require additional <code>Proguard<\/code> rules to keep things\nworking. <code>R8<\/code> can be too aggressive, and remove something that is\ncalled by Java reflection, etc. We don&#8217;t yet have a good approach for\nmaking this the default across all .NET Android applications.<\/p>\n<p>To opt into using <code>R8<\/code> for <code>Release<\/code> builds, add the following to your\n<code>.csproj<\/code>:<\/p>\n<pre><code class=\"language-xml\">&lt;!-- NOTE: not recommended for Debug builds! --&gt;\r\n&lt;AndroidLinkTool Condition=\"'$(Configuration)' == 'Release'\"&gt;r8&lt;\/AndroidLinkTool&gt;<\/code><\/pre>\n<p>If launching a <code>Release<\/code> build of your application crashes after\nenabling this, review <a href=\"https:\/\/docs.microsoft.com\/xamarin\/android\/deploy-test\/debugging\/android-debug-log\"><code>adb logcat<\/code><\/a> output to see what went wrong.<\/p>\n<p>If you see a <code>java.lang.ClassNotFoundException<\/code> or\n<code>java.lang.MethodNotFoundException<\/code>, you may need to add a\n<code>ProguardConfiguration<\/code> file to your project such as:<\/p>\n<pre><code class=\"language-xml\">&lt;ItemGroup&gt;\r\n  &lt;ProguardConfiguration Include=\"proguard.cfg\" \/&gt;\r\n&lt;\/ItemGroup&gt;<\/code><\/pre>\n<pre><code class=\"language-groovy\">-keep class com.thepackage.TheClassYouWantToPreserve { *; &lt;init&gt;(...); }<\/code><\/pre>\n<p>We are investigating options to enable <code>R8<\/code> by default in a future\n.NET release.<\/p>\n<p>See our <a href=\"https:\/\/github.com\/xamarin\/xamarin-android\/blob\/main\/Documentation\/guides\/D8andR8.md\">documentation on D8\/R8<\/a> for details.<\/p>\n<h2>AOT Everything<\/h2>\n<p>Profiled AOT is the default, because it gives the best tradeoff\nbetween app size and startup performance. If app size is not a concern\nfor your application, you might consider using AOT for all .NET\nassemblies.<\/p>\n<p>To opt into this, add the following to your <code>.csproj<\/code> for <code>Release<\/code>\nconfigurations:<\/p>\n<pre><code class=\"language-xml\">&lt;PropertyGroup Condition=\"'$(Configuration)' == 'Release'\"&gt;\r\n  &lt;RunAOTCompilation&gt;true&lt;\/RunAOTCompilation&gt;\r\n  &lt;AndroidEnableProfiledAot&gt;false&lt;\/AndroidEnableProfiledAot&gt;\r\n&lt;\/PropertyGroup&gt;<\/code><\/pre>\n<p>This will reduce the amount of JIT compilation that happens during\nstartup in your application, as well as navigation to later screens,\netc.<\/p>\n<h3>AOT and LLVM<\/h3>\n<p><a href=\"https:\/\/llvm.org\/\">LLVM<\/a> provides a modern source- and\ntarget-independent optimizer that can be combined with Mono AOT\nCompiler output. The result is a slightly larger app size and longer\n<code>Release<\/code> build times, with better runtime performance.<\/p>\n<p>To opt into using LLVM for <code>Release<\/code> builds, add the following to your\n<code>.csproj<\/code>:<\/p>\n<pre><code class=\"language-xml\">&lt;PropertyGroup Condition=\"'$(Configuration)' == 'Release'\"&gt;\r\n  &lt;RunAOTCompilation&gt;true&lt;\/RunAOTCompilation&gt;\r\n  &lt;EnableLLVM&gt;true&lt;\/EnableLLVM&gt;\r\n&lt;\/PropertyGroup&gt;<\/code><\/pre>\n<p>This feature can be used in combination with Profiled AOT (or AOT-ing\neverything). Compare your application before &amp; after to know what\nimpact <code>EnableLLVM<\/code> has on your application size and startup\nperformance.<\/p>\n<p>Currently, an Android NDK is required to be installed to use this\nfeature. If we can solve this requirement, <code>EnableLLVM<\/code> could become\nthe default in a future .NET Release.<\/p>\n<p>See our <a href=\"https:\/\/docs.microsoft.com\/xamarin\/android\/deploy-test\/building-apps\/build-properties#enablellvm\">documentation on <code>EnableLLVM<\/code><\/a> for details.<\/p>\n<h3>Record a Custom AOT Profile<\/h3>\n<p>Profiled AOT by default uses &#8220;built-in&#8221; profiles that we ship in .NET\nMAUI and Android workloads to be useful for most applications. To get\nthe optimal startup performance, you ideally would record a profile\nspecific to your application. We have an experimental\n<a href=\"https:\/\/github.com\/jonathanpeppers\/Mono.Profiler.Android\">Mono.Profiler.Android<\/a> package for this scenario.<\/p>\n<p>To record a profile:<\/p>\n<pre><code class=\"language-dotnetcli\">dotnet add package Mono.AotProfiler.Android\r\ndotnet build -t:BuildAndStartAotProfiling\r\n# Wait until app launches, or you navigate to a screen\r\ndotnet build -t:FinishAotProfiling<\/code><\/pre>\n<p>This will produce a <code>custom.aprof<\/code> in your project directory. To use\nit for future builds:<\/p>\n<pre><code class=\"language-xml\">&lt;ItemGroup&gt;\r\n  &lt;AndroidAotProfile Include=\"custom.aprof\" \/&gt;\r\n&lt;\/ItemGroup&gt;<\/code><\/pre>\n<p>We are working on full support for recording custom profiles in a\nfuture .NET release.<\/p>\n<h2>Conclusion<\/h2>\n<p>I hope you&#8217;ve enjoyed our .NET MAUI performance treatise. You should\ncertainly pat yourself on the back if you made it this far.<\/p>\n<p>Please try out .NET MAUI, file issues, or learn more at\n<a href=\"https:\/\/dot.net\/maui\">http:\/\/dot.net\/maui<\/a>!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Want to know why .NET MAUI apps boot faster, run smoother, and are smaller? This post break down how we made .NET MAUI fast!<\/p>\n","protected":false},"author":1345,"featured_media":40342,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7233,3009],"tags":[7238,108],"class_list":["post-40341","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-maui","category-performance","tag-net-maui","tag-performance"],"acf":[],"blog_post_summary":"<p>Want to know why .NET MAUI apps boot faster, run smoother, and are smaller? This post break down how we made .NET MAUI fast!<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/40341","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=40341"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/40341\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/40342"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=40341"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=40341"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=40341"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}