Skip to content

Commit

Permalink
[xaml] improve performance in debug-mode (#21460)
Browse files Browse the repository at this point in the history
* [xaml] improve performance in debug-mode

Applies to: #18505
Context: https://github.com/dotnet/maui/files/13251041/MauiCollectionView.zip

In reviewing various issues about `CollectionView`, there is a pattern of:

* My app is slow (in `Debug` mode)

* Oh, but it seems completely OK in `Release`!

This is due to several features enabled in `Debug` mode, such as:

* On mobile, the Mono interpreter is used (enables C# hot reload)

* Xaml compilation is disabled (enables XAML hot reload)

* Other debug features are enabled (such as the `$(Optimize)` flag)

I attached `dotnet-trace` to the app above on a Pixel 5 while
debugging, just to see what I could find. I also had XAML & C# hot
reload enabled.

One thing that appears when parsing XAML is:

      2.11s microsoft.maui.controls.xaml!Microsoft.Maui.Controls.Xaml.ApplyPropertiesVisitor.GetBindableProperty(System.Type,string
    257.40ms System.Private.CoreLib!System.String.Format(string,object,object)
    172.25ms microsoft.maui.controls!Microsoft.Maui.Controls.Xaml.XamlParseException.FormatMessage(string,System.Xml.IXmlLineInfo)

So about ~2 seconds of this app's startup was in
`ApplyPropertiesVisitor.GetBindableProperty()`. Looking at what is
happening:

    if (exception == null && bindableFieldInfo == null)
    {
        exception =
            new XamlParseException(
                Format("BindableProperty {0} not found on {1}", localName + "Property", elementType.Name), lineInfo);
    }
    //...
    if (throwOnError)
        throw exception; // only use of `exception`

But then `throwOnError` is actually `false` in this case, so a
`XamlParseException` is created and not used. I could just reorder the
logic to avoid creating a `XamlParseException` and calling `Format()`.

Next, I noticed the System.Reflection usage:

    var bindableFieldInfo = elementType.GetFields(supportedFlags)
        .FirstOrDefault(fi => (fi.IsAssembly || fi.IsPublic) && fi.Name == localName + "Property");

If this were called on many bindable properties on `Label`, `Button`,
etc., this code would create lots of `FieldInfo[]` arrays and iterate
until a matching property (field) is found.

I think we can change this to avoid creating `FieldInfo[]` arrays,
such as:

    var bindableFieldInfo = elementType.GetField(localName + "Property", supportedFlags);
    if (bindableFieldInfo is not null && (bindableFieldInfo.IsAssembly || bindableFieldInfo.IsPublic))
    {
        //...

With these changes in place, it significantly improves the call:

    816.35ms microsoft.maui.controls.xaml!Microsoft.Maui.Controls.Xaml.ApplyPropertiesVisitor.GetBindableProperty(System.Type,string

Saving over about ~1.3 seconds of startup time in debug mode.

This should improve the performance of XAML parsing on all platforms,
which is most likely being used while debugging. You *can* disable
Xaml compilation in `Release` mode, but it is not recommended.

* Update BuildWarningsUtilities.cs

* Remove `bool throwOnError = false` parameter
  • Loading branch information
jonathanpeppers authored Mar 29, 2024
1 parent c7d1a4e commit a7434fc
Show file tree
Hide file tree
Showing 2 changed files with 8 additions and 19 deletions.
25 changes: 7 additions & 18 deletions src/Controls/src/Xaml/ApplyPropertiesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,26 +326,15 @@ static bool GetRealNameAndType(ref Type elementType, string namespaceURI, ref st
return false;
}

static BindableProperty GetBindableProperty(Type elementType, string localName, IXmlLineInfo lineInfo,
bool throwOnError = false)
static BindableProperty GetBindableProperty(Type elementType, string localName, IXmlLineInfo lineInfo)
{
// F# does not support public fields, so allow internal (Assembly) as well as public
const BindingFlags supportedFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
var bindableFieldInfo = elementType.GetFields(supportedFlags)
.FirstOrDefault(fi => (fi.IsAssembly || fi.IsPublic) && fi.Name == localName + "Property");

Exception exception = null;
if (exception == null && bindableFieldInfo == null)
var bindableFieldInfo = elementType.GetField(localName + "Property", supportedFlags);
if (bindableFieldInfo is not null && (bindableFieldInfo.IsAssembly || bindableFieldInfo.IsPublic))
{
exception =
new XamlParseException(
Format("BindableProperty {0} not found on {1}", localName + "Property", elementType.Name), lineInfo);
}

if (exception == null)
return bindableFieldInfo.GetValue(null) as BindableProperty;
if (throwOnError)
throw exception;
}
return null;
}

Expand All @@ -355,7 +344,7 @@ static object GetTargetProperty(object xamlelement, XmlName propertyName, object
//If it's an attached BP, update elementType and propertyName
var bpOwnerType = xamlelement.GetType();
GetRealNameAndType(ref bpOwnerType, propertyName.NamespaceURI, ref localName, rootElement, lineInfo);
var property = GetBindableProperty(bpOwnerType, localName, lineInfo, false);
var property = GetBindableProperty(bpOwnerType, localName, lineInfo);

if (property != null)
return property;
Expand Down Expand Up @@ -397,7 +386,7 @@ void registerSourceInfo(object target, string path)
//If it's an attached BP, update elementType and propertyName
var bpOwnerType = element.GetType();
var attached = GetRealNameAndType(ref bpOwnerType, propertyName.NamespaceURI, ref localName, rootElement, lineInfo);
var property = GetBindableProperty(bpOwnerType, localName, lineInfo, false);
var property = GetBindableProperty(bpOwnerType, localName, lineInfo);

//If the target is an event, connect
if (xpe == null && TryConnectEvent(element, localName, attached, value, rootElement, lineInfo, out xpe))
Expand Down Expand Up @@ -452,7 +441,7 @@ public static object GetPropertyValue(object xamlElement, XmlName propertyName,
//If it's an attached BP, update elementType and propertyName
var bpOwnerType = xamlElement.GetType();
var attached = GetRealNameAndType(ref bpOwnerType, propertyName.NamespaceURI, ref localName, rootElement, lineInfo);
var property = GetBindableProperty(bpOwnerType, localName, lineInfo, false);
var property = GetBindableProperty(bpOwnerType, localName, lineInfo);

//If it's a BindableProberty, GetValue
if (xpe == null && TryGetValue(xamlElement, property, attached, out var value, lineInfo, out xpe, out targetProperty))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ public static void AssertWarnings(this List<WarningsPerFile> actualWarnings, Lis
Code = "IL2070",
Messages = new List<string>
{
"Microsoft.Maui.Controls.Xaml.ApplyPropertiesVisitor.GetBindableProperty(Type,String,IXmlLineInfo,Boolean): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicFields', 'DynamicallyAccessedMemberTypes.NonPublicFields' in call to 'System.Type.GetFields(BindingFlags)'. The parameter '#0' of method 'Microsoft.Maui.Controls.Xaml.ApplyPropertiesVisitor.GetBindableProperty(Type,String,IXmlLineInfo,Boolean)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.",
"Microsoft.Maui.Controls.Xaml.ApplyPropertiesVisitor.GetBindableProperty(Type,String,IXmlLineInfo): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicFields', 'DynamicallyAccessedMemberTypes.NonPublicFields' in call to 'System.Type.GetField(String,BindingFlags)'. The parameter '#0' of method 'Microsoft.Maui.Controls.Xaml.ApplyPropertiesVisitor.GetBindableProperty(Type,String,IXmlLineInfo)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.",
"Microsoft.Maui.Controls.Xaml.ApplyPropertiesVisitor.GetAllRuntimeMethods(Type): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter '#0' of method 'Microsoft.Maui.Controls.Xaml.ApplyPropertiesVisitor.GetAllRuntimeMethods(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.",
}
},
Expand Down

0 comments on commit a7434fc

Please sign in to comment.