From a7e102676f5854274f246ee82e78b3a3d7f80a30 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 25 Apr 2024 04:51:33 +0200 Subject: [PATCH] Reduce allocations while loading deferred resources (#15070) * Reduce allocations in XamlIlRuntimeHelpers * Iterate XAML parents without allocations * Use IDeferredContent for XAML deferred content to reduce allocations * Use function pointer in DeferredTransformationFactory * Reuse parent resource nodes if possible for deferred content * Fix function pointer usage with SRE --- .../Controls/IDeferredContent.cs | 16 + .../Controls/ResourceDictionary.cs | 32 +- .../AvaloniaXamlIlLanguage.cs | 69 +++- .../AvaloniaXamlIlWellKnownTypes.cs | 4 +- .../Avalonia.Markup.Xaml.csproj | 2 + .../EagerParentStackEnumerator.cs | 47 +++ src/Markup/Avalonia.Markup.Xaml/Extensions.cs | 47 ++- .../DynamicResourceExtension.cs | 4 +- .../StaticResourceExtension.cs | 63 +++- .../Templates/TemplateContent.cs | 30 +- .../IAvaloniaXamlIlParentStackProvider.cs | 7 + .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 325 +++++++++++++++--- .../Xaml/StyleIncludeTests.cs | 7 +- 13 files changed, 538 insertions(+), 115 deletions(-) create mode 100644 src/Avalonia.Base/Controls/IDeferredContent.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/EagerParentStackEnumerator.cs diff --git a/src/Avalonia.Base/Controls/IDeferredContent.cs b/src/Avalonia.Base/Controls/IDeferredContent.cs new file mode 100644 index 0000000000..23901e098f --- /dev/null +++ b/src/Avalonia.Base/Controls/IDeferredContent.cs @@ -0,0 +1,16 @@ +using System; + +namespace Avalonia.Controls; + +/// +/// Represents a deferred content. +/// +public interface IDeferredContent +{ + /// + /// Builds the deferred content using the specified service provider. + /// + /// The service provider to use. + /// The built content. + object? Build(IServiceProvider? serviceProvider); +} diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 83264a477d..70c3e884e8 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -1,12 +1,9 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Templates; -using Avalonia.Media; using Avalonia.Styling; namespace Avalonia.Controls @@ -16,7 +13,7 @@ namespace Avalonia.Controls /// public class ResourceDictionary : ResourceProvider, IResourceDictionary, IThemeVariantProvider { - private object? lastDeferredItemKey; + private object? _lastDeferredItemKey; private Dictionary? _inner; private AvaloniaList? _mergedDictionaries; private AvaloniaDictionary? _themeDictionary; @@ -145,10 +142,10 @@ namespace Avalonia.Controls } public void AddDeferred(object key, Func factory) - { - Inner.Add(key, new DeferredItem(factory)); - Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); - } + => Add(key, new DeferredItem(factory)); + + public void AddDeferred(object key, IDeferredContent deferredContent) + => Add(key, deferredContent); public void Clear() { @@ -227,10 +224,10 @@ namespace Avalonia.Controls { if (_inner is not null && _inner.TryGetValue(key, out value)) { - if (value is DeferredItem deffered) + if (value is IDeferredContent deferred) { // Avoid simple reentrancy, which could commonly occur on redefining the resource. - if (lastDeferredItemKey == key) + if (_lastDeferredItemKey == key) { value = null; return false; @@ -238,8 +235,8 @@ namespace Avalonia.Controls try { - lastDeferredItemKey = key; - _inner[key] = value = deffered.Factory(null) switch + _lastDeferredItemKey = key; + _inner[key] = value = deferred.Build(null) switch { ITemplateResult t => t.Result, { } v => v, @@ -248,7 +245,7 @@ namespace Avalonia.Controls } finally { - lastDeferredItemKey = null; + _lastDeferredItemKey = null; } } return true; @@ -313,7 +310,7 @@ namespace Avalonia.Controls { if (_inner is not null && _inner.TryGetValue(key, out var result)) { - return result is DeferredItem; + return result is IDeferredContent; } return false; @@ -373,10 +370,11 @@ namespace Avalonia.Controls } } - private class DeferredItem + private sealed class DeferredItem : IDeferredContent { - public DeferredItem(Func factory) => Factory = factory; - public Func Factory { get; } + private readonly Func _factory; + public DeferredItem(Func factory) => _factory = factory; + public object? Build(IServiceProvider? serviceProvider) => _factory(serviceProvider); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 947714d4b0..22ac094114 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -1,13 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; -using Avalonia.Controls; -using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.AstNodes; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; -using Avalonia.Media; -using XamlX; using XamlX.Ast; using XamlX.Emit; using XamlX.IL; @@ -64,7 +58,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions "TemplateResultType" }, DeferredContentExecutorCustomization = - runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV2"), + runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV3"), UsableDuringInitializationAttributes = { typeSystem.GetType("Avalonia.Metadata.UsableDuringInitializationAttribute"), @@ -79,18 +73,22 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var emit = new XamlLanguageEmitMappings { ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.EmitProvideValueTarget, - ContextTypeBuilderCallback = definition => EmitNameScopeField(rv, typeSystem, definition) + ContextTypeBuilderCallback = definition => + { + EmitNameScopeField(rv, typeSystem, definition); + EmitEagerParentStackProvider(rv, typeSystem, definition); + } }; return (rv, emit); } public const string ContextNameScopeFieldName = "AvaloniaNameScope"; - private static void EmitNameScopeField(XamlLanguageTypeMappings mappings, + private static void EmitNameScopeField( + XamlLanguageTypeMappings mappings, IXamlTypeSystem typeSystem, IXamlILContextDefinition definition) { - var nameScopeType = typeSystem.FindType("Avalonia.Controls.INameScope"); var field = definition.TypeBuilder.DefineField(nameScopeType, ContextNameScopeFieldName, XamlVisibility.Public, false); @@ -102,7 +100,56 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions typeSystem.FindType("System.Object"), typeSystem.FindType("System.Type")))) .Stfld(field); } - + + private static void EmitEagerParentStackProvider( + XamlLanguageTypeMappings mappings, + IXamlTypeSystem typeSystem, + IXamlILContextDefinition definition) + { + var interfaceType = typeSystem.FindType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlEagerParentStackProvider"); + + definition.TypeBuilder.AddInterfaceImplementation(interfaceType); + + // IReadOnlyList DirectParents => (IReadOnlyList)ParentsStack; + var directParentsGetter = ImplementInterfacePropertyGetter("DirectParents"); + directParentsGetter.Generator + .LdThisFld(definition.ParentListField) + .Castclass(directParentsGetter.ReturnType) + .Ret(); + + var serviceProviderGetServiceMethod = mappings.ServiceProvider.GetMethod(new FindMethodMethodSignature( + "GetService", + typeSystem.FindType("System.Object"), + typeSystem.FindType("System.Type"))); + + // IAvaloniaXamlIlEagerParentStackProvider? ParentProvider + // => (IAvaloniaXamlIlEagerParentStackProvider)_serviceProvider.GetService(typeof(IAvaloniaXamlIlParentStackProvider)) + var parentProviderGetter = ImplementInterfacePropertyGetter("ParentProvider"); + parentProviderGetter.Generator + .LdThisFld(definition.ParentServiceProviderField) + .Ldtype(mappings.ParentStackProvider) + .EmitCall(serviceProviderGetServiceMethod) + .Castclass(interfaceType) + .Ret(); + + IXamlMethodBuilder ImplementInterfacePropertyGetter(string propertyName) + { + var interfaceGetter = interfaceType.FindMethod(m => m.Name == "get_" + propertyName); + + var getter = definition.TypeBuilder.DefineMethod( + interfaceGetter.ReturnType, + Array.Empty(), + "get_" + propertyName, + XamlVisibility.Private, + false, + true, + interfaceGetter); + + definition.TypeBuilder.DefineProperty(interfaceGetter.ReturnType, propertyName, null, getter); + + return getter; + } + } class AttributeResolver : IXamlCustomAttributeResolver { 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 89d2518e2d..3250c7b836 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -261,9 +261,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, - cfg.TypeSystem.GetType("System.Func`2").MakeGenericType( - cfg.TypeSystem.GetType("System.IServiceProvider"), - XamlIlTypes.Object)); + cfg.TypeSystem.GetType("Avalonia.Controls.IDeferredContent")); ResourceDictionaryEnsureCapacity = ResourceDictionary.FindMethod("EnsureCapacity", XamlIlTypes.Void, true, XamlIlTypes.Int32); ResourceDictionaryGetCount = ResourceDictionary.FindMethod("get_Count", XamlIlTypes.Int32, true); IThemeVariantProvider = cfg.TypeSystem.GetType("Avalonia.Controls.IThemeVariantProvider"); diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 299aac61ee..28911562e6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -7,6 +7,7 @@ False false $(NoWarn);CS1591 + true @@ -15,6 +16,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/EagerParentStackEnumerator.cs b/src/Markup/Avalonia.Markup.Xaml/EagerParentStackEnumerator.cs new file mode 100644 index 0000000000..359cb901ed --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/EagerParentStackEnumerator.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Avalonia.Markup.Xaml.XamlIl.Runtime; + +namespace Avalonia.Markup.Xaml; + +internal struct EagerParentStackEnumerator +{ + private IAvaloniaXamlIlEagerParentStackProvider? _provider; + private IReadOnlyList? _currentParents; + private int _currentIndex; // only valid when _currentParents isn't null + + public EagerParentStackEnumerator(IAvaloniaXamlIlEagerParentStackProvider? provider) + => _provider = provider; + + public object? TryGetNext() + { + while (_provider is not null) + { + if (_currentParents is null) + { + _currentParents = _provider.DirectParents; + _currentIndex = _currentParents.Count; + } + + --_currentIndex; + + if (_currentIndex >= 0) + return _currentParents[_currentIndex]; + + _currentParents = null; + _provider = _provider.ParentProvider; + } + + return null; + } + + public T? TryGetNextOfType() where T : class + { + while (TryGetNext() is { } parent) + { + if (parent is T typedParent) + return typedParent; + } + + return null; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs index 8d226189dd..f428fadb08 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs @@ -18,13 +18,52 @@ namespace Avalonia.Markup.Xaml public static Uri? GetContextBaseUri(this IServiceProvider ctx) => ctx.GetService()?.BaseUri; public static T? GetFirstParent(this IServiceProvider ctx) where T : class - => ctx.GetService()?.Parents.OfType().FirstOrDefault(); + => ctx.GetService() switch + { + null => null, + IAvaloniaXamlIlEagerParentStackProvider eager => new EagerParentStackEnumerator(eager).TryGetNextOfType(), + { } provider => provider.Parents.OfType().FirstOrDefault() + }; public static T? GetLastParent(this IServiceProvider ctx) where T : class - => ctx.GetService()?.Parents.OfType().LastOrDefault(); + { + switch (ctx.GetService()) + { + case null: + return null; + + case IAvaloniaXamlIlEagerParentStackProvider eager: + var enumerator = new EagerParentStackEnumerator(eager); + T? lastTypedParent = null; + + while (enumerator.TryGetNextOfType() is { } typedParent) + lastTypedParent = typedParent; + + return lastTypedParent; + + case { } provider: + return provider.Parents.OfType().LastOrDefault(); + } + } + + public static IEnumerable GetParents(this IServiceProvider sp) where T : class + { + return sp.GetService() switch + { + null => Enumerable.Empty(), + IAvaloniaXamlIlEagerParentStackProvider eager => EnumerateEagerProvider(eager), + { } provider => provider.Parents.OfType() + }; - public static IEnumerable GetParents(this IServiceProvider sp) - => sp.GetService()?.Parents.OfType() ?? Enumerable.Empty(); + // allocates the final iterator, but still avoids the intermediate ones + static IEnumerable EnumerateEagerProvider(IAvaloniaXamlIlEagerParentStackProvider eagerProvider) + { + var enumerator = new EagerParentStackEnumerator(eagerProvider); + + while (enumerator.TryGetNextOfType() is { } typedParent) + yield return typedParent; + } + } public static bool IsInControlTemplate(this IServiceProvider sp) => sp.GetService() != null; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 0167b3b89a..7737aa19bf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Markup.Xaml.XamlIl.Runtime; using Avalonia.Styling; namespace Avalonia.Markup.Xaml.MarkupExtensions @@ -37,7 +38,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions (object?)serviceProvider.GetFirstParent(); } - _themeVariant = StaticResourceExtension.GetDictionaryVariant(serviceProvider); + _themeVariant = StaticResourceExtension.GetDictionaryVariant( + serviceProvider.GetService()); return this; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index acf1dc1143..6b048ef0d6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -40,7 +40,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions }; var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant - ?? GetDictionaryVariant(serviceProvider); + ?? GetDictionaryVariant(stack); var targetType = targetProperty switch { @@ -58,11 +58,26 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // which might be able to give us the resource. if (stack is not null) { - foreach (var parent in stack.Parents) + // avoid allocations iterating the parents when possible + if (stack is IAvaloniaXamlIlEagerParentStackProvider eagerStack) { - if (parent is IResourceNode node && node.TryGetResource(resourceKey, themeVariant, out var value)) + var enumerator = new EagerParentStackEnumerator(eagerStack); + while (enumerator.TryGetNextOfType() is { } node) { - return ColorToBrushConverter.Convert(value, targetType); + if (node.TryGetResource(resourceKey, themeVariant, out var value)) + { + return ColorToBrushConverter.Convert(value, targetType); + } + } + } + else + { + foreach (var parent in stack.Parents) + { + if (parent is IResourceNode node && node.TryGetResource(resourceKey, themeVariant, out var value)) + { + return ColorToBrushConverter.Convert(value, targetType); + } } } } @@ -86,23 +101,37 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions return ColorToBrushConverter.Convert(control.FindResource(ResourceKey!), targetType); } - internal static ThemeVariant? GetDictionaryVariant(IServiceProvider serviceProvider) + internal static ThemeVariant? GetDictionaryVariant(IAvaloniaXamlIlParentStackProvider? stack) { - var parents = serviceProvider.GetService()?.Parents; - if (parents is null) + switch (stack) { - return null; - } + case null: + return null; - foreach (var parent in parents) - { - if (parent is IThemeVariantProvider { Key: { } setKey }) - { - return setKey; - } - } + case IAvaloniaXamlIlEagerParentStackProvider eager: + var enumerator = new EagerParentStackEnumerator(eager); - return null; + while (enumerator.TryGetNextOfType() is { } themeVariantProvider) + { + if (themeVariantProvider.Key is { } setKey) + { + return setKey; + } + } + + return null; + + case { } provider: + foreach (var parent in provider.Parents) + { + if (parent is IThemeVariantProvider { Key: { } setKey }) + { + return setKey; + } + } + + return null; + } } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 504478f9b3..582a7b86b7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -7,29 +7,15 @@ namespace Avalonia.Markup.Xaml.Templates public static class TemplateContent { public static TemplateResult? Load(object? templateContent) - { - if (templateContent is Func direct) - { - return (TemplateResult?)direct(null); - } - - if (templateContent is null) - { - return null; - } - - throw new ArgumentException($"Unexpected content {templateContent.GetType()}", nameof(templateContent)); - } + => Load(templateContent); public static TemplateResult? Load(object? templateContent) - { - if (templateContent is Func direct) - return (TemplateResult?)direct(null); - - if (templateContent is null) - return null; - - throw new ArgumentException($"Unexpected content {templateContent.GetType()}", nameof(templateContent)); - } + => templateContent switch + { + IDeferredContent deferred => (TemplateResult?)deferred.Build(null), + Func deferred => (TemplateResult?)deferred(null), + null => null, + _ => throw new ArgumentException($"Unexpected content {templateContent.GetType()}", nameof(templateContent)) + }; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlParentStackProvider.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlParentStackProvider.cs index 16475ee77a..eef51a7f50 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlParentStackProvider.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlParentStackProvider.cs @@ -6,4 +6,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { IEnumerable Parents { get; } } + + public interface IAvaloniaXamlIlEagerParentStackProvider : IAvaloniaXamlIlParentStackProvider + { + IReadOnlyList DirectParents { get; } + + IAvaloniaXamlIlEagerParentStackProvider? ParentProvider { get; } + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index be6b37bb02..6e48ef2508 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -1,8 +1,11 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -15,6 +18,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { public static class XamlIlRuntimeHelpers { + [ThreadStatic] private static List? s_resourceNodeBuffer; + [ThreadStatic] private static LastParentStack? s_lastParentStack; + public static Func DeferredTransformationFactoryV1(Func builder, IServiceProvider provider) { @@ -24,36 +30,193 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime public static Func DeferredTransformationFactoryV2(Func builder, IServiceProvider provider) { - var resourceNodes = provider.GetRequiredService().Parents - .OfType().ToList(); + var resourceNodes = AsResourceNodes(provider.GetRequiredService()); + var rootObject = provider.GetRequiredService().RootObject; + var parentScope = provider.GetService(); + + return new DelegateDeferredContent(resourceNodes, rootObject, parentScope, builder).Build; + } + + // The builder is typed as IntPtr instead of delegate* because Reflection.Emit has + // trouble with generic methods containing function pointers. See https://github.com/dotnet/runtime/issues/100020 + public static unsafe IDeferredContent DeferredTransformationFactoryV3( + /*delegate**/ IntPtr builder, + IServiceProvider provider) + { + var resourceNodes = AsResourceNodes(provider.GetRequiredService()); var rootObject = provider.GetRequiredService().RootObject; var parentScope = provider.GetService(); - return sp => + var typedBuilder = (delegate*)builder; + + return new PointerDeferredContent(resourceNodes, rootObject, parentScope, typedBuilder); + } + + private static IResourceNode[] AsResourceNodes(IAvaloniaXamlIlParentStackProvider provider) + { + var buffer = s_resourceNodeBuffer ??= new List(8); + buffer.Clear(); + + if (provider is IAvaloniaXamlIlEagerParentStackProvider eagerProvider) { - var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope(); - var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); - scope.Complete(); + var enumerator = new EagerParentStackEnumerator(eagerProvider); + + while (enumerator.TryGetNextOfType() is { } node) + buffer.Add(node); + } + else + { + foreach (var item in provider.Parents) + { + if (item is IResourceNode node) + buffer.Add(node); + } + } + + var lastParentStack = s_lastParentStack; + + if (lastParentStack is null + || !lastParentStack.IsEquivalentTo(provider, buffer, out var resourceNodes)) + { + resourceNodes = buffer.ToArray(); + + if (lastParentStack is null) + { + lastParentStack = new LastParentStack(); + s_lastParentStack = lastParentStack; + } + + lastParentStack.Set(provider, resourceNodes); + } - if(typeof(T) == typeof(Control)) - return new TemplateResult((Control)obj, scope); + buffer.Clear(); + return resourceNodes; + } + + // Parent resource nodes are often the same (e.g. most values in a ResourceDictionary), cache the last ones. + private sealed class LastParentStack + { + private readonly WeakReference _parentStackProvider = new(null); + private readonly WeakReference _resourceNodes = new(null); + + public void Set(IAvaloniaXamlIlParentStackProvider parentStackProvider, IResourceNode[] resourceNodes) + { + _parentStackProvider.SetTarget(parentStackProvider); + _resourceNodes.SetTarget(resourceNodes); + } + + public bool IsEquivalentTo( + IAvaloniaXamlIlParentStackProvider parentStackProvider, + List resourceNodes, + [NotNullWhen(true)] out IResourceNode[]? cachedResourceNodes) + { + if (!_parentStackProvider.TryGetTarget(out var lastParentStackProvider) + || !_resourceNodes.TryGetTarget(out var lastResourceNodes) + || parentStackProvider != lastParentStackProvider + || resourceNodes.Count != lastResourceNodes.Length) + { + cachedResourceNodes = null; + return false; + } + +#if NET6_0_OR_GREATER + if (!CollectionsMarshal.AsSpan(resourceNodes).SequenceEqual(lastResourceNodes)) + { + cachedResourceNodes = null; + return false; + } +#else + for (var i = 0; i < lastResourceNodes.Length; ++i) + { + if (lastResourceNodes[i] != resourceNodes[i]) + { + cachedResourceNodes = null; + return false; + } + } +#endif + + cachedResourceNodes = lastResourceNodes; + return true; + } + } + + private abstract class DeferredContent : IDeferredContent + { + private readonly INameScope? _parentNameScope; + private readonly object _rootObject; + private readonly IResourceNode[] _parentResourceNodes; + + protected DeferredContent( + IResourceNode[] parentResourceNodes, + object rootObject, + INameScope? parentNameScope) + { + _parentNameScope = parentNameScope; + _parentResourceNodes = parentResourceNodes; + _rootObject = rootObject; + } + + public object Build(IServiceProvider? serviceProvider) + { + INameScope scope = _parentNameScope is null ? new NameScope() : new ChildNameScope(_parentNameScope); + var obj = InvokeBuilder(new DeferredParentServiceProvider(serviceProvider, _parentResourceNodes, _rootObject, scope)); + scope.Complete(); return new TemplateResult((T)obj, scope); - }; + } + + protected abstract object InvokeBuilder(IServiceProvider serviceProvider); + } + + private sealed unsafe class PointerDeferredContent : DeferredContent + { + private readonly delegate* _builder; + + public PointerDeferredContent( + IResourceNode[] parentResourceNodes, + object rootObject, + INameScope? parentNameScope, + delegate* builder) + : base(parentResourceNodes, rootObject, parentNameScope) + => _builder = builder; + + protected override object InvokeBuilder(IServiceProvider serviceProvider) + => _builder(serviceProvider); } - private class DeferredParentServiceProvider : - IAvaloniaXamlIlParentStackProvider, + private sealed class DelegateDeferredContent : DeferredContent + { + private readonly Func _builder; + + public DelegateDeferredContent( + IResourceNode[] parentResourceNodes, + object rootObject, + INameScope? parentNameScope, + Func builder) + : base(parentResourceNodes, rootObject, parentNameScope) + => _builder = builder; + + protected override object InvokeBuilder(IServiceProvider serviceProvider) + => _builder(serviceProvider); + } + + private sealed class DeferredParentServiceProvider : + IAvaloniaXamlIlEagerParentStackProvider, IServiceProvider, IRootObjectProvider, IAvaloniaXamlIlControlTemplateProvider { private readonly IServiceProvider? _parentProvider; - private readonly List? _parentResourceNodes; + private readonly IResourceNode[] _parentResourceNodes; private readonly INameScope _nameScope; private IRuntimePlatform? _runtimePlatform; + private Optional _parentStackProvider; - public DeferredParentServiceProvider(IServiceProvider? parentProvider, List? parentResourceNodes, - object rootObject, INameScope nameScope) + public DeferredParentServiceProvider( + IServiceProvider? parentProvider, + IResourceNode[] parentResourceNodes, + object rootObject, + INameScope nameScope) { _parentProvider = parentProvider; _parentResourceNodes = parentResourceNodes; @@ -61,16 +224,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime RootObject = rootObject; } - public IEnumerable Parents => GetParents(); + public object RootObject { get; } + + public object IntermediateRootObject + => RootObject; - IEnumerable GetParents() + public IEnumerable Parents + => _parentResourceNodes; + + public IReadOnlyList DirectParents + => _parentResourceNodes; + + public IAvaloniaXamlIlEagerParentStackProvider? ParentProvider { - if(_parentResourceNodes == null) - yield break; - foreach (var p in _parentResourceNodes) - yield return p; + get + { + if (!_parentStackProvider.HasValue) + { + _parentStackProvider = + new Optional(GetParentStackProviderFromServices()); + } + + return _parentStackProvider.GetValueOrDefault(); + } } + private IAvaloniaXamlIlEagerParentStackProvider? GetParentStackProviderFromServices() + => _parentProvider?.GetService() + as IAvaloniaXamlIlEagerParentStackProvider; + public object? GetService(Type serviceType) { if (serviceType == typeof(INameScope)) @@ -82,19 +264,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime if (serviceType == typeof(IAvaloniaXamlIlControlTemplateProvider)) return this; if (serviceType == typeof(IRuntimePlatform)) - { - if(_runtimePlatform == null) - _runtimePlatform = AvaloniaLocator.Current.GetService(); - return _runtimePlatform; - } + return _runtimePlatform ??= AvaloniaLocator.Current.GetService(); return _parentProvider?.GetService(serviceType); } - - public object RootObject { get; } - public object IntermediateRootObject => RootObject; } - public static void ApplyNonMatchingMarkupExtensionV1(object target, object property, IServiceProvider prov, object value) { @@ -119,7 +293,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime public static IServiceProvider CreateInnerServiceProviderV1(IServiceProvider compiled) => new InnerServiceProvider(compiled); - private class InnerServiceProvider : IServiceProvider + private sealed class InnerServiceProvider : IServiceProvider { private readonly IServiceProvider _compiledProvider; private XamlTypeResolver? _resolver; @@ -137,7 +311,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime } } - private class XamlTypeResolver : IXamlTypeResolver + private sealed class XamlTypeResolver : IXamlTypeResolver { private readonly IAvaloniaXamlIlXmlNamespaceInfoProvider _nsInfo; @@ -182,11 +356,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime } #line default - private class RootServiceProvider : IServiceProvider + private sealed class RootServiceProvider : IServiceProvider { private readonly INameScope _nameScope; private readonly IServiceProvider? _parentServiceProvider; private readonly IRuntimePlatform? _runtimePlatform; + private IAvaloniaXamlIlParentStackProvider? _parentStackProvider; public RootServiceProvider(INameScope nameScope, IServiceProvider? parentServiceProvider) { @@ -200,26 +375,98 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime if (serviceType == typeof(INameScope)) return _nameScope; if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) - return _parentServiceProvider?.GetService() - ?? DefaultAvaloniaXamlIlParentStackProvider.Instance; + return _parentStackProvider ??= CreateParentStackProvider(); if (serviceType == typeof(IRuntimePlatform)) return _runtimePlatform ?? throw new KeyNotFoundException($"{nameof(IRuntimePlatform)} was not registered"); return null; } - private class DefaultAvaloniaXamlIlParentStackProvider : IAvaloniaXamlIlParentStackProvider + private IAvaloniaXamlIlParentStackProvider CreateParentStackProvider() + => _parentServiceProvider?.GetService() + ?? GetParentStackProviderForApplication(Application.Current); + + private static IAvaloniaXamlIlEagerParentStackProvider GetParentStackProviderForApplication(Application? application) + => application is null ? + EmptyAvaloniaXamlIlParentStackProvider.Instance : + ApplicationAvaloniaXamlIlParentStackProvider.GetForApplication(application); + + private sealed class ApplicationAvaloniaXamlIlParentStackProvider : + IAvaloniaXamlIlEagerParentStackProvider, + IReadOnlyList { - public static DefaultAvaloniaXamlIlParentStackProvider Instance { get; } = new(); - + private static ApplicationAvaloniaXamlIlParentStackProvider? s_lastProvider; + private static Application? s_lastApplication; + + public static ApplicationAvaloniaXamlIlParentStackProvider GetForApplication(Application application) + { + if (application != s_lastApplication) + { + s_lastProvider = new ApplicationAvaloniaXamlIlParentStackProvider(application); + s_lastApplication = application; + } + + return s_lastProvider!; + } + + private readonly Application _application; + private IEnumerable? _parents; + + public ApplicationAvaloniaXamlIlParentStackProvider(Application application) + => _application = application; + public IEnumerable Parents + => _parents ??= new object[] { _application }; + + public IReadOnlyList DirectParents + => this; + + public int Count + => 1; + + public IEnumerator GetEnumerator() + => Parents.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public object this[int index] { get { - if (Application.Current != null) - yield return Application.Current; + if (index != 0) + ThrowArgumentOutOfRangeException(); + + return _application; } } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + [SuppressMessage("ReSharper", "NotResolvedInText")] + private static void ThrowArgumentOutOfRangeException() + => throw new ArgumentOutOfRangeException("index"); + + public IAvaloniaXamlIlEagerParentStackProvider? ParentProvider + => null; + } + + private sealed class EmptyAvaloniaXamlIlParentStackProvider : IAvaloniaXamlIlEagerParentStackProvider + { + public static EmptyAvaloniaXamlIlParentStackProvider Instance { get; } = new(); + + private EmptyAvaloniaXamlIlParentStackProvider() + { + } + + public IEnumerable Parents + => Array.Empty(); + + public IReadOnlyList DirectParents + => Array.Empty(); + + public IAvaloniaXamlIlEagerParentStackProvider? ParentProvider + => null; } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs index 374c5d1544..74e49b801d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs @@ -326,7 +326,10 @@ public class StyleIncludeTests } } -public class TestServiceProvider : IServiceProvider, IUriContext, IAvaloniaXamlIlParentStackProvider +public class TestServiceProvider : + IServiceProvider, + IUriContext, + IAvaloniaXamlIlEagerParentStackProvider { private IServiceProvider _root = XamlIlRuntimeHelpers.CreateRootServiceProviderV2(); public object GetService(Type serviceType) @@ -345,4 +348,6 @@ public class TestServiceProvider : IServiceProvider, IUriContext, IAvaloniaXamlI public Uri BaseUri { get; set; } public List Parents { get; set; } = new List { new ContentControl() }; IEnumerable IAvaloniaXamlIlParentStackProvider.Parents => Parents; + public IReadOnlyList DirectParents => Parents; + public IAvaloniaXamlIlEagerParentStackProvider ParentProvider => null; }