diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 285031e256..83264a477d 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -258,6 +258,24 @@ namespace Avalonia.Controls return false; } + /// + /// Ensures that the resource dictionary can hold up to entries without + /// any further expansion of its backing storage. + /// + /// This method may have no effect when targeting .NET Standard 2.0. + public void EnsureCapacity(int capacity) + { + if (_inner is null) + { + _inner = new(capacity); + return; + } + +#if !NETSTANDARD2_0 + Inner.EnsureCapacity(capacity); +#endif + } + public IEnumerator> GetEnumerator() { return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 71d77b12ed..9d1f439bab 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -88,6 +88,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Transformers.Add(new AvaloniaXamlIlControlTemplatePriorityTransformer()); Transformers.Add(new AvaloniaXamlIlMetadataRemover()); + Transformers.Add(new AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer()); Transformers.Add(new AvaloniaXamlIlRootObjectScope()); Emitters.Add(new AvaloniaNameScopeRegistrationXamlIlNodeEmitter()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs index 61a12aa72d..20123f2e32 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -162,16 +162,27 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer && ThemeVariantNodeEquals(context, key, sameKeyPrevAssignmentNode.Values[0])) { sameKeyPrevValueGroup.Children.AddRange(valueGroup.Children); + FixEnsureCapacityNodes(context, sameKeyPrevValueGroup); children.RemoveAt(i); break; } } } } - + + FixEnsureCapacityNodes(context, resourceDictionaryManipulation); + return node; } + // // Removes all existing EnsureCapacityNode (from the merged dictionaries) and adds a new one. + private static void FixEnsureCapacityNodes(AstGroupTransformationContext context, XamlManipulationGroupNode manipulation) + { + var children = manipulation.Children; + children.RemoveAll(c => c is AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer.EnsureCapacityNode); + new AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer().Apply(context, manipulation); + } + public static bool ThemeVariantNodeEquals(AstGroupTransformationContext context, IXamlAstValueNode left, IXamlAstValueNode right) { if (left is XamlConstantNode leftConst diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer.cs new file mode 100644 index 0000000000..76781046b2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer.cs @@ -0,0 +1,142 @@ +#nullable enable + +using System.Collections.Generic; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +/// +/// Adds a call to EnsureCapacity before adding items to a ResourceDictionary. +/// +internal sealed class AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer : IXamlAstTransformer +{ + private readonly HashSet _processedGroups = new(); + + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlManipulationGroupNode group) + Apply(context, group); + + return node; + } + + public void Apply(AstTransformationContext context, XamlManipulationGroupNode group) + { + var info = GetResourcesInfo(group, context.GetAvaloniaTypes()); + + if (info.Mode != ResourcesMode.None && info.Count >= 2) + group.Children.Insert(0, new EnsureCapacityNode(group, info.Count, info.ResourcesGetter)); + } + + private ResourcesInfo GetResourcesInfo(IXamlAstManipulationNode node, AvaloniaXamlIlWellKnownTypes types) + => node switch + { + XamlPropertyAssignmentNode propertyAssignment => GetResourcesInfo(propertyAssignment, types), + XamlManipulationGroupNode group => GetResourcesInfo(group, types), + _ => default + }; + + private ResourcesInfo GetResourcesInfo(XamlManipulationGroupNode node, AvaloniaXamlIlWellKnownTypes types) + { + if (!_processedGroups.Add(node)) + return default; + + ResourcesInfo groupInfo = default; + + foreach (var child in node.Children) + { + var childInfo = GetResourcesInfo(child, types); + + if (childInfo.Mode == ResourcesMode.None) + continue; + + if (groupInfo.Mode == ResourcesMode.None) + groupInfo = new ResourcesInfo(childInfo.Mode, childInfo.ResourcesGetter); + else if (groupInfo.Mode != childInfo.Mode || groupInfo.ResourcesGetter != childInfo.ResourcesGetter) + return default; + + groupInfo.Count += childInfo.Count; + } + + return groupInfo; + } + + private static ResourcesInfo GetResourcesInfo(XamlPropertyAssignmentNode node, AvaloniaXamlIlWellKnownTypes types) + => node.Property.Name switch + { + "Content" when node.Property.DeclaringType == types.ResourceDictionary + => new ResourcesInfo(ResourcesMode.ResourceDictionaryContent, null) { Count = 1 }, + "Resources" when types.IResourceDictionary.IsAssignableFrom(node.Property.Getter.ReturnType) + => new ResourcesInfo(ResourcesMode.ElementResources, node.Property.Getter) { Count = 1 }, + _ + => default + }; + + private struct ResourcesInfo + { + public ResourcesMode Mode { get; } + public IXamlMethod? ResourcesGetter { get; } + public int Count { get; set; } + + public ResourcesInfo(ResourcesMode mode, IXamlMethod? resourcesGetter) + { + Mode = mode; + ResourcesGetter = resourcesGetter; + } + } + + private enum ResourcesMode + { + None, + ResourceDictionaryContent, + ElementResources + } + + public sealed class EnsureCapacityNode : XamlAstNode, IXamlAstManipulationNode, IXamlAstILEmitableNode + { + private readonly int _capacity; + private readonly IXamlMethod? _resourcesGetter; + + public EnsureCapacityNode(IXamlLineInfo lineInfo, int capacity, IXamlMethod? resourcesGetter) + : base(lineInfo) + { + _capacity = capacity; + _resourcesGetter = resourcesGetter; + } + + public XamlILNodeEmitResult Emit( + XamlEmitContext context, + IXamlILEmitter codeGen) + { + var types = context.GetAvaloniaTypes(); + var exitLabel = codeGen.DefineLabel(); + + using var local = codeGen.LocalsPool.GetLocal(types.ResourceDictionary); + + if (_resourcesGetter is not null) + codeGen.EmitCall(_resourcesGetter); + + codeGen + // if (value is not ResourceDictionary local) goto exit; + .Isinst(types.ResourceDictionary) + .Stloc(local.Local) + .Ldloc(local.Local) + .Brfalse(exitLabel) + // local.EnsureCapacity(local.Count + `_capacity`); + .Ldloc(local.Local) + .Dup() + .EmitCall(types.ResourceDictionaryGetCount) + .Ldc_I4(_capacity) + .Add() + .EmitCall(types.ResourceDictionaryEnsureCapacity) + // exit: + .MarkLabel(exitLabel); + + return XamlILNodeEmitResult.Void(1); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 1b26f6c899..89d2518e2d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -114,6 +114,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } + public IXamlMethod ResourceDictionaryEnsureCapacity { get; } + public IXamlMethod ResourceDictionaryGetCount { get; } public IXamlType IThemeVariantProvider { get; } public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } @@ -262,6 +264,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers cfg.TypeSystem.GetType("System.Func`2").MakeGenericType( cfg.TypeSystem.GetType("System.IServiceProvider"), XamlIlTypes.Object)); + ResourceDictionaryEnsureCapacity = ResourceDictionary.FindMethod("EnsureCapacity", XamlIlTypes.Void, true, XamlIlTypes.Int32); + ResourceDictionaryGetCount = ResourceDictionary.FindMethod("get_Count", XamlIlTypes.Int32, true); IThemeVariantProvider = cfg.TypeSystem.GetType("Avalonia.Controls.IThemeVariantProvider"); UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind });