Description
We've observed that a plain "hello world" Xamarin.Android application (dotnet new android
) starts up slower
than it did under net7. The slowdown cannot be attribute to changes in XA itself, as there haven't been any
changes that could affect performance to this degree. Our performance test measures three startup metrics:
- "Application displayed time" as reported by the Android OS. This is measured by the system and is the
time between starting the application (via intent or by clicking its icon in the UI) and the point where
the application frame is fully painted. This metric regressed by an average of 11% across 10 test runs. - First managed call performance (we call a static class method which does some initialization for us).
This metric regressed by around 50% - The "total init time" is the time spent in the native XA runtime. This includes the "first managed call"
sequence above, plus MonoVM initialization, loading of some assemblies as needed to make the first managed
call, setting up handlers etc. This metric regressed by about 25%
XA uses profiled AOT to improve startup time, and recording a new profile allowed me to see what new methods
are called at startup. Here's a set of them that has the biggest potential to adversely affect performance:
+ bool System.Collections.Generic.List`1/Enumerator<System.Diagnostics.Metrics.Instrument>:MoveNext ()
+ bool System.Collections.Generic.List`1/Enumerator<System.Diagnostics.Metrics.Instrument>:MoveNextRare ()
+ bool System.Collections.Generic.List`1<System.Diagnostics.Metrics.Instrument>:Contains (System.Diagnostics.Metrics.Instrument)
+ bool System.Diagnostics.Metrics.Meter:AddInstrument (System.Diagnostics.Metrics.Instrument)
+ bool System.Globalization.CultureData:IsValidCultureName (string,int&,int&)
+ bool System.Guid:TryWriteBytes (System.Span`1<byte>)
+ bool System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1/ConfiguredTaskAwaiter<Java.Net.Proxy>:get_IsCompleted ()
+ bool System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1/ConfiguredTaskAwaiter<System.Net.HttpStatusCode>:get_IsCompleted ()
+ bool System.Runtime.CompilerServices.ConfiguredTaskAwaitable/ConfiguredTaskAwaiter:get_IsCompleted ()
+ bool System.RuntimeTypeHandle:IsFunctionPointer (System.RuntimeType)
+ Interop/Globalization/ResultCode System.TimeZoneInfo/<>c:<GetDisplayName>b__192_0 (System.Span`1<char>,string,string,Interop/Globalization/TimeZoneDisplayNameType)
+ Interop/Sys/OpenFlags Microsoft.Win32.SafeHandles.SafeFileHandle:PreOpenConfigurationFromOptions (System.IO.FileMode,System.IO.FileAccess,System.IO.FileShare,System.IO.FileOptions,bool)
+ int System.Collections.Generic.List`1<System.Diagnostics.Metrics.Instrument>:IndexOf (System.Diagnostics.Metrics.Instrument)
+ object System.Diagnostics.Metrics.Instrument:get_SyncObject ()
+ object System.Reflection.MethodBaseInvoker:InterpretedInvoke_Constructor (object,intptr*)
+ object System.Reflection.MethodBaseInvoker:InterpretedInvoke_Method (object,intptr*)
+ object System.Reflection.MethodBaseInvoker:InvokeConstructorWithoutAlloc (object,bool)
+ object System.Reflection.MethodBaseInvoker:InvokeDirectByRefWithFewArgs (object,System.Span`1<object>,System.Reflection.BindingFlags)
+ object System.Reflection.MethodBaseInvoker:InvokeWithFewArgs (object,System.Reflection.BindingFlags,System.Reflection.Binder,object[],System.Globalization.CultureInfo)
+ object System.Reflection.MethodBaseInvoker:InvokeWithNoArgs (object,System.Reflection.BindingFlags)
+ string System.Globalization.CultureData:NormalizeCultureName (string,System.ReadOnlySpan`1<char>,int&)
+ string System.TimeZoneInfo:get_DaylightName ()
+ string System.TimeZoneInfo:get_DisplayName ()
+ string System.TimeZoneInfo:PopulateDaylightDisplayName ()
+ string System.TimeZoneInfo:PopulateDisplayName ()
+ string System.TimeZoneInfo:PopulateStandardDisplayName ()
+ System.Collections.Generic.List`1/Enumerator<System.Diagnostics.Metrics.Instrument> System.Collections.Generic.List`1<System.Diagnostics.Metrics.Instrument>:GetEnumerator ()
+ System.Collections.Generic.List`1<System.Diagnostics.Metrics.MeterListener> System.Diagnostics.Metrics.MeterListener:GetAllListeners ()
+ System.Diagnostics.Metrics.Counter`1<long> System.Diagnostics.Metrics.Meter:CreateCounter (string,string,string)
+ System.Diagnostics.Metrics.Counter`1<long> System.Diagnostics.Metrics.Meter:CreateCounter (string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ System.Diagnostics.Metrics.Histogram`1<double> System.Diagnostics.Metrics.Meter:CreateHistogram (string,string,string)
+ System.Diagnostics.Metrics.Histogram`1<double> System.Diagnostics.Metrics.Meter:CreateHistogram (string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ System.Diagnostics.Metrics.Instrument System.Diagnostics.Metrics.Meter/<>c__DisplayClass29_0`1<System.Int64>:<CreateCounter>b__0 ()
+ System.Diagnostics.Metrics.Instrument System.Diagnostics.Metrics.Meter/<>c__DisplayClass31_0`1<System.Double>:<CreateHistogram>b__0 ()
+ System.Diagnostics.Metrics.Instrument System.Diagnostics.Metrics.Meter/<>c__DisplayClass33_0`1<System.Int64>:<CreateUpDownCounter>b__0 ()
+ System.Diagnostics.Metrics.Instrument System.Diagnostics.Metrics.Meter:GetCachedInstrument (System.Collections.Generic.List`1<System.Diagnostics.Metrics.Instrument>,System.Type,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ System.Diagnostics.Metrics.Instrument System.Diagnostics.Metrics.Meter:GetOrCreateInstrument (System.Type,string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>,System.Func`1<System.Diagnostics.Metrics.Instrument>)
+ System.Diagnostics.Metrics.Instrument System.Diagnostics.Metrics.Meter:GetOrCreateInstrument (System.Type,string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>,System.Func`1<System.Diagnostics.Metrics.Instrument>)
+ System.Diagnostics.Metrics.Meter System.Net.Http.Metrics.MetricsHandler/SharedMeter:get_Instance ()
+ System.Diagnostics.Metrics.UpDownCounter`1<long> System.Diagnostics.Metrics.Meter:CreateUpDownCounter (string,string,string)
+ System.Diagnostics.Metrics.UpDownCounter`1<long> System.Diagnostics.Metrics.Meter:CreateUpDownCounter (string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ System.Guid System.Reflection.RuntimeModule:get_ModuleVersionId ()
+ System.Guid System.Reflection.RuntimeModule:GetModuleVersionId ()
+ System.Net.Http.Metrics.MetricsHandler System.Net.Http.HttpClientHandler:SetupHandlerChain ()
+ System.Reflection.MethodInfo System.Type:GetMethod (string,System.Reflection.BindingFlags,System.Reflection.Binder,System.Reflection.CallingConventions,System.Type[],System.Reflection.ParameterModifier[])
+ System.Reflection.MethodInfo System.Type:GetMethod (string,System.Reflection.BindingFlags,System.Reflection.Binder,System.Type[],System.Reflection.ParameterModifier[])
+ System.Reflection.Module System.RuntimeType:get_Module ()
+ System.Threading.Tasks.Task`1<System.Net.Http.HttpResponseMessage> System.Net.Http.HttpMessageHandlerStage:SendAsync (System.Net.Http.HttpRequestMessage,System.Threading.CancellationToken)
+ System.Threading.Tasks.Task`1<System.Net.Http.HttpResponseMessage> System.Threading.Tasks.ValueTask`1<System.Net.Http.HttpResponseMessage>:AsTask ()
+ System.Threading.Tasks.ValueTask`1<System.Net.Http.HttpResponseMessage> System.Net.Http.Metrics.MetricsHandler:SendAsync (System.Net.Http.HttpRequestMessage,bool,System.Threading.CancellationToken)
+ System.TimeZoneInfo/AdjustmentRule[] System.Collections.Generic.List`1<System.TimeZoneInfo/AdjustmentRule>:ToArray ()
+ void System.Collections.Generic.List`1/Enumerator<System.Diagnostics.Metrics.Instrument>:.ctor (System.Collections.Generic.List`1<System.Diagnostics.Metrics.Instrument>)
+ void System.Diagnostics.Metrics.Counter`1<System.Int64>:.ctor (System.Diagnostics.Metrics.Meter,string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ void System.Diagnostics.Metrics.Histogram`1<System.Double>:.ctor (System.Diagnostics.Metrics.Meter,string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ void System.Diagnostics.Metrics.Instrument`1<System.Double>:.ctor (System.Diagnostics.Metrics.Meter,string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ void System.Diagnostics.Metrics.Instrument`1<System.Int64>:.ctor (System.Diagnostics.Metrics.Meter,string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ void System.Diagnostics.Metrics.Instrument/<>c:.cctor ()
+ void System.Diagnostics.Metrics.Instrument/<>c:.ctor ()
+ void System.Diagnostics.Metrics.Instrument:.cctor ()
+ void System.Diagnostics.Metrics.Instrument:.ctor (System.Diagnostics.Metrics.Meter,string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ void System.Diagnostics.Metrics.Instrument:Publish ()
+ void System.Diagnostics.Metrics.Instrument:ValidateTypeParameter ()
+ void System.Diagnostics.Metrics.Instrument:ValidateTypeParameter ()
+ void System.Diagnostics.Metrics.Meter/<>c:.cctor ()
+ void System.Diagnostics.Metrics.Meter/<>c:.ctor ()
+ void System.Diagnostics.Metrics.Meter:.cctor ()
+ void System.Diagnostics.Metrics.Meter:.ctor (string)
+ void System.Diagnostics.Metrics.Meter:.ctor (string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>,object)
+ void System.Diagnostics.Metrics.Meter:Initialize (string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>,object)
+ void System.Diagnostics.Metrics.MeterListener:.cctor ()
+ void System.Diagnostics.Metrics.MetricsEventSource:.cctor ()
+ void System.Diagnostics.Metrics.MetricsEventSource:.ctor ()
+ void System.Diagnostics.Metrics.UpDownCounter`1<System.Int64>:.ctor (System.Diagnostics.Metrics.Meter,string,string,string,System.Collections.Generic.IEnumerable`1<System.Collections.Generic.KeyValuePair`2<string, object>>)
+ void System.Net.Http.Metrics.MetricsHandler:.ctor (System.Net.Http.HttpMessageHandler,System.Diagnostics.Metrics.IMeterFactory,System.Diagnostics.Metrics.Meter&)
+ void System.Net.Http.Metrics.MetricsHandler/SharedMeter:.cctor ()
+ void System.Net.Http.Metrics.MetricsHandler/SharedMeter:.ctor ()
+ void System.TimeZoneInfo:GetDaylightDisplayName (string,string&)
+ void System.TimeZoneInfo:GetFullValueForDisplayNameField (string,System.TimeSpan,string&)
+ void System.TimeZoneInfo:GetStandardDisplayName (string,string&)
The worrying parts are the HTTP requests, the System.Diagnostic.Metrics
calls, the TPL use and the timezone data - the latter was greatly improved lately, but it appears that accessing timezone data has somehow become part of the default startup path.
startup (the biggest issue here is retrieving the zone's display name, which is an expensive operation)