Skip to content

Commit

Permalink
perf: Make VisualState and VisualTransition lazy loaded
Browse files Browse the repository at this point in the history
Setting `UnoXamlLazyVisualStateManagerEnabled` to false as an msbuild property disables the feature.
  • Loading branch information
jeromelaban committed May 6, 2021
1 parent d5b622a commit 64a79fc
Show file tree
Hide file tree
Showing 12 changed files with 540 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ internal partial class XamlCodeGeneration
/// </summary>
private readonly bool _shouldAnnotateGeneratedXaml = false;

/// <summary>
/// When set, Visual State Manager children will be initialized lazily for performance
/// </summary>
private readonly bool _isLazyVisualStateManagerEnabled = true;

private static DateTime _buildTasksBuildDate = File.GetLastWriteTime(new Uri(typeof(XamlFileGenerator).Assembly.Location).LocalPath);
private INamedTypeSymbol[]? _ambientGlobalResources;
private readonly bool _isUiAutomationMappingEnabled;
Expand Down Expand Up @@ -157,6 +162,11 @@ public XamlCodeGeneration(GeneratorExecutionContext context)
_shouldAnnotateGeneratedXaml = shouldAnnotateGeneratedXaml;
}

if (bool.TryParse(context.GetMSBuildPropertyValue("UnoXamlLazyVisualStateManagerEnabled"), out var isLazyVisualStateManagerEnabled))
{
_isLazyVisualStateManagerEnabled = isLazyVisualStateManagerEnabled;
}

_targetPath = Path.Combine(
_projectDirectory,
context.GetMSBuildPropertyValue("IntermediateOutputPath")
Expand Down Expand Up @@ -286,7 +296,8 @@ public KeyValuePair<string, string>[] Generate()
isDebug: _isDebug,
skipUserControlsInVisualTree: _skipUserControlsInVisualTree,
shouldAnnotateGeneratedXaml: _shouldAnnotateGeneratedXaml,
isUnoAssembly: IsUnoAssembly
isUnoAssembly: IsUnoAssembly,
isLazyVisualStateManagerEnabled: _isLazyVisualStateManagerEnabled
)
.GenerateFile()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ internal partial class XamlFileGenerator
/// </summary>
private bool _isTopLevelDictionary;
private readonly bool _isUnoAssembly;

/// <summary>
/// True if VisualStateManager children can be set lazily
/// </summary>
private readonly bool _isLazyVisualStateManagerEnabled;
/// <summary>
/// True if the generator is currently creating child subclasses (for templates, etc)
/// </summary>
Expand Down Expand Up @@ -191,7 +196,8 @@ public XamlFileGenerator(
bool isDebug,
bool skipUserControlsInVisualTree,
bool shouldAnnotateGeneratedXaml,
bool isUnoAssembly
bool isUnoAssembly,
bool isLazyVisualStateManagerEnabled
)
{
_fileDefinition = file;
Expand All @@ -210,6 +216,7 @@ bool isUnoAssembly
_isDebug = isDebug;
_skipUserControlsInVisualTree = skipUserControlsInVisualTree;
_shouldAnnotateGeneratedXaml = shouldAnnotateGeneratedXaml;
_isLazyVisualStateManagerEnabled = isLazyVisualStateManagerEnabled;

InitCaches();

Expand Down Expand Up @@ -2185,6 +2192,10 @@ private bool BuildProperties(IIndentedStringBuilder writer, XamlObjectDefinition
.InvariantCultureFormat(contentProperty.Name));
}
}
else if (IsLazyVisualStateManagerProperty(contentProperty))
{
writer.AppendLineInvariant($"/* Lazy VisualStateManager property {contentProperty}*/");
}
else // Content is not a collection
{
var objectUid = GetObjectUid(topLevelControl);
Expand Down Expand Up @@ -2765,7 +2776,9 @@ private void BuildExtendedProperties(IIndentedStringBuilder outerwriter, XamlObj
writer.AppendLine(GenerateRootPhases(objectDefinition, closureName) ?? "");
}

foreach (var member in extendedProperties)
var lazyProperties = extendedProperties.Where(IsLazyVisualStateManagerProperty).ToArray();

foreach (var member in extendedProperties.Except(lazyProperties))
{
if (extractionTargetMembers != null)
{
Expand Down Expand Up @@ -3065,6 +3078,35 @@ private void BuildExtendedProperties(IIndentedStringBuilder outerwriter, XamlObj
}
}

var implicitContentChild = FindImplicitContentMember(objectDefinition);
var lazyContentProperty = FindLazyContentProperty(implicitContentChild, objectDefinition);

if (lazyProperties.Any() || lazyContentProperty != null)
{
// This block is used to generate lazy initializations of some
// inner VisualStateManager properties in VisualState and VisualTransition.
// This allows for faster materialization of controls, avoiding the construction
// of inferequently used objects graphs.

using (writer.BlockInvariant($"global::Uno.UI.Helpers.MarkupHelper.Set{objectDefinition.Type.Name}Lazy({closureName}, () => "))
{
BuildLiteralLazyVisualStateManagerProperties(writer, objectDefinition, closureName);

if (implicitContentChild != null && lazyContentProperty != null)
{
writer.AppendLineInvariant($"{closureName}.{lazyContentProperty.Name} = ");

var xamlObjectDefinition = implicitContentChild.Objects.First();
using (TryAdaptNative(writer, xamlObjectDefinition, lazyContentProperty.Type as INamedTypeSymbol))
{
BuildChild(writer, implicitContentChild, xamlObjectDefinition);
}
writer.AppendLineInvariant($";");
}
}
writer.AppendLineInvariant($");");
}

if (HasXBindMarkupExtension(objectDefinition) || HasMarkupExtensionNeedingComponent(objectDefinition))
{
writer.AppendLineInvariant($"/* _isTopLevelDictionary:{_isTopLevelDictionary} */");
Expand Down Expand Up @@ -3134,6 +3176,26 @@ void BuildCustomMarkupExtensionPropertyValue(IIndentedStringBuilder writer, Xaml
}
}

private IPropertySymbol? FindLazyContentProperty(XamlMemberDefinition? implicitContentChild, XamlObjectDefinition objectDefinition)
{
if (implicitContentChild != null)
{
var elementType = FindType(objectDefinition.Type);

if (elementType != null)
{
var contentProperty = FindContentProperty(elementType);

if (contentProperty != null && IsLazyVisualStateManagerProperty(contentProperty))
{
return contentProperty;
}
}
}

return null;
}

private bool IsXLoadMember(XamlMemberDefinition member) =>
member.Member.Name == "Load"
&& member.Member.PreferredXamlNamespace == XamlConstants.XamlXmlNamespace;
Expand Down Expand Up @@ -4611,26 +4673,51 @@ private string RewriteAttachedPropertyPath(string value)

private void BuildLiteralProperties(IIndentedStringBuilder writer, XamlObjectDefinition objectDefinition, string? closureName = null)
{
TryAnnotateWithGeneratorSource(writer);
var closingPunctuation = string.IsNullOrWhiteSpace(closureName) ? "," : ";";
var extendedProperties = GetExtendedProperties(objectDefinition);

bool PropertyFilter(XamlMemberDefinition member)
=> IsType(objectDefinition.Type, member.Member.DeclaringType)
&& !IsAttachedProperty(member)
&& !IsLazyVisualStateManagerProperty(member)
&& FindEventType(member.Member) == null
&& member.Member.Name != "_UnknownContent"; // We are defining the elements of a collection explicitly declared in XAML

BuildLiteralPropertiesWithFilter(writer, objectDefinition, closureName, extendedProperties, PropertyFilter);
}

private void BuildLiteralLazyVisualStateManagerProperties(IIndentedStringBuilder writer, XamlObjectDefinition objectDefinition, string? closureName = null)
{
var extendedProperties = GetExtendedProperties(objectDefinition);

var objectUid = GetObjectUid(objectDefinition);
bool PropertyFilter(XamlMemberDefinition member)
=> IsLazyVisualStateManagerProperty(member);

BuildLiteralPropertiesWithFilter(writer, objectDefinition, closureName, extendedProperties, PropertyFilter);
}

private void BuildLiteralPropertiesWithFilter(
IIndentedStringBuilder writer,
XamlObjectDefinition objectDefinition,
string? closureName,
IEnumerable<XamlMemberDefinition> extendedProperties,
Func<XamlMemberDefinition, bool> propertyPredicate
)
{
if (extendedProperties.Any())
{
TryAnnotateWithGeneratorSource(writer);

var objectUid = GetObjectUid(objectDefinition);
var closingPunctuation = string.IsNullOrWhiteSpace(closureName) ? "," : ";";

foreach (var member in extendedProperties)
{
var fullValueSetter = string.IsNullOrWhiteSpace(closureName) ? member.Member!.Name : "{0}.{1}".InvariantCultureFormat(closureName, member.Member!.Name);

// Exclude attached properties, must be set in the extended apply section.
// If there is no type attached, this can be a binding.
if (IsType(objectDefinition.Type, member.Member.DeclaringType)
&& !IsAttachedProperty(member)
&& FindEventType(member.Member) == null
&& member.Member.Name != "_UnknownContent" // We are defining the elements of a collection explicitly declared in XAML
)
// Exclude lazy initialized properties, those are always in the extended block.
if (propertyPredicate(member))
{
if (member.Objects.None())
{
Expand Down Expand Up @@ -4740,6 +4827,27 @@ private void BuildLiteralProperties(IIndentedStringBuilder writer, XamlObjectDef
}
}

private bool IsLazyVisualStateManagerProperty(XamlMemberDefinition member)
=> _isLazyVisualStateManagerEnabled
&& member.Owner != null
&& member.Owner.Type.Name switch
{
"VisualState" => member.Member.Name == "Storyboard"
|| member.Member.Name == "Setters",
"VisualTransition" => member.Member.Name == "Storyboard",
_ => false,
};

private bool IsLazyVisualStateManagerProperty(IPropertySymbol property)
=> _isLazyVisualStateManagerEnabled
&& property.ContainingSymbol.Name switch
{
"VisualState" => property.Name == "Storyboard"
|| property.Name == "Setters",
"VisualTransition" => property.Name == "Storyboard",
_ => false,
};

/// <summary>
/// Determines if the member is inline initializable and the first item is not a new collection instance
/// </summary>
Expand Down Expand Up @@ -4831,6 +4939,7 @@ private IEnumerable<XamlMemberDefinition> GetExtendedProperties(XamlObjectDefini
m.Member.Name == "Style" && HasBindingMarkupExtension(m)
)
|| IsAttachedProperty(m)
|| IsLazyVisualStateManagerProperty(m)
||
(
// If the object is a collection and it has both _UnknownContent (i.e. an item) and other properties defined,
Expand Down
9 changes: 7 additions & 2 deletions src/Uno.UI.RuntimeTests/Helpers/ThemeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ public static IDisposable UseDarkTheme()
{
var root = Window.Current.Content as FrameworkElement;
Assert.IsNotNull(root);
var currentTheme = root!.RequestedTheme;
var currentTheme = Application.Current.RequestedTheme;
root.RequestedTheme = ElementTheme.Dark;
return new DisposableAction(() => root.RequestedTheme = currentTheme);
Console.WriteLine($"applying currentTheme={currentTheme}");
return new DisposableAction(() =>
{
Console.WriteLine($"restoring currentTheme={currentTheme}");
root.RequestedTheme = currentTheme == ApplicationTheme.Light ? ElementTheme.Light : ElementTheme.Dark;
});
}
}
}
1 change: 1 addition & 0 deletions src/Uno.UI.Tests/Uno.UI.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
<Page Include="Windows_UI_Xaml\**\*.xaml" />
<Page Include="Windows_UI_Xaml_Data\**\*.xaml" />
<Page Include="Windows_UI_Xaml_Controls\**\*.xaml" />
<Page Include="Windows_UI_Xaml_Markup\**\*.xaml" />
<Page Include="App\**\*.xaml" />
<None Remove="Lottie\animation.json" />
<Page Include="..\SamplesApp\UITests.Shared\Windows_UI_Xaml\Resources\Test_Dictionary_Linked.xaml" Link="App/Linked/Test_Dictionary_Linked.xaml" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<UserControl
x:Class="Uno.UI.Tests.Windows_UI_Xaml_Markup.VisualStateManager_Lazy_Tests.Controls.When_VisualStateManager_Lazy"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Uno.UI.Tests.Windows_UI_Xaml_Markup.VisualStateManager_Lazy_Tests.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>

<VisualTransition From="Normal" To="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>

</VisualStateGroup.Transitions>

<VisualState x:Name="Normal">
<Storyboard>
<PointerUpThemeAnimation Storyboard.TargetName="border1" />
</Storyboard>
</VisualState>

<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<PointerUpThemeAnimation Storyboard.TargetName="border1" />
</Storyboard>
</VisualState>

<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<PointerDownThemeAnimation Storyboard.TargetName="border1" />
</Storyboard>
</VisualState>

<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border1" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>

<ContentPresenter x:Name="border1" x:FieldModifier="public" x:Load="false" />

</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236

namespace Uno.UI.Tests.Windows_UI_Xaml_Markup.VisualStateManager_Lazy_Tests.Controls
{
public sealed partial class When_VisualStateManager_Lazy : UserControl
{
public When_VisualStateManager_Lazy()
{
this.InitializeComponent();
}
}
}
Loading

0 comments on commit 64a79fc

Please sign in to comment.