From 4745066c70675eed6c21b08f4c1fe66ca8d2f215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 20 Sep 2024 18:41:02 +0200 Subject: [PATCH] Refactoring of DotvvmProperty value storage All DotvvmProperties are now assigned 32-bit ids, which can be used for more efficient lookups and identifications. The ID is formatted to allow optimizing certain common operations and make the assignment consistent even when we initialize controls on multiple threads. The ID format is (bit 0 is (id >> 31)&1, bit 31 is id&1) * bit 0 - =1 - is property group * bits 16-30: Identifies the property declaring type. * bits 0-15: Identifies the property in the declaring type. - =0 - any other DotvvmProperty * bits 16-30: Identifies the property group * bits 0-15: Identifies a string - the property group member All IDs are assigned sequentially, with reserved blocks at the start for the most important types which we might want to adress directly in a compile-time constant. IDs put a limit on the number of properties in a type (64k), the number of property groups (32k), and the number of property group members. All property groups share the same name dictionary, which allows for significant memory savings, but it might be limiting in obscure cases. As property groups share the name-ID mapping, we do not to keep the GroupedDotvvmProperty instances in memory after the compilation is done. VirtualPropertyGroupDictionary will map strings directly to the IDs and back. Shorter unmanaged IDs allows for efficient lookups in unorganized arrays using SIMD. 8 property IDs fit into a single vector register. Since, controls with more than 8 properties are not common, we can eliminate hashing with this "brute force". We should evaluate whether it makes sense to keep the custom small table--optimized hashtable. This patch keeps that in place. The standard Dictionary`2 is also faster when indexed with integer compared to a reference type. Number of other places in the framework were adjusted to adress properties directly using the IDs. --- src/Directory.Build.props | 4 +- .../Framework/Binding/ActiveDotvvmProperty.cs | 4 + .../Binding/CompileTimeOnlyDotvvmProperty.cs | 4 +- .../Binding/DelegateActionProperty.cs | 7 +- ...DotvvmCapabilityProperty.CodeGeneration.cs | 38 +- .../DotvvmCapabilityProperty.Helpers.cs | 40 +- .../Binding/DotvvmCapabilityProperty.cs | 20 +- .../Framework/Binding/DotvvmProperty.cs | 113 ++- .../Framework/Binding/DotvvmPropertyAlias.cs | 5 +- .../Binding/DotvvmPropertyIdAssignment.cs | 465 ++++++++++++ .../Binding/DotvvmPropertyWithFallback.cs | 4 +- .../Binding/GroupedDotvvmProperty.cs | 16 +- .../Framework/Binding/ValueOrBinding.cs | 2 + .../Binding/VirtualPropertyGroupDictionary.cs | 242 ++++--- .../Compilation/AttributeValueMergerBase.cs | 10 +- .../ControlTree/DefaultControlResolver.cs | 181 ++++- .../ControlTree/DotvvmPropertyGroup.cs | 34 +- .../Compilation/HtmlAttributeValueMerger.cs | 15 +- .../Compilation/IAttributeValueMerger.cs | 2 +- .../Styles/ResolvedControlHelper.cs | 4 +- .../DefaultViewCompilerCodeEmitter.cs | 109 ++- .../Framework/Controls/CompositeControl.cs | 12 +- .../Controls/DotvvmBindableObject.cs | 56 +- .../Controls/DotvvmBindableObjectHelper.cs | 9 +- .../Framework/Controls/DotvvmControl.cs | 27 +- .../Controls/DotvvmControlProperties.cs | 677 +++++++++++++----- .../DotvvmControlPropertyIdGroupEnumerator.cs | 0 .../Framework/Controls/HtmlGenericControl.cs | 28 +- src/Framework/Framework/Controls/Literal.cs | 8 +- .../Controls/PropertyImmutableHashtable.cs | 440 ++++++++++-- src/Framework/Framework/Controls/RouteLink.cs | 1 + src/Tests/Runtime/DotvvmPropertyTests.cs | 15 + src/Tests/Runtime/PropertyGroupTests.cs | 48 ++ 33 files changed, 2123 insertions(+), 517 deletions(-) create mode 100644 src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs create mode 100644 src/Framework/Framework/Controls/DotvvmControlPropertyIdGroupEnumerator.cs create mode 100644 src/Tests/Runtime/PropertyGroupTests.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cc0bba6d99..5aca179d24 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -25,8 +25,8 @@ $(NoWarn);CS1591;CS1573 true true - netstandard2.1;net6.0;net472 - netstandard2.1;net6.0 + net8.0;net6.0;netstandard2.1;net472 + net8.0;net6.0;netstandard2.1 true false diff --git a/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs b/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs index 831b85d787..7d87b9982e 100644 --- a/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs +++ b/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs @@ -13,6 +13,10 @@ namespace DotVVM.Framework.Binding /// An abstract DotvvmProperty which contains code to be executed when the assigned control is being rendered. public abstract class ActiveDotvvmProperty : DotvvmProperty { + internal ActiveDotvvmProperty(string name, Type declaringType, bool isValueInherited) : base(name, declaringType, isValueInherited) + { + } + public abstract void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context, DotvvmControl control); diff --git a/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs b/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs index b982ee6569..971ca845d8 100644 --- a/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs +++ b/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs @@ -11,7 +11,7 @@ namespace DotVVM.Framework.Binding /// public class CompileTimeOnlyDotvvmProperty : DotvvmProperty { - public CompileTimeOnlyDotvvmProperty() + private CompileTimeOnlyDotvvmProperty(string name, Type declaringType) : base(name, declaringType, isValueInherited: false) { } @@ -37,7 +37,7 @@ public override bool IsSet(DotvvmBindableObject control, bool inherit = true) /// public static CompileTimeOnlyDotvvmProperty Register(string propertyName) { - var property = new CompileTimeOnlyDotvvmProperty(); + var property = new CompileTimeOnlyDotvvmProperty(propertyName, typeof(TDeclaringType)); return (CompileTimeOnlyDotvvmProperty)Register(propertyName, property: property); } } diff --git a/src/Framework/Framework/Binding/DelegateActionProperty.cs b/src/Framework/Framework/Binding/DelegateActionProperty.cs index 81c1e23c86..3bb96daef1 100644 --- a/src/Framework/Framework/Binding/DelegateActionProperty.cs +++ b/src/Framework/Framework/Binding/DelegateActionProperty.cs @@ -13,9 +13,9 @@ namespace DotVVM.Framework.Binding /// DotvvmProperty which calls the function passed in the Register method, when the assigned control is being rendered. public sealed class DelegateActionProperty: ActiveDotvvmProperty { - private Action func; + private readonly Action func; - public DelegateActionProperty(Action func) + public DelegateActionProperty(Action func, string name, Type declaringType, bool isValueInherited) : base(name, declaringType, isValueInherited) { this.func = func; } @@ -27,7 +27,8 @@ public override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestCon public static DelegateActionProperty Register(string name, Action func, [AllowNull] TValue defaultValue = default(TValue)) { - return (DelegateActionProperty)DotvvmProperty.Register(name, defaultValue, false, new DelegateActionProperty(func)); + var property = new DelegateActionProperty(func, name, typeof(TDeclaringType), isValueInherited: false); + return (DelegateActionProperty)DotvvmProperty.Register(name, defaultValue, false, property); } } diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs index 41e03091c4..f18f3d3de4 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs @@ -105,8 +105,10 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyG valueParameter ) ); - } + + static readonly ConstructorInfo DotvvmPropertyIdConstructor = typeof(DotvvmPropertyId).GetConstructor(new [] { typeof(uint) }).NotNull(); + public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyAccessors(Type type, DotvvmProperty property) { if (property is DotvvmPropertyAlias propertyAlias) @@ -123,18 +125,26 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA var valueParameter = Expression.Parameter(type, "value"); var unwrappedType = type.UnwrapNullableType(); + var defaultObj = TypeConversion.BoxToObject(Constant(property.DefaultValue)); + // try to access the readonly static field, as .NET can optimize that better than whatever Linq.Expression Constant compiles to + var propertyExpr = + property.AttributeProvider is FieldInfo field && field.IsStatic && field.IsInitOnly && field.GetValue(null) == property + ? Field(null, field) + : (Expression)Constant(property); + var propertyIdExpr = New(DotvvmPropertyIdConstructor, Constant(property.Id.Id, typeof(uint))); + var boxedValueParameter = TypeConversion.BoxToObject(valueParameter); var setValueRaw = canUseDirectAccess - ? Call(typeof(Helpers), nameof(Helpers.SetValueDirect), Type.EmptyTypes, currentControlParameter, Constant(property), boxedValueParameter) - : Call(currentControlParameter, nameof(DotvvmBindableObject.SetValueRaw), Type.EmptyTypes, Constant(property), boxedValueParameter); + ? Call(typeof(Helpers), nameof(Helpers.SetValueDirect), Type.EmptyTypes, currentControlParameter, propertyIdExpr, defaultObj, boxedValueParameter) + : Call(currentControlParameter, nameof(DotvvmBindableObject.SetValueRaw), Type.EmptyTypes, propertyExpr, boxedValueParameter); if (typeof(IBinding).IsAssignableFrom(type)) { var getValueRaw = canUseDirectAccess - ? Call(typeof(Helpers), nameof(Helpers.GetValueRawDirect), Type.EmptyTypes, currentControlParameter, Constant(property)) - : Call(currentControlParameter, nameof(DotvvmBindableObject.GetValueRaw), Type.EmptyTypes, Constant(property), Constant(property.IsValueInherited)); + ? Call(typeof(Helpers), nameof(Helpers.GetValueRawDirect), Type.EmptyTypes, currentControlParameter, propertyIdExpr, defaultObj) + : Call(currentControlParameter, nameof(DotvvmBindableObject.GetValueRaw), Type.EmptyTypes, propertyExpr, Constant(property.IsValueInherited)); return ( Lambda( Convert(getValueRaw, type), @@ -173,11 +183,17 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA Expression.Call( getValueOrBindingMethod, currentControlParameter, - Constant(property)), + canUseDirectAccess ? propertyIdExpr : propertyExpr, + defaultObj), currentControlParameter ), Expression.Lambda( - Expression.Call(setValueOrBindingMethod, currentControlParameter, Expression.Constant(property), valueParameter), + Expression.Call( + setValueOrBindingMethod, + currentControlParameter, + canUseDirectAccess ? propertyIdExpr : propertyExpr, + defaultObj, + valueParameter), currentControlParameter, valueParameter ) ); @@ -191,18 +207,18 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA Expression getValue; if (canUseDirectAccess && unwrappedType.IsValueType) { - getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), new Type[] { unwrappedType }, currentControlParameter, Constant(property)); - if (type.IsNullable()) + getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), new Type[] { unwrappedType }, currentControlParameter, propertyIdExpr, Constant(property.DefaultValue, type.MakeNullableType())); + if (!type.IsNullable()) getValue = Expression.Property(getValue, "Value"); } else { - getValue = Call(currentControlParameter, getValueMethod, Constant(property), Constant(property.IsValueInherited)); + getValue = Call(currentControlParameter, getValueMethod, propertyExpr, Constant(property.IsValueInherited)); } return ( Expression.Lambda( Expression.Convert( - Expression.Call(currentControlParameter, getValueMethod, Expression.Constant(property), Expression.Constant(false)), + Expression.Call(currentControlParameter, getValueMethod, propertyExpr, Expression.Constant(false)), type ), currentControlParameter diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs index 12b4253588..4f771e0f3e 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs @@ -12,60 +12,60 @@ public partial class DotvvmCapabilityProperty { internal static class Helpers { - public static ValueOrBinding? GetOptionalValueOrBinding(DotvvmBindableObject c, DotvvmProperty p) + public static ValueOrBinding? GetOptionalValueOrBinding(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue) { if (c.properties.TryGet(p, out var x)) return ValueOrBinding.FromBoxedValue(x); else return null; } - public static ValueOrBinding GetValueOrBinding(DotvvmBindableObject c, DotvvmProperty p) + public static ValueOrBinding GetValueOrBinding(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue) { if (!c.properties.TryGet(p, out var x)) - x = p.DefaultValue; + x = defaultValue; return ValueOrBinding.FromBoxedValue(x); } - public static ValueOrBinding? GetOptionalValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p) + public static ValueOrBinding? GetOptionalValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue) { if (c.IsPropertySet(p)) return ValueOrBinding.FromBoxedValue(c.GetValue(p)); else return null; } - public static ValueOrBinding GetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p) + public static ValueOrBinding GetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue) { return ValueOrBinding.FromBoxedValue(c.GetValue(p)); } - public static void SetOptionalValueOrBinding(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding? val) + public static void SetOptionalValueOrBinding(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue, ValueOrBinding? val) { if (val.HasValue) { - SetValueOrBinding(c, p, val.GetValueOrDefault()); + SetValueOrBinding(c, p, defaultValue, val.GetValueOrDefault()); } else { c.properties.Remove(p); } } - public static void SetValueOrBinding(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding val) + public static void SetValueOrBinding(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue, ValueOrBinding val) { var boxedVal = val.UnwrapToObject(); - SetValueDirect(c, p, boxedVal); + SetValueDirect(c, p, defaultValue, boxedVal); } - public static void SetOptionalValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding? val) + public static void SetOptionalValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue, ValueOrBinding? val) { if (val.HasValue) { - SetValueOrBindingSlow(c, p, val.GetValueOrDefault()); + SetValueOrBindingSlow(c, p, defaultValue, val.GetValueOrDefault()); } else { - c.SetValue(p, p.DefaultValue); // set to default value, just in case this property is backed in a different place than c.properties[p] + c.SetValue(p, defaultValue); // set to default value, just in case this property is backed in a different place than c.properties[p] c.properties.Remove(p); } } - public static void SetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding val) + public static void SetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue, ValueOrBinding val) { var boxedVal = val.UnwrapToObject(); - if (Object.Equals(boxedVal, p.DefaultValue) && c.IsPropertySet(p)) + if (Object.Equals(boxedVal, defaultValue) && c.IsPropertySet(p)) { // setting to default value and the property is not set -> do nothing } @@ -75,15 +75,15 @@ public static void SetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProper } } - public static object? GetValueRawDirect(DotvvmBindableObject c, DotvvmProperty p) + public static object? GetValueRawDirect(DotvvmBindableObject c, DotvvmPropertyId p, object defaultValue) { if (c.properties.TryGet(p, out var x)) { return x; } - else return p.DefaultValue; + else return defaultValue; } - public static T? GetStructValueDirect(DotvvmBindableObject c, DotvvmProperty p) + public static T? GetStructValueDirect(DotvvmBindableObject c, DotvvmPropertyId p, T? defaultValue) where T: struct { if (c.properties.TryGet(p, out var x)) @@ -94,11 +94,11 @@ public static void SetValueOrBindingSlow(DotvvmBindableObject c, DotvvmProper return xValue; return (T?)c.EvalPropertyValue(p, x); } - else return (T?)p.DefaultValue; + else return defaultValue; } - public static void SetValueDirect(DotvvmBindableObject c, DotvvmProperty p, object? value) + public static void SetValueDirect(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue, object? value) { - if (Object.Equals(p.DefaultValue, value) && !c.properties.Contains(p)) + if (Object.Equals(defaultValue, value) && !c.properties.Contains(p)) { // setting to default value and the property is not set -> do nothing } diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs index aa22586320..dedcac62dc 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs @@ -45,13 +45,9 @@ private DotvvmCapabilityProperty( Type declaringType, ICustomAttributeProvider? attributeProvider, DotvvmCapabilityProperty? declaringCapability - ): base() + ): base(name ?? prefix + type.Name, declaringType, isValueInherited: false) { - name ??= prefix + type.Name; - - this.Name = name; this.PropertyType = type; - this.DeclaringType = declaringType; this.Prefix = prefix; this.AddUsedInCapability(declaringCapability); @@ -63,14 +59,15 @@ private DotvvmCapabilityProperty( AssertPropertyNotDefined(this, postContent: false); - var dotnetFieldName = ToPascalCase(name.Replace("-", "_").Replace(":", "_")); + var dotnetFieldName = ToPascalCase(Name.Replace("-", "_").Replace(":", "_")); attributeProvider ??= declaringType.GetProperty(dotnetFieldName) ?? declaringType.GetField(dotnetFieldName) ?? (ICustomAttributeProvider?)declaringType.GetField(dotnetFieldName + "Property") ?? - throw new Exception($"Capability backing field could not be found and capabilityAttributeProvider argument was not provided. Property: {declaringType.Name}.{name}. Please declare a field or property named {dotnetFieldName}."); + throw new Exception($"Capability backing field could not be found and capabilityAttributeProvider argument was not provided. Property: {declaringType.Name}.{Name}. Please declare a field or property named {dotnetFieldName}."); DotvvmProperty.InitializeProperty(this, attributeProvider); + this.MarkupOptions._mappingMode ??= MappingMode.Exclude; } public override object GetValue(DotvvmBindableObject control, bool inherit = true) => Getter(control); @@ -200,15 +197,8 @@ static DotvvmCapabilityProperty RegisterCapability(DotvvmCapabilityProperty prop { var declaringType = property.DeclaringType.NotNull(); var capabilityType = property.PropertyType.NotNull(); - var name = property.Name.NotNull(); AssertPropertyNotDefined(property); - var attributes = new CustomAttributesProvider( - new MarkupOptionsAttribute - { - MappingMode = MappingMode.Exclude - } - ); - DotvvmProperty.Register(name, capabilityType, declaringType, DBNull.Value, false, property, attributes); + DotvvmProperty.Register(property); if (!capabilityRegistry.TryAdd((declaringType, capabilityType, property.Prefix), property)) throw new($"unhandled naming conflict when registering capability {capabilityType}."); capabilityListRegistry.AddOrUpdate( diff --git a/src/Framework/Framework/Binding/DotvvmProperty.cs b/src/Framework/Framework/Binding/DotvvmProperty.cs index 8eb7261768..897549b88f 100644 --- a/src/Framework/Framework/Binding/DotvvmProperty.cs +++ b/src/Framework/Framework/Binding/DotvvmProperty.cs @@ -25,11 +25,13 @@ namespace DotVVM.Framework.Binding [DebuggerDisplay("{FullName}")] public class DotvvmProperty : IPropertyDescriptor { + public DotvvmPropertyId Id { get; } /// /// Gets or sets the name of the property. /// - public string Name { get; protected set; } + public string Name { get; } + [JsonIgnore] ITypeDescriptor IControlAttributeDescriptor.DeclaringType => new ResolvedTypeDescriptor(DeclaringType); @@ -50,7 +52,7 @@ public class DotvvmProperty : IPropertyDescriptor /// /// Gets the type of the class where the property is registered. /// - public Type DeclaringType { get; protected set; } + public Type DeclaringType { get; } /// /// Gets whether the value can be inherited from the parent controls. @@ -61,17 +63,17 @@ public class DotvvmProperty : IPropertyDescriptor /// Gets or sets the Reflection property information. /// [JsonIgnore] - public PropertyInfo? PropertyInfo { get; private set; } + public PropertyInfo? PropertyInfo { get; protected set; } /// /// Provider of custom attributes for this property. /// - internal ICustomAttributeProvider AttributeProvider { get; set; } + internal ICustomAttributeProvider AttributeProvider { get; private protected set; } /// /// Gets or sets the markup options. /// - public MarkupOptionsAttribute MarkupOptions { get; set; } + public MarkupOptionsAttribute MarkupOptions { get; protected set; } /// /// Determines if property type inherits from IBinding @@ -109,6 +111,8 @@ public string FullName IPropertyDescriptor? IControlAttributeDescriptor.OwningCapability => OwningCapability; IEnumerable IControlAttributeDescriptor.UsedInCapabilities => UsedInCapabilities; + private bool initialized = false; + internal void AddUsedInCapability(DotvvmCapabilityProperty? p) { @@ -119,12 +123,24 @@ internal void AddUsedInCapability(DotvvmCapabilityProperty? p) } } - /// - /// Prevents a default instance of the class from being created. - /// #pragma warning disable CS8618 // DotvvmProperty is usually initialized by InitializeProperty - internal DotvvmProperty() + internal DotvvmProperty(string name, Type declaringType, bool isValueInherited) + { + if (name is null) throw new ArgumentNullException(nameof(name)); + if (declaringType is null) throw new ArgumentNullException(nameof(declaringType)); + this.Name = name; + this.DeclaringType = declaringType; + this.IsValueInherited = isValueInherited; + this.Id = DotvvmPropertyIdAssignment.RegisterProperty(this); + } + internal DotvvmProperty(DotvvmPropertyId id, string name, Type declaringType) { + if (name is null) throw new ArgumentNullException(nameof(name)); + if (declaringType is null) throw new ArgumentNullException(nameof(declaringType)); + if (id.Id == 0) throw new ArgumentException("DotvvmProperty must have an ID", nameof(id)); + this.Name = name; + this.DeclaringType = declaringType; + this.Id = id; } internal DotvvmProperty( #pragma warning restore CS8618 @@ -133,7 +149,8 @@ internal DotvvmProperty( Type declaringType, object? defaultValue, bool isValueInherited, - ICustomAttributeProvider attributeProvider + ICustomAttributeProvider attributeProvider, + DotvvmPropertyId id = default ) { this.Name = name ?? throw new ArgumentNullException(nameof(name)); @@ -142,6 +159,9 @@ ICustomAttributeProvider attributeProvider this.DefaultValue = defaultValue; this.IsValueInherited = isValueInherited; this.AttributeProvider = attributeProvider ?? throw new ArgumentNullException(nameof(attributeProvider)); + if (id.Id == 0) + id = DotvvmPropertyIdAssignment.RegisterProperty(this); + this.Id = id; InitializeProperty(this); } @@ -158,6 +178,23 @@ public T[] GetAttributes() return attrA.Concat(attrB).ToArray(); } + public T? GetAttribute() where T: Attribute + { + var t = typeof(T); + var provider = AttributeProvider; + if (provider.IsDefined(t, true)) + { + return (T)provider.GetCustomAttributes(t, true).Single(); + } + var property = PropertyInfo; + if (property is {} && !object.ReferenceEquals(property, provider)) + { + return (T?)property.GetCustomAttribute(t, true); + } + + return null; + } + public bool IsOwnedByCapability(Type capability) => (this is DotvvmCapabilityProperty && this.PropertyType == capability) || OwningCapability?.IsOwnedByCapability(capability) == true; @@ -254,14 +291,32 @@ public virtual void SetValue(DotvvmBindableObject control, object? value) public static DotvvmProperty Register(string propertyName, Type propertyType, Type declaringType, object? defaultValue, bool isValueInherited, DotvvmProperty? property, ICustomAttributeProvider attributeProvider, bool throwOnDuplicateRegistration = true) { - if (property == null) property = new DotvvmProperty(); + if (propertyName is null) throw new ArgumentNullException(nameof(propertyName)); + if (propertyType is null) throw new ArgumentNullException(nameof(propertyType)); + if (declaringType is null) throw new ArgumentNullException(nameof(declaringType)); + if (attributeProvider is null) throw new ArgumentNullException(nameof(attributeProvider)); - property.Name = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); - property.IsValueInherited = isValueInherited; - property.DeclaringType = declaringType ?? throw new ArgumentNullException(nameof(declaringType)); - property.PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - property.DefaultValue = defaultValue; - property.AttributeProvider = attributeProvider ?? throw new ArgumentNullException(nameof(attributeProvider)); + if (property == null) + { + property = new DotvvmProperty(propertyName, propertyType, declaringType, defaultValue, isValueInherited, attributeProvider); + } + else + { + if (!property.initialized) + { + property.PropertyType = propertyType; + property.DefaultValue = defaultValue; + property.IsValueInherited = isValueInherited; + property.AttributeProvider = attributeProvider; + InitializeProperty(property, attributeProvider); + } + if (property.Name != propertyName) throw new ArgumentException("The property name does not match the existing property.", nameof(propertyName)); + if (property.IsValueInherited != isValueInherited) throw new ArgumentException("The IsValueInherited does not match the existing property.", nameof(isValueInherited)); + if (property.DeclaringType != declaringType) throw new ArgumentException("The declaring type does not match the existing property.", nameof(declaringType)); + if (property.PropertyType != propertyType) throw new ArgumentException("The property type does not match the existing property.", nameof(propertyType)); + if (property.DefaultValue != defaultValue) throw new ArgumentException("The default value does not match the existing property.", nameof(defaultValue)); + if (property.AttributeProvider != attributeProvider) throw new ArgumentException("The attribute provider does not match the existing property.", nameof(attributeProvider)); + } return Register(property, throwOnDuplicateRegistration); } @@ -284,7 +339,11 @@ public override string Message { get { internal static DotvvmProperty Register(DotvvmProperty property, bool throwOnDuplicateRegistration = true) { - InitializeProperty(property); + if (property.Id.Id == 0) + throw new Exception("DotvvmProperty must have an ID"); + + if (!property.initialized) + throw new Exception("DotvvmProperty must be initialized before registration."); var key = (property.DeclaringType, property.Name); if (!registeredProperties.TryAdd(key, property)) @@ -369,8 +428,8 @@ public static DotvvmPropertyAlias RegisterAlias( aliasName, declaringType, aliasAttribute.AliasedPropertyName, - aliasAttribute.AliasedPropertyDeclaringType ?? declaringType); - propertyAlias.AttributeProvider = attributeProvider; + aliasAttribute.AliasedPropertyDeclaringType ?? declaringType, + attributeProvider); propertyAlias.ObsoleteAttribute = attributeProvider.GetCustomAttribute(); var key = (propertyAlias.DeclaringType, propertyAlias.Name); @@ -392,6 +451,8 @@ public static DotvvmPropertyAlias RegisterAlias( public static void InitializeProperty(DotvvmProperty property, ICustomAttributeProvider? attributeProvider = null) { + if (property.initialized) + throw new Exception("DotvvmProperty should not be initialized twice."); if (string.IsNullOrWhiteSpace(property.Name)) throw new Exception("DotvvmProperty must not have empty name."); if (property.DeclaringType is null || property.PropertyType is null) @@ -405,24 +466,26 @@ public static void InitializeProperty(DotvvmProperty property, ICustomAttributeP property.PropertyInfo ?? throw new ArgumentNullException(nameof(attributeProvider)); property.MarkupOptions ??= - property.GetAttributes().SingleOrDefault() + property.GetAttribute() ?? new MarkupOptionsAttribute(); if (string.IsNullOrEmpty(property.MarkupOptions.Name)) property.MarkupOptions.Name = property.Name; property.DataContextChangeAttributes ??= - property.GetAttributes().ToArray(); + property.GetAttributes(); property.DataContextManipulationAttribute ??= - property.GetAttributes().SingleOrDefault(); - if (property.DataContextManipulationAttribute != null && property.DataContextChangeAttributes.Any()) + property.GetAttribute(); + if (property.DataContextManipulationAttribute != null && property.DataContextChangeAttributes.Length != 0) throw new ArgumentException($"{nameof(DataContextChangeAttributes)} and {nameof(DataContextManipulationAttribute)} cannot be set both at property '{property.FullName}'."); property.IsBindingProperty = typeof(IBinding).IsAssignableFrom(property.PropertyType); - property.ObsoleteAttribute = property.AttributeProvider.GetCustomAttribute(); + property.ObsoleteAttribute = property.GetAttribute(); if (property.IsBindingProperty) { property.MarkupOptions.AllowHardCodedValue = false; } + + property.initialized = true; } public static void CheckAllPropertiesAreRegistered(Type controlType) diff --git a/src/Framework/Framework/Binding/DotvvmPropertyAlias.cs b/src/Framework/Framework/Binding/DotvvmPropertyAlias.cs index dca78455b9..1adb9deeee 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyAlias.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyAlias.cs @@ -12,10 +12,9 @@ public DotvvmPropertyAlias( string aliasName, Type declaringType, string aliasedPropertyName, - Type aliasedPropertyDeclaringType) + Type aliasedPropertyDeclaringType, + System.Reflection.ICustomAttributeProvider attributeProvider): base(aliasName, declaringType, isValueInherited: false) { - Name = aliasName; - DeclaringType = declaringType; AliasedPropertyName = aliasedPropertyName; AliasedPropertyDeclaringType = aliasedPropertyDeclaringType; MarkupOptions = new MarkupOptionsAttribute(); diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs new file mode 100644 index 0000000000..9cf5aad070 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -0,0 +1,465 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; + +namespace DotVVM.Framework.Binding +{ + public readonly struct DotvvmPropertyId: IEquatable, IEquatable, IComparable + { + public readonly uint Id; + public DotvvmPropertyId(uint id) + { + Id = id; + } + + public DotvvmPropertyId(ushort typeOrGroupId, ushort memberId) + { + Id = ((uint)typeOrGroupId << 16) | memberId; + } + + [MemberNotNullWhen(true, nameof(PropertyGroupInstance), nameof(GroupMemberName))] + public bool IsPropertyGroup => (int)Id < 0; + public ushort TypeId => (ushort)(Id >> 16); + public ushort GroupId => (ushort)((Id >> 16) ^ 0x80_00); + public ushort MemberId => (ushort)(Id & 0xFFFF); + + public bool IsZero => Id == 0; + + public DotvvmProperty PropertyInstance => DotvvmPropertyIdAssignment.GetProperty(Id) ?? throw new Exception($"Property with ID {Id} not registered."); + public DotvvmPropertyGroup? PropertyGroupInstance => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetPropertyGroup(GroupId); + public string? GroupMemberName => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetGroupMemberName(MemberId); + + public bool IsInPropertyGroup(ushort id) => (this.Id >> 16) == ((uint)id | 0x80_00u); + + public static DotvvmPropertyId CreatePropertyGroupId(ushort groupId, ushort memberId) => new DotvvmPropertyId((ushort)(groupId | 0x80_00), memberId); + + public static implicit operator DotvvmPropertyId(uint id) => new DotvvmPropertyId(id); + + public bool Equals(DotvvmPropertyId other) => Id == other.Id; + public bool Equals(uint other) => Id == other; + public override bool Equals(object? obj) => obj is DotvvmPropertyId id && Equals(id); + public override int GetHashCode() => (int)Id; + + public static bool operator ==(DotvvmPropertyId left, DotvvmPropertyId right) => left.Equals(right); + public static bool operator !=(DotvvmPropertyId left, DotvvmPropertyId right) => !left.Equals(right); + + public override string ToString() => $"PropId={Id}"; + public int CompareTo(DotvvmPropertyId other) => Id.CompareTo(other.Id); + } + + static class DotvvmPropertyIdAssignment + { + const int DEFAULT_PROPERTY_COUNT = 16; + static readonly ConcurrentDictionary typeIds; + private static readonly object controlTypeRegisterLock = new object(); + private static int controlCounter = 256; // first 256 types are reserved for DotVVM controls + private static ControlTypeInfo[] controls = new ControlTypeInfo[1024]; + private static readonly object groupRegisterLock = new object(); + private static int groupCounter = 256; // first 256 types are reserved for DotVVM controls + private static DotvvmPropertyGroup?[] propertyGroups = new DotvvmPropertyGroup[1024]; + private static ulong[] propertyGroupActiveBitmap = new ulong[1024 / 64]; + static readonly ConcurrentDictionary propertyGroupMemberIds = new(concurrencyLevel: 1, capacity: 256) { + ["id"] = GroupMembers.id, + ["class"] = GroupMembers.@class, + ["style"] = GroupMembers.style, + ["name"] = GroupMembers.name, + ["data-bind"] = GroupMembers.databind, + }; + private static readonly object groupMemberRegisterLock = new object(); + static string?[] propertyGroupMemberNames = new string[1024]; + + static DotvvmPropertyIdAssignment() + { + foreach (var n in propertyGroupMemberIds) + { + propertyGroupMemberNames[n.Value] = n.Key; + } + + typeIds = new() { + [typeof(DotvvmBindableObject)] = TypeIds.DotvvmBindableObject, + [typeof(DotvvmControl)] = TypeIds.DotvvmControl, + [typeof(HtmlGenericControl)] = TypeIds.HtmlGenericControl, + [typeof(RawLiteral)] = TypeIds.RawLiteral, + [typeof(Literal)] = TypeIds.Literal, + [typeof(ButtonBase)] = TypeIds.ButtonBase, + [typeof(Button)] = TypeIds.Button, + [typeof(LinkButton)] = TypeIds.LinkButton, + [typeof(TextBox)] = TypeIds.TextBox, + [typeof(RouteLink)] = TypeIds.RouteLink, + [typeof(CheckableControlBase)] = TypeIds.CheckableControlBase, + [typeof(CheckBox)] = TypeIds.CheckBox, + [typeof(Validator)] = TypeIds.Validator, + [typeof(Validation)] = TypeIds.Validation, + [typeof(ValidationSummary)] = TypeIds.ValidationSummary, + }; + } + +#region Optimized metadata accessors + public static bool IsInherited(DotvvmPropertyId propertyId) + { + if (propertyId.IsPropertyGroup) + return false; + return BitmapRead(controls[propertyId.TypeId].inheritedBitmap, propertyId.MemberId); + } + + public static bool UsesStandardAccessors(DotvvmPropertyId propertyId) + { + if (propertyId.IsPropertyGroup) + { + // property groups can't override GetValue, otherwise VirtualPropertyGroupDictionary wouldn't work either + return true; + } + else + { + var bitmap = controls[propertyId.TypeId].standardBitmap; + var index = propertyId.MemberId; + return BitmapRead(bitmap, index); + } + } + + public static bool IsActive(DotvvmPropertyId propertyId) + { + Debug.Assert(DotvvmPropertyIdAssignment.GetProperty(propertyId) != null, $"Property {propertyId} not registered."); + ulong[] bitmap; + uint index; + if (propertyId.IsPropertyGroup) + { + bitmap = propertyGroupActiveBitmap; + index = propertyId.GroupId; + } + else + { + bitmap = controls[propertyId.TypeId].activeBitmap; + index = propertyId.MemberId; + } + return BitmapRead(bitmap, index); + } + + public static DotvvmProperty? GetProperty(DotvvmPropertyId id) + { + if (id.IsPropertyGroup) + { + var groupIx = id.GroupId; + if (groupIx >= propertyGroups.Length) + return null; + var group = propertyGroups[groupIx]; + if (group is null) + return null; + + return group.GetDotvvmProperty(id.MemberId); + } + else + { + var typeId = id.TypeId; + if (typeId >= controls.Length) + return null; + var typeProps = controls[typeId].properties; + if (typeProps is null) + return null; + return typeProps[id.MemberId]; + } + } + + public static Compilation.IControlAttributeDescriptor? GetPropertyOrPropertyGroup(DotvvmPropertyId id) + { + if (id.IsPropertyGroup) + { + var groupIx = id.GroupId; + if (groupIx >= propertyGroups.Length) + return null; + return propertyGroups[groupIx]; + } + else + { + var typeId = id.TypeId; + if (typeId >= controls.Length) + return null; + var typeProps = controls[typeId].properties; + if (typeProps is null) + return null; + return typeProps[id.MemberId]; + } + } + + public static object? GetValueRaw(DotvvmBindableObject obj, DotvvmPropertyId id, bool inherit = true) + { + if (id.IsPropertyGroup) + { + // property groups can't override GetValue + if (obj.properties.TryGet(id, out var value)) + return value; + + return propertyGroups[id.GroupId]!.DefaultValue; + } + else + { + // TODO: maybe try if using the std/inherit bitmaps would be faster + var property = controls[id.TypeId].properties[id.MemberId]; + return property!.GetValue(obj, inherit); + } + } + + public static MarkupOptionsAttribute GetMarkupOptions(DotvvmPropertyId id) + { + if (id.IsPropertyGroup) + { + var groupIx = id.GroupId; + return propertyGroups[groupIx]!.MarkupOptions; + } + else + { + var typeId = id.TypeId; + var typeProps = controls[typeId].properties; + return typeProps[id.MemberId]!.MarkupOptions; + } + } + + /// Property or property group has type assignable to IBinding and bindings should not be evaluated in GetValue + public static bool IsBindingProperty(DotvvmPropertyId id) + { + if (id.IsPropertyGroup) + { + var groupIx = id.GroupId; + return propertyGroups[groupIx]!.IsBindingProperty; + } + else + { + var typeId = id.TypeId; + var typeProps = controls[typeId].properties; + return typeProps[id.MemberId]!.IsBindingProperty; + } + } + + public static DotvvmPropertyGroup? GetPropertyGroup(ushort id) + { + if (id >= propertyGroups.Length) + return null; + return propertyGroups[id]; + } +#endregion + +#region Registration + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort RegisterType(Type type) + { + if (typeIds.TryGetValue(type, out var existingId) && controls[existingId].locker is {}) + return existingId; + + return unlikely(type); + + static ushort unlikely(Type type) + { + var types = MemoryMarshal.CreateReadOnlySpan(ref type, 1); + Span ids = stackalloc ushort[1]; + RegisterTypes(types, ids); + return ids[0]; + } + } + public static void RegisterTypes(ReadOnlySpan types, Span ids) + { + if (types.Length == 0) + return; + + lock (controlTypeRegisterLock) + { + if (controlCounter + types.Length >= controls.Length) + { + VolatileResize(ref controls, 1 << (BitOperations.Log2((uint)(controlCounter + types.Length)) + 1)); + } + for (int i = 0; i < types.Length; i++) + { + var type = types[i]; + if (!typeIds.TryGetValue(type, out var id)) + { + id = (ushort)controlCounter++; + } + if (controls[id].locker is null) + { + controls[id].locker = new object(); + controls[id].controlType = type; + controls[id].properties = new DotvvmProperty[DEFAULT_PROPERTY_COUNT]; + controls[id].inheritedBitmap = new ulong[(DEFAULT_PROPERTY_COUNT - 1) / 64 + 1]; + controls[id].standardBitmap = new ulong[(DEFAULT_PROPERTY_COUNT - 1) / 64 + 1]; + controls[id].activeBitmap = new ulong[(DEFAULT_PROPERTY_COUNT - 1) / 64 + 1]; + typeIds[type] = id; + } + ids[i] = id; + } + } + } + public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) + { + if (property.GetType() == typeof(GroupedDotvvmProperty)) + throw new ArgumentException("RegisterProperty cannot be called with GroupedDotvvmProperty!"); + + var typeId = RegisterType(property.DeclaringType); + ref ControlTypeInfo control = ref controls[typeId]; + lock (control.locker) + { + var id = ++control.counter; + if (id > ushort.MaxValue) + throw new Exception("Too many properties registered for a single control type."); + if (id >= control.properties.Length) + { + VolatileResize(ref control.properties, control.properties.Length * 2); + VolatileResize(ref control.inheritedBitmap, control.inheritedBitmap.Length * 2); + VolatileResize(ref control.standardBitmap, control.standardBitmap.Length * 2); + VolatileResize(ref control.activeBitmap, control.activeBitmap.Length * 2); + } + + if (property.IsValueInherited) + BitmapSet(control.inheritedBitmap, (uint)id); + if (property.GetType() == typeof(DotvvmProperty)) + BitmapSet(control.standardBitmap, (uint)id); + if (property is ActiveDotvvmProperty) + BitmapSet(control.activeBitmap, (uint)id); + + control.properties[id] = property; + return new DotvvmPropertyId(typeId, (ushort)id); + } + } + + public static ushort RegisterPropertyGroup(DotvvmPropertyGroup group) + { + lock (groupRegisterLock) + { + var id = (ushort)groupCounter++; + if (id == 0) + throw new Exception("Too many property groups registered already."); + + if (id >= propertyGroups.Length) + { + VolatileResize(ref propertyGroups, propertyGroups.Length * 2); + VolatileResize(ref propertyGroupActiveBitmap, propertyGroupActiveBitmap.Length * 2); + } + + propertyGroups[id] = group; + if (group is ActiveDotvvmPropertyGroup) + BitmapSet(propertyGroupActiveBitmap, id); + return id; + } + } + + /// Thread-safe to read from the array while we are resizing + private static void VolatileResize(ref T[] array, int newSize) + { + var local = array; + Array.Resize(ref local, newSize); + Thread.MemoryBarrier(); // prevent reordering of the array assignment and array contents copy on weakly-ordered platforms + array = local; + } + +#endregion Registration + +#region Group members + private static ushort PredefinedPropertyGroupMemberId(ReadOnlySpan name) + { + switch (name) + { + case "class": return GroupMembers.@class; + case "id": return GroupMembers.id; + case "style": return GroupMembers.style; + case "name": return GroupMembers.name; + case "data-bind": return GroupMembers.databind; + default: return 0; + } + } + + public static ushort GetGroupMemberId(string name, bool registerIfNotFound) + { + var id = PredefinedPropertyGroupMemberId(name); + if (id != 0) + return id; + if (propertyGroupMemberIds.TryGetValue(name, out id)) + return id; + if (!registerIfNotFound) + return 0; + return RegisterGroupMember(name); + } + + private static ushort RegisterGroupMember(string name) + { + lock (groupMemberRegisterLock) + { + if (propertyGroupMemberIds.TryGetValue(name, out var id)) + return id; + id = (ushort)(propertyGroupMemberIds.Count + 1); + if (id == 0) + throw new Exception("Too many property group members registered already."); + if (id >= propertyGroupMemberNames.Length) + VolatileResize(ref propertyGroupMemberNames, propertyGroupMemberNames.Length * 2); + propertyGroupMemberNames[id] = name; + propertyGroupMemberIds[name] = id; + return id; + } + } + + internal static string? GetGroupMemberName(ushort id) + { + if (id < propertyGroupMemberNames.Length) + return propertyGroupMemberNames[id]; + return null; + } +#endregion Group members + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool BitmapRead(ulong[] bitmap, uint index) + { + return (bitmap[index / 64] & (1ul << (int)(index % 64))) != 0; + } + + static void BitmapSet(ulong[] bitmap, uint index) + { + bitmap[index / 64] |= 1ul << (int)(index % 64); + } + + private struct ControlTypeInfo + { + public object locker; + public DotvvmProperty?[] properties; + public Type controlType; + public ulong[] inheritedBitmap; + public ulong[] standardBitmap; + public ulong[] activeBitmap; + public int counter; + } + + public static class GroupMembers + { + public const ushort id = 1; + public const ushort @class = 2; + public const ushort style = 3; + public const ushort name = 4; + public const ushort databind = 5; + } + + public static class TypeIds + { + public const ushort DotvvmBindableObject = 1; + public const ushort DotvvmControl = 2; + public const ushort HtmlGenericControl = 3; + public const ushort RawLiteral = 4; + public const ushort Literal = 5; + public const ushort ButtonBase = 6; + public const ushort Button = 7; + public const ushort LinkButton = 8; + public const ushort TextBox = 9; + public const ushort RouteLink = 10; + public const ushort CheckableControlBase = 11; + public const ushort CheckBox = 12; + public const ushort Validator = 13; + public const ushort Validation = 14; + public const ushort ValidationSummary = 15; + // public const short Internal = 4; + } + + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs b/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs index 08ede07bb6..f41257d340 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs @@ -16,7 +16,7 @@ public sealed class DotvvmPropertyWithFallback : DotvvmProperty /// public DotvvmProperty FallbackProperty { get; private set; } - public DotvvmPropertyWithFallback(DotvvmProperty fallbackProperty) + public DotvvmPropertyWithFallback(DotvvmProperty fallbackProperty, string name, Type declaringType, bool isValueInherited): base(name, declaringType, isValueInherited) { this.FallbackProperty = fallbackProperty; } @@ -61,7 +61,7 @@ public static DotvvmProperty Register(Expression< /// Indicates whether the value can be inherited from the parent controls. public static DotvvmPropertyWithFallback Register(string propertyName, DotvvmProperty fallbackProperty, bool isValueInherited = false) { - var property = new DotvvmPropertyWithFallback(fallbackProperty); + var property = new DotvvmPropertyWithFallback(fallbackProperty, propertyName, typeof(TDeclaringType), isValueInherited: isValueInherited); Register(propertyName, isValueInherited: isValueInherited, property: property); property.DefaultValue = fallbackProperty.DefaultValue; return property; diff --git a/src/Framework/Framework/Binding/GroupedDotvvmProperty.cs b/src/Framework/Framework/Binding/GroupedDotvvmProperty.cs index 4f5f91c0d1..8556b39e87 100644 --- a/src/Framework/Framework/Binding/GroupedDotvvmProperty.cs +++ b/src/Framework/Framework/Binding/GroupedDotvvmProperty.cs @@ -16,28 +16,26 @@ public sealed class GroupedDotvvmProperty : DotvvmProperty, IGroupedPropertyDesc IPropertyGroupDescriptor IGroupedPropertyDescriptor.PropertyGroup => PropertyGroup; - public GroupedDotvvmProperty(string groupMemberName, DotvvmPropertyGroup propertyGroup) + private GroupedDotvvmProperty(string memberName, ushort memberId, DotvvmPropertyGroup group) + : base(DotvvmPropertyId.CreatePropertyGroupId(group.Id, memberId), group.Name + ":" + memberName, group.DeclaringType) { - this.GroupMemberName = groupMemberName; - this.PropertyGroup = propertyGroup; + this.GroupMemberName = memberName; + this.PropertyGroup = group; } - public static GroupedDotvvmProperty Create(DotvvmPropertyGroup group, string name) + public static GroupedDotvvmProperty Create(DotvvmPropertyGroup group, string name, ushort id) { - var propname = group.Name + ":" + name; - var prop = new GroupedDotvvmProperty(name, group) { + var prop = new GroupedDotvvmProperty(name, id, group) { PropertyType = group.PropertyType, - DeclaringType = group.DeclaringType, DefaultValue = group.DefaultValue, IsValueInherited = false, - Name = propname, ObsoleteAttribute = group.ObsoleteAttribute, OwningCapability = group.OwningCapability, UsedInCapabilities = group.UsedInCapabilities }; - DotvvmProperty.InitializeProperty(prop, group.AttributeProvider); + DotvvmProperty.InitializeProperty(prop, group.AttributeProvider); // TODO: maybe inline and specialize to just copy the group attributes return prop; } } diff --git a/src/Framework/Framework/Binding/ValueOrBinding.cs b/src/Framework/Framework/Binding/ValueOrBinding.cs index 7c6a073fe9..b3f4e254ff 100644 --- a/src/Framework/Framework/Binding/ValueOrBinding.cs +++ b/src/Framework/Framework/Binding/ValueOrBinding.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Binding.Properties; @@ -50,6 +51,7 @@ public ValueOrBinding(IStaticValueBinding binding) } /// Creates new ValueOrBinding which contains the specified value. Note that there is an implicit conversion for this, so calling the constructor explicitly may be unnecessary. + [DebuggerStepThrough] public ValueOrBinding(T value) { this.value = value; diff --git a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs index 8be168dd0e..f967e80f9b 100644 --- a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs +++ b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs @@ -24,17 +24,24 @@ public VirtualPropertyGroupDictionary(DotvvmBindableObject control, DotvvmProper this.group = group; } + DotvvmPropertyId GetMemberId(string key, bool createNew = false) + { + var memberId = DotvvmPropertyIdAssignment.GetGroupMemberId(key, registerIfNotFound: createNew); + return DotvvmPropertyId.CreatePropertyGroupId(group.Id, memberId); + } + + string GetMemberName(DotvvmPropertyId key) + { + return DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(key.Id & 0xFF_FF))!; + } + public IEnumerable Keys { get { - foreach (var (p, _) in control.properties) + foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return pg.GroupMemberName; - } + yield return GetMemberName(p); } } } @@ -44,13 +51,9 @@ public IEnumerable Values { get { - foreach (var (p, _) in control.properties) + foreach (var (p, value) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return (TValue)control.GetValue(p)!; - } + yield return (TValue)control.EvalPropertyValue(group, value)!; } } } @@ -59,48 +62,17 @@ public IEnumerable Properties { get { - foreach (var (p, _) in control.properties) + foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return pg; - } + var prop = group.GetDotvvmProperty(p.MemberId); + yield return prop; } } } - public int Count - { - get - { - // we don't want to use Linq Enumerable.Count() as it would allocate - // the enumerator. foreach gets the struct enumerator so it does not allocate anything - var count = 0; - foreach (var (p, _) in control.properties) - { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - count++; - } - } - return count; - } - } + public int Count => control.properties.CountPropertyGroup(group.Id); - public bool Any() - { - foreach (var (p, _) in control.properties) - { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - return true; - } - } - return false; - } + public bool Any() => control.properties.ContainsPropertyGroup(group.Id); public bool IsReadOnly => false; @@ -113,65 +85,79 @@ public TValue this[string key] { get { - var p = group.GetDotvvmProperty(key); + var p = GetMemberId(key); if (control.properties.TryGet(p, out var value)) - return (TValue)control.EvalPropertyValue(p, value)!; + return (TValue)control.EvalPropertyValue(group, value)!; else - return (TValue)p.DefaultValue!; + return (TValue)group.DefaultValue!; } set { - control.properties.Set(group.GetDotvvmProperty(key), value); + control.properties.Set(GetMemberId(key), value); } } /// Gets the value binding set to a specified property. Returns null if the property is not a binding, throws if the binding some kind of command. - public IValueBinding? GetValueBinding(string key) => control.GetValueBinding(group.GetDotvvmProperty(key)); + public IValueBinding? GetValueBinding(string key) + { + var binding = GetBinding(key); + if (binding != null && binding is not IStaticValueBinding) // throw exception on incompatible binding types + { + throw new BindingHelper.BindingNotSupportedException(binding) { RelatedControl = control }; + } + return binding as IValueBinding; + + } /// Gets the binding set to a specified property. Returns null if the property is not set or if the value is not a binding. - public IBinding? GetBinding(string key) => control.GetBinding(group.GetDotvvmProperty(key)); + public IBinding? GetBinding(string key) => GetValueRaw(key) as IBinding; /// Gets the value or a binding object for a specified property. public object? GetValueRaw(string key) { - var p = group.GetDotvvmProperty(key); - if (control.properties.TryGet(p, out var value)) + if (control.properties.TryGet(GetMemberId(key), out var value)) return value; else - return p.DefaultValue!; + return group.DefaultValue!; } /// Adds value or overwrites the property identified by . public void Set(string key, ValueOrBinding value) { - control.properties.Set(group.GetDotvvmProperty(key), value.UnwrapToObject()); + control.properties.Set(GetMemberId(key, createNew: true), value.UnwrapToObject()); } /// Adds value or overwrites the property identified by with the value. public void Set(string key, TValue value) => - control.properties.Set(group.GetDotvvmProperty(key), value); + control.properties.Set(GetMemberId(key, createNew: true), value); /// Adds binding or overwrites the property identified by with the binding. public void SetBinding(string key, IBinding binding) => - control.properties.Set(group.GetDotvvmProperty(key), binding); + control.properties.Set(GetMemberId(key, createNew: true), binding); public bool ContainsKey(string key) { - return control.Properties.ContainsKey(group.GetDotvvmProperty(key)); + return control.properties.Contains(GetMemberId(key)); } - private void AddOnConflict(GroupedDotvvmProperty property, object? value) + private void AddOnConflict(DotvvmPropertyId id, string key, object? value) { var merger = this.group.ValueMerger; if (merger is null) - throw new ArgumentException($"Cannot Add({property.Name}, {value}) since the value is already set and merging is not enabled on this property group."); - var mergedValue = merger.MergePlainValues(property, control.properties.GetOrThrow(property), value); - control.properties.Set(property, mergedValue); + throw new ArgumentException($"Cannot Add({key}, {value}) since the value is already set and merging is not enabled on this property group."); + var mergedValue = merger.MergePlainValues(id, control.properties.GetOrThrow(id), value); + control.properties.Set(id, mergedValue); } + internal void AddInternal(ushort key, object? val) + { + var prop = DotvvmPropertyId.CreatePropertyGroupId(group.Id, key); + if (!control.properties.TryAdd(prop, val)) + AddOnConflict(prop, prop.GroupMemberName.NotNull(), val); + } /// Adds the property identified by . If the property is already set, it tries appending the value using the group's public void Add(string key, ValueOrBinding value) { - var prop = group.GetDotvvmProperty(key); - object? val = value.UnwrapToObject(); + var prop = GetMemberId(key, createNew: true); + object? val = value.UnwrapToObject(); // TODO VOB boxing if (!control.properties.TryAdd(prop, val)) - AddOnConflict(prop, val); + AddOnConflict(prop, key, val); } /// Adds the property identified by . If the property is already set, it tries appending the value using the group's @@ -206,13 +192,14 @@ public static IDictionary CreateValueDictionary(DotvvmBindableOb var result = new Dictionary(); foreach (var (p, valueRaw) in control.properties) { - if (p is GroupedDotvvmProperty pg && pg.PropertyGroup == group) + if (p.IsInPropertyGroup(group.Id)) { - var valueObj = control.EvalPropertyValue(p, valueRaw); + var name = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + var valueObj = control.EvalPropertyValue(group, valueRaw); if (valueObj is TValue value) - result.Add(pg.GroupMemberName, value); + result.Add(name, value); else if (valueObj is null) - result.Add(pg.GroupMemberName, default!); + result.Add(name, default!); } } return result; @@ -223,16 +210,17 @@ public static IDictionary> CreatePropertyDictiona var result = new Dictionary>(); foreach (var (p, valRaw) in control.properties) { - if (p is GroupedDotvvmProperty pg && pg.PropertyGroup == group) + if (p.IsInPropertyGroup(group.Id)) { - result.Add(pg.GroupMemberName, ValueOrBinding.FromBoxedValue(valRaw)); + var name = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + result.Add(name, ValueOrBinding.FromBoxedValue(valRaw)); } } return result; } public bool Remove(string key) { - return control.Properties.Remove(group.GetDotvvmProperty(key)); + return control.properties.Remove(GetMemberId(key)); } /// Tries getting value of property identified by . If the property contains a binding, it will be automatically evaluated. @@ -240,10 +228,11 @@ public bool Remove(string key) public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) #pragma warning restore CS8767 { - var prop = group.GetDotvvmProperty(key); - if (control.properties.TryGet(prop, out var valueRaw)) + var memberId = DotvvmPropertyIdAssignment.GetGroupMemberId(key, registerIfNotFound: false); + var p = DotvvmPropertyId.CreatePropertyGroupId(group.Id, memberId); + if (control.properties.TryGet(p, out var valueRaw)) { - value = (TValue)control.EvalPropertyValue(prop, valueRaw)!; + value = (TValue)control.EvalPropertyValue(group, valueRaw)!; return true; } else @@ -262,31 +251,26 @@ public void Add(KeyValuePair item) public void Clear() { // we want to avoid allocating the list if there is only one property - DotvvmProperty? toRemove = null; - List? toRemoveRest = null; + DotvvmPropertyId toRemove = default; + List? toRemoveRest = null; - foreach (var (p, _) in control.properties) + foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) + if (toRemove.Id == 0) + toRemove = p; + else { - if (toRemove is null) - toRemove = p; - else - { - if (toRemoveRest is null) - toRemoveRest = new List(); - toRemoveRest.Add(p); - } + toRemoveRest ??= new List(); + toRemoveRest.Add(p); } } - if (toRemove is {}) - control.Properties.Remove(toRemove); + if (toRemove.Id != 0) + control.properties.Remove(toRemove); if (toRemoveRest is {}) foreach (var p in toRemoveRest) - control.Properties.Remove(p); + control.properties.Remove(p); } public bool Contains(KeyValuePair item) @@ -321,31 +305,73 @@ public bool Remove(KeyValuePair item) /// Enumerates all keys and values. If a property contains a binding, it will be automatically evaluated. public IEnumerator> GetEnumerator() { - foreach (var (p, value) in control.properties) + foreach (var (p, value) in control.properties.PropertyGroup(group.Id)) { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return new KeyValuePair(pg.GroupMemberName, (TValue)control.EvalPropertyValue(p, value)!); - } + var name = GetMemberName(p); + yield return new KeyValuePair(name, (TValue)control.EvalPropertyValue(group, value)!); } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// Enumerates all keys and values, without evaluating the bindings. - public IEnumerable> RawValues + public RawValuesCollection RawValues => new RawValuesCollection(this); + + public readonly struct RawValuesCollection: IEnumerable>, IReadOnlyDictionary { - get + readonly VirtualPropertyGroupDictionary self; + + internal RawValuesCollection(VirtualPropertyGroupDictionary self) { - foreach (var (p, value) in control.properties) + this.self = self; + } + + public object? this[string key] => self.GetValueRaw(key); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value) => + self.control.properties.TryGet(self.GetMemberId(key), out value); + + public IEnumerable Keys => self.Keys; + + public IEnumerable Values + { + get { - var pg = p as GroupedDotvvmProperty; - if (pg != null && pg.PropertyGroup == group) - { - yield return new KeyValuePair(pg.GroupMemberName, value!); - } + foreach (var (_, value) in self.control.properties.PropertyGroup(self.group.Id)) + yield return value; } } + + public int Count => self.Count; + + public bool ContainsKey(string key) => self.ContainsKey(key); + + public RawValuesEnumerator GetEnumerator() => new RawValuesEnumerator(self.control.properties.EnumeratePropertyGroup(self.group.Id)); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + public struct RawValuesEnumerator : IEnumerator> + { + private DotvvmControlPropertyIdGroupEnumerator inner; + + public KeyValuePair Current + { + get + { + var (p, value) = inner.Current; + var mem = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + return new KeyValuePair(mem, value); + } + } + + object IEnumerator.Current => Current; + + public RawValuesEnumerator(DotvvmControlPropertyIdGroupEnumerator dotvvmControlPropertyIdEnumerator) + { + this.inner = dotvvmControlPropertyIdEnumerator; + } + + public bool MoveNext() => inner.MoveNext(); + public void Reset() => inner.Reset(); + public void Dispose() => inner.Dispose(); } } } diff --git a/src/Framework/Framework/Compilation/AttributeValueMergerBase.cs b/src/Framework/Framework/Compilation/AttributeValueMergerBase.cs index a4248d2287..05d175ed2c 100644 --- a/src/Framework/Framework/Compilation/AttributeValueMergerBase.cs +++ b/src/Framework/Framework/Compilation/AttributeValueMergerBase.cs @@ -20,7 +20,7 @@ namespace DotVVM.Framework.Compilation /// /// Merges provided values based on implemented static 'MergeValues' or 'MergeExpression' method: /// - /// implement public static object MergeValues([DotvvmProperty], ValueA, ValueB) and this will decide which will should be used + /// implement public static object MergeValues([DotvvmPropertyId], ValueA, ValueB) and this will decide which will should be used /// or implement public static Expression MergeExpressions(DotvvmProperty, Expression a, Expression b) /// public abstract class AttributeValueMergerBase : IAttributeValueMerger @@ -60,7 +60,10 @@ public abstract class AttributeValueMergerBase : IAttributeValueMerger if (bindingA.BindingType != bindingB.BindingType) { error = $"Cannot merge values of different binding types"; return null; } } - var resultExpression = TryOptimizeMethodCall(TryFindMethod(GetType(), MergeExpressionsMethodName, Expression.Constant(property), Expression.Constant(valA), Expression.Constant(valB))) as Expression; + var resultExpression = TryOptimizeMethodCall( + TryFindMethod(GetType(), MergeExpressionsMethodName, Expression.Constant(property), Expression.Constant(valA), Expression.Constant(valB)) ?? + TryFindMethod(GetType(), MergeExpressionsMethodName, Expression.Constant(property.Id), Expression.Constant(valA), Expression.Constant(valB)) + ) as Expression; // Try to find MergeValues method if MergeExpression does not exists, or try to eval it to constant if expression is not constant if (resultExpression == null || valA.NodeType == ExpressionType.Constant && valB.NodeType == ExpressionType.Constant && resultExpression.NodeType != ExpressionType.Constant) @@ -121,6 +124,7 @@ protected virtual ResolvedPropertySetter EmitConstant(object? value, DotvvmPrope protected virtual MethodCallExpression? TryFindMergeMethod(DotvvmProperty property, Expression a, Expression b) { return + TryFindMethod(GetType(), MergeValuesMethodName, Expression.Constant(property.Id), a, b) ?? TryFindMethod(GetType(), MergeValuesMethodName, Expression.Constant(property), a, b) ?? TryFindMethod(GetType(), MergeValuesMethodName, a, b); } @@ -143,7 +147,7 @@ protected virtual ResolvedPropertySetter EmitConstant(object? value, DotvvmPrope return methodCall; else return null; } - public virtual object? MergePlainValues(DotvvmProperty prop, object? a, object? b) + public virtual object? MergePlainValues(DotvvmPropertyId prop, object? a, object? b) { return ((dynamic)this).MergeValues(prop, a, b); } diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs index 7405493110..dd0f043f14 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs @@ -1,11 +1,16 @@ using System; +using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Configuration; using DotVVM.Framework.Controls; @@ -24,10 +29,10 @@ public class DefaultControlResolver : ControlResolverBase private readonly CompiledAssemblyCache compiledAssemblyCache; private readonly Dictionary? controlNameMappings; - private static object locker = new object(); - private static bool isInitialized = false; - private static object dotvvmLocker = new object(); - private static bool isDotvvmInitialized = false; + private static readonly object locker = new object(); + private static volatile bool isInitialized = false; + private static readonly object dotvvmLocker = new object(); + private static volatile bool isDotvvmInitialized = false; public DefaultControlResolver(DotvvmConfiguration configuration, IControlBuilderFactory controlBuilderFactory, CompiledAssemblyCache compiledAssemblyCache) : base(configuration.Markup) @@ -65,6 +70,7 @@ internal static Task InvokeStaticConstructorsOnDotvvmControls() lock(dotvvmLocker) { if (isDotvvmInitialized) return; InvokeStaticConstructorsOnAllControls(typeof(DotvvmControl).Assembly); + isDotvvmInitialized = true; } }); } @@ -74,29 +80,149 @@ internal static Task InvokeStaticConstructorsOnDotvvmControls() /// private void InvokeStaticConstructorsOnAllControls() { - var dotvvmAssembly = typeof(DotvvmControl).Assembly.GetName().Name!; var dotvvmInitTask = InvokeStaticConstructorsOnDotvvmControls(); - if (configuration.ExperimentalFeatures.ExplicitAssemblyLoading.Enabled) - { + var dotvvmAssembly = typeof(DotvvmControl).Assembly.GetName().Name!; + + var assemblies = configuration.ExperimentalFeatures.ExplicitAssemblyLoading.Enabled ? // use only explicitly specified assemblies from configuration - // and do not call GetTypeInfo to prevent unnecessary dependent assemblies from loading - var assemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)) - .Distinct(); - - Parallel.ForEach(assemblies, a => { - InvokeStaticConstructorsOnAllControls(a); - }); + compiledAssemblyCache.GetReferencedAssemblies() : + compiledAssemblyCache.GetAllAssemblies(); + InvokeStaticConstructorsOnAllControls(OrderAndFilterAssemblies(assemblies, dotvvmAssembly)); + dotvvmInitTask.Wait(); + } + + /// Filters out assemblies which don't reference DotVVM.Framework, and topologically orders them according to their references, then alphabetically to resolve ties + private List OrderAndFilterAssemblies(IEnumerable assemblies, string rootAssembly) + { + var assemblyList = new List(); + var renumbering = new Dictionary(); + var references = new List(); + + var namelessAssemblies = new List(); // place them at the end + foreach (var a in assemblies) + { + var name = a.GetName(); + var r = a.GetReferencedAssemblies(); + if (ReferencesAssembly(r, rootAssembly)) + { + if (name.Name is null) + namelessAssemblies.Add(a); + else if (renumbering.TryAdd(name.Name, assemblyList.Count)) + { + assemblyList.Add(a); + references.Add(r); + } + } } - else + + // Kahn's algorithm - https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + // with additional sorting step to resolve ties + var inDegree = new int[assemblyList.Count]; + var forwardReferences = new List?[assemblyList.Count]; + var roots = new List(); + for (int i = 0; i < references.Count; i++) { - var assemblies = GetAllRelevantAssemblies(dotvvmAssembly); - Parallel.ForEach(assemblies, a => { - InvokeStaticConstructorsOnAllControls(a); - }); + var inCount = 0; + foreach (var r in references[i]) + if (renumbering.TryGetValue(r.Name!, out var idx)) + { + inCount++; + forwardReferences[idx] ??= new List(); + forwardReferences[idx]!.Add(i); + } + inDegree[i] = inCount; + if (inCount == 0) + roots.Add(i); } - dotvvmInitTask.Wait(); + + var sorted = new List(capacity: assemblyList.Count + namelessAssemblies.Count); + var newRoots = new List(); + var comparer = makeComparator(assemblyList); + while (roots.Count > 0) + { + if (roots.Count > 1) + roots.Sort(comparer); + + foreach (var item in roots) + { + sorted.Add(assemblyList[item]); + if (forwardReferences[item] is null) + continue; + foreach (var r in forwardReferences[item]!) + { + inDegree[r]--; + if (inDegree[r] == 0) + newRoots.Add(r); + } + } + roots.Clear(); + (roots, newRoots) = (newRoots, roots); + } + + // no need to throw in production, we only want the topological ordering for consistent property IDs + Debug.Assert(sorted.Count == assemblyList.Count, "Loop in assembly references detected"); + + sorted.AddRange(namelessAssemblies); + return sorted; + + static Comparison makeComparator(List assemblyList) => (a, b) => string.Compare(assemblyList[a].GetName().Name, assemblyList[b].GetName().Name, StringComparison.Ordinal); + } + static bool ReferencesAssembly(AssemblyName[] references, string root) + { + foreach (var r in references) + if (r.Name == root) + return true; + return false; + } + + private void InvokeStaticConstructorsOnAllControls(List assemblies) + { + // try to assign property IDs consistently across runs, as the order of properties depends on this which may be observable to the user + // we first assigns IDs to all controls is each assembly, then we run the static constructors in parallel + + // this means we sequence the control registration, while parallelizing assembly loading and property registration + // in practice, control registration is trivial, the main performance hit might arrise from a single assembly taking longer to load + + // Assembly1 loading ................|register controls|register properties + // Assembly2 loading. waiting |registercontrols|... + // Assembly3 loading. waiting |registercontrols|... + + var paralelismLimiter = new SemaphoreSlim(Environment.ProcessorCount); + + var controlIdsAssigned = Enumerable.Range(0, assemblies.Count).Select(_ => new TaskCompletionSource()).ToArray(); + + var tasks = Enumerable.Range(0, assemblies.Count).Select(i => Task.Run(async () => { + await paralelismLimiter.WaitAsync(); + try { + var controls = new List(); + foreach (var type in assemblies[i].GetLoadableTypes()) + { + if (type.IsClass && !type.ContainsGenericParameters && type.IsDefined(typeof(ContainsDotvvmPropertiesAttribute), true)) + { + controls.Add(type); + } + } + // wait for the previous assembly to finish loading and assigning control IDs + if (i > 0) + await controlIdsAssigned[i - 1].Task; + var controlIds = new ushort[controls.Count]; + DotvvmPropertyIdAssignment.RegisterTypes(CollectionsMarshal.AsSpan(controls), controlIds); + + // let the next assembly run + controlIdsAssigned[i].SetResult(); + + foreach (var type in controls) + { + InitType(type); + } + } + finally { + paralelismLimiter.Release(); + } + + })).ToArray(); + Task.WaitAll(tasks); } private static void InvokeStaticConstructorsOnAllControls(Assembly assembly) @@ -106,7 +232,6 @@ private static void InvokeStaticConstructorsOnAllControls(Assembly assembly) if (!c.IsClass || c.ContainsGenericParameters) continue; - InitType(c); } } @@ -162,14 +287,13 @@ private static void RegisterCapabilitiesFromInterfaces(Type type) } } - private IEnumerable GetAllRelevantAssemblies(string dotvvmAssembly) + private Assembly[] GetAllRelevantAssemblies(string dotvvmAssembly) { #if DotNetCore - var assemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)); + var assemblies = compiledAssemblyCache.GetAllAssemblies(); #else var loadedAssemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)); + .Where(a => ReferencesAssembly(a.GetReferencedAssemblies(), dotvvmAssembly)); var visitedAssemblies = new HashSet(); @@ -186,8 +310,9 @@ private IEnumerable GetAllRelevantAssemblies(string dotvvmAssembly) throw new Exception($"Unable to load assembly '{an.FullName}' referenced by '{a.FullName}'.", ex); } })) - .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)) - .Distinct(); + .Where(a => ReferencesAssembly(a.GetReferencedAssemblies(), dotvvmAssembly)) + .Distinct() + .ToArray(); #endif return assemblies; } diff --git a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs index 2fa03d4137..d86bf91649 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs @@ -9,6 +9,7 @@ using DotVVM.Framework.Utils; using System.Runtime.CompilerServices; using System.Collections.Immutable; +using DotVVM.Framework.Binding.Expressions; namespace DotVVM.Framework.Compilation.ControlTree { @@ -38,8 +39,10 @@ public class DotvvmPropertyGroup : IPropertyGroupDescriptor public Type PropertyType { get; } ITypeDescriptor IControlAttributeDescriptor.PropertyType => new ResolvedTypeDescriptor(PropertyType); public IAttributeValueMerger? ValueMerger { get; } + public bool IsBindingProperty { get; } + internal ushort Id { get; } - private ConcurrentDictionary generatedProperties = new(); + private readonly ConcurrentDictionary> generatedProperties = new(); /// The capability which declared this property. When the property is declared by an capability, it can only be used by this capability. public DotvvmCapabilityProperty? OwningCapability { get; } @@ -61,7 +64,9 @@ internal DotvvmPropertyGroup(PrefixArray prefixes, Type valueType, Type declarin { ValueMerger = (IAttributeValueMerger?)Activator.CreateInstance(MarkupOptions.AttributeValueMerger); } + this.IsBindingProperty = typeof(IBinding).IsAssignableFrom(valueType); this.OwningCapability = owningCapability; + this.Id = DotvvmPropertyIdAssignment.RegisterPropertyGroup(this); } private static (MarkupOptionsAttribute, DataContextChangeAttribute[], DataContextStackManipulationAttribute?, ObsoleteAttribute?) @@ -89,7 +94,32 @@ internal void AddUsedInCapability(DotvvmCapabilityProperty? p) IPropertyDescriptor IPropertyGroupDescriptor.GetDotvvmProperty(string name) => GetDotvvmProperty(name); public GroupedDotvvmProperty GetDotvvmProperty(string name) { - return generatedProperties.GetOrAdd(name, n => GroupedDotvvmProperty.Create(this, name)); + var id = DotvvmPropertyIdAssignment.GetGroupMemberId(name, registerIfNotFound: true); + return GetDotvvmProperty(id); + } + + public GroupedDotvvmProperty GetDotvvmProperty(ushort nameId) + { + while (true) + { + if (generatedProperties.TryGetValue(nameId, out var resultRef)) + { + if (resultRef.TryGetTarget(out var result)) + return result; + else + generatedProperties.TryUpdate(nameId, new(CreateMemberProperty(nameId)), resultRef); + } + else + { + generatedProperties.TryAdd(nameId, new(CreateMemberProperty(nameId))); + } + } + } + + private GroupedDotvvmProperty CreateMemberProperty(ushort nameId) + { + var name = DotvvmPropertyIdAssignment.GetGroupMemberName(nameId).NotNull(); + return GroupedDotvvmProperty.Create(this, name, nameId); } private static ConcurrentDictionary<(Type, string), DotvvmPropertyGroup> descriptorDictionary = new(); diff --git a/src/Framework/Framework/Compilation/HtmlAttributeValueMerger.cs b/src/Framework/Framework/Compilation/HtmlAttributeValueMerger.cs index df2546f49b..6a2a2182f5 100644 --- a/src/Framework/Framework/Compilation/HtmlAttributeValueMerger.cs +++ b/src/Framework/Framework/Compilation/HtmlAttributeValueMerger.cs @@ -22,10 +22,12 @@ public static Expression MergeExpressions(GroupedDotvvmProperty property, Expres // return Expression.Call(typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string), typeof(string) }), a, Expression.Constant(separator), b); } - public static string? MergeValues(GroupedDotvvmProperty property, string? a, string? b) + public static string? MergeValues(DotvvmPropertyId property, string? a, string? b) { + if (!property.IsPropertyGroup) throw new ArgumentException("HtmlAttributeValueMerger only supports property group", nameof(property)); + var attributeName = property.GroupMemberName; // for perf reasons only do this compile time - we'll deduplicate the attribute if it's a CSS class - if (property.GroupMemberName == "class" && a is string && b is string) + if (attributeName == "class" && a is string && b is string) { var classesA = a.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); var classesB = b.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries) @@ -33,18 +35,19 @@ public static Expression MergeExpressions(GroupedDotvvmProperty property, Expres b = string.Join(" ", classesB); } - return HtmlWriter.JoinAttributeValues(property.GroupMemberName, a, b); + return HtmlWriter.JoinAttributeValues(attributeName, a, b); } - public override object? MergePlainValues(DotvvmProperty prop, object? a, object? b) + public override object? MergePlainValues(DotvvmPropertyId prop, object? a, object? b) { - var gProp = (GroupedDotvvmProperty)prop; + if (!prop.IsPropertyGroup) throw new ArgumentException("HtmlAttributeValueMerger only supports property group", nameof(prop)); + var attributeName = prop.GroupMemberName; if (a is null) return b; if (b is null) return a; if (a is string aString && b is string bString) - return HtmlWriter.JoinAttributeValues(gProp.GroupMemberName, aString, bString); + return HtmlWriter.JoinAttributeValues(attributeName, aString, bString); // append to list. Order does not matter in html attributes if (a is HtmlGenericControl.AttributeList alist) diff --git a/src/Framework/Framework/Compilation/IAttributeValueMerger.cs b/src/Framework/Framework/Compilation/IAttributeValueMerger.cs index 16608691f8..ea764b40df 100644 --- a/src/Framework/Framework/Compilation/IAttributeValueMerger.cs +++ b/src/Framework/Framework/Compilation/IAttributeValueMerger.cs @@ -12,6 +12,6 @@ namespace DotVVM.Framework.Compilation public interface IAttributeValueMerger { ResolvedPropertySetter? MergeResolvedValues(ResolvedPropertySetter a, ResolvedPropertySetter b, out string? error); - object? MergePlainValues(DotvvmProperty prop, object? a, object? b); + object? MergePlainValues(DotvvmPropertyId prop, object? a, object? b); } } diff --git a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs index 24ac760cf6..354d7e6c26 100644 --- a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs +++ b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs @@ -52,7 +52,7 @@ public static ResolvedControl FromRuntimeControl( { var templateControl = (DotvvmMarkupControl)Activator.CreateInstance(descriptor.ControlType)!; markupControl.SetProperties(templateControl); - foreach (var p in templateControl.properties) + foreach (var p in templateControl.Properties) { var propertyDC = GetPropertyDataContext(obj, p.Key, dataContext); control.SetProperty( @@ -81,7 +81,7 @@ public static ResolvedControl FromRuntimeControl( rc.ConstructorParameters = new object[] { htmlControl.TagName! }; } - foreach (var p in obj.properties) + foreach (var p in obj.Properties) { var propertyDC = GetPropertyDataContext(obj, p.Key, dataContext); rc.SetProperty( diff --git a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs index d9dd2a0476..e1fee4bacc 100644 --- a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs +++ b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -175,30 +176,110 @@ public void CommitDotvvmProperties(string name) controlProperties.Remove(name); if (properties.Count == 0) return; - properties.Sort((a, b) => string.Compare(a.prop.FullName, b.prop.FullName, StringComparison.Ordinal)); + properties.Sort((a, b) => a.prop.Id.CompareTo(b.prop.Id)); - var (hashSeed, keys, values) = PropertyImmutableHashtable.CreateTableWithValues(properties.Select(p => p.prop).ToArray(), properties.Select(p => p.value).ToArray()); + if (!TryEmitPerfectHashAssignment(GetParameterOrVariable(name), properties)) + { + EmitDictionaryAssignment(GetParameterOrVariable(name), properties); + } + } - Expression valueExpr; - if (TryCreateArrayOfConstants(values, out var invertedValues)) + /// Set DotVVM properties as array of keys and array of values + private bool TryEmitPerfectHashAssignment(ParameterExpression control, List<(DotvvmProperty prop, Expression value)> properties) + { + return false; + if (properties.Count > 50) { - valueExpr = EmitValue(invertedValues); + return false; } - else + + try { - valueExpr = EmitCreateArray( - typeof(object), - values.Select(v => v ?? EmitValue(null)) - ); + var (flags, keys, values) = PropertyImmutableHashtable.CreateTableWithValues(properties.Select(p => p.prop.Id).ToArray(), properties.Select(p => p.value).ToArray()); + + Expression valueExpr; + if (TryCreateArrayOfConstants(values, out var invertedValues)) + { + valueExpr = EmitValue(invertedValues); + } + else + { + valueExpr = EmitCreateArray( + typeof(object), + values.Select(v => v ?? EmitValue(null)) + ); + flags |= 1u << 31; // owns values flag + } + + var keyExpr = EmitValue(keys); + + // PropertyImmutableHashtable.SetValuesToDotvvmControl(control, keys, values, hashSeed) + var magicSetValueCall = Expression.Call(typeof(PropertyImmutableHashtable), nameof(PropertyImmutableHashtable.SetValuesToDotvvmControl), emptyTypeArguments, Expression.Convert(control, typeof(DotvvmBindableObject)), keyExpr, valueExpr, EmitValue(flags)); + + EmitStatement(magicSetValueCall); } + catch (PropertyImmutableHashtable.CannotMakeHashtableException) + { + return false; + } + return true; + } - var keyExpr = EmitValue(keys); - // control.MagicSetValue(keys, values, hashSeed) - var controlParameter = GetParameterOrVariable(name); + /// Set DotVVM properties as a Dictionary, potentially shared one across different instantiations + private void EmitDictionaryAssignment(ParameterExpression control, List<(DotvvmProperty prop, Expression value)> properties) + { + if (properties.Count == 0) + { + return; + } + var constants = new Dictionary(capacity: properties.Count); + var variables = new List>(); + + foreach (var (prop, value) in properties) + { + if (value is ConstantExpression constant) + { + Debug.Assert(constant.Value is not DotvvmBindableObject and not IEnumerable, "Internal compiler bug: We cannot allow sharing of DotvvmBindableObject instances in the constants dictionary."); + constants.Add(prop.Id, constant.Value); + } + else + { + variables.Add(new (prop.Id, value)); + } + } + + Expression dict; + + if (variables.Count == 0) + { + dict = EmitValue(constants); + } + else + { + throw new Exception("kokoooot"); + var variable = Expression.Parameter(typeof(Dictionary), "props_" + control.Name); + + // var dict = new Dictionary(constants); + // dict.Add(variables[0].Key, variables[0].Value); + // dict.Add(variables[1].Key, variables[1].Value); + // ... + var copyConstructor = typeof(Dictionary).GetConstructor([ typeof(IDictionary) ]).NotNull(); + var propIdConstructor = typeof(DotvvmPropertyId).GetConstructor([ typeof(uint) ]).NotNull(); + dict = Expression.Block(new [] { variable }, + Expression.Assign(variable, + Expression.New(copyConstructor, EmitValue(constants))), + Expression.Block(variables.Select(kv => + Expression.Call(variable, "Add", emptyTypeArguments, Expression.New(propIdConstructor, EmitValue(kv.Key.Id)), kv.Value))), + variable); + } - var magicSetValueCall = Expression.Call(controlParameter, nameof(DotvvmBindableObject.MagicSetValue), emptyTypeArguments, keyExpr, valueExpr, EmitValue(hashSeed)); + // PropertyImmutableHashtable.SetValuesToDotvvmControl(control, dict) + var magicSetValueCall = Expression.Call(typeof(PropertyImmutableHashtable), nameof(PropertyImmutableHashtable.SetValuesToDotvvmControl), emptyTypeArguments, + /*control:*/ Expression.Convert(control, typeof(DotvvmBindableObject)), + /*dict:*/ dict, + /*owns:*/ EmitValue(variables.Count > 0)); EmitStatement(magicSetValueCall); } diff --git a/src/Framework/Framework/Controls/CompositeControl.cs b/src/Framework/Framework/Controls/CompositeControl.cs index cf8b774555..c7803b4efc 100644 --- a/src/Framework/Framework/Controls/CompositeControl.cs +++ b/src/Framework/Framework/Controls/CompositeControl.cs @@ -154,10 +154,10 @@ private void CopyPostbackHandlersAndAdd(IEnumerable childControls } var commands = new List<(string, ICommandBinding)>(); - foreach (var (property, value) in this.Properties) + foreach (var (property, value) in this.properties) { if (value is ICommandBinding command) - commands.Add((property.Name, command)); + commands.Add((property.PropertyInstance.Name, command)); } foreach (var child in childControls) @@ -180,10 +180,10 @@ protected internal T CopyPostBackHandlersRecursive(T target) return target; var commands = new List<(string, ICommandBinding)>(); - foreach (var (property, value) in this.Properties) + foreach (var (property, value) in this.properties) { if (value is ICommandBinding command) - commands.Add((property.Name, command)); + commands.Add((property.PropertyInstance.Name, command)); } CopyPostBackHandlersRecursive(handlers, commands, target); @@ -193,7 +193,7 @@ protected internal T CopyPostBackHandlersRecursive(T target) private static void CopyPostBackHandlersRecursive(PostBackHandlerCollection handlers, List<(string, ICommandBinding)> commands, DotvvmBindableObject target) { PostBackHandlerCollection? childHandlers = null; - foreach (var (property, value) in target.Properties) + foreach (var (property, value) in target.properties) { if (value is ICommandBinding command) { @@ -201,7 +201,7 @@ private static void CopyPostBackHandlersRecursive(PostBackHandlerCollection hand { if (object.ReferenceEquals(command, matchedCommand)) { - CopyMatchingPostBackHandlers(handlers, oldName, property.Name, ref childHandlers); + CopyMatchingPostBackHandlers(handlers, oldName, property.PropertyInstance.Name, ref childHandlers); break; } } diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index cb45b1d0a6..1d224671cd 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -5,6 +5,7 @@ using System.Linq; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Utils; @@ -124,6 +125,56 @@ public T GetValue(DotvvmProperty property, bool inherit = true) return value; } + /// If the object is IBinding and the property is not of type IBinding, it is evaluated. + internal object? EvalPropertyValue(DotvvmPropertyGroup property, object? value) + { + if (property.IsBindingProperty) return value; + if (value is IBinding) + { + // handle binding + if (value is IStaticValueBinding binding) + { + value = binding.Evaluate(this); + } + else if (value is ICommandBinding command) + { + value = command.GetCommandDelegate(this); + } + else + { + throw new NotSupportedException($"Cannot evaluate binding {value} of type {value.GetType().Name}."); + } + } + return value; + } + + /// If the object is IBinding and the property is not of type IBinding, it is evaluated. + internal object? EvalPropertyValue(DotvvmPropertyId property, object? value) + { + if (DotvvmPropertyIdAssignment.IsBindingProperty(property)) return value; + if (value is IBinding) + { + DotvvmBindableObject control = this; + // DataContext is always bound to it's parent, setting it right here is a bit faster + if (property == DataContextProperty.Id) + control = control.Parent ?? throw new DotvvmControlException(this, "Cannot set DataContext binding on the root control"); + // handle binding + if (value is IStaticValueBinding binding) + { + value = binding.Evaluate(control); + } + else if (value is ICommandBinding command) + { + value = command.GetCommandDelegate(control); + } + else + { + throw new NotSupportedException($"Cannot evaluate binding {value} of type {value.GetType().Name}."); + } + } + return value; + } + /// /// Gets the value of a specified property. If the property contains a binding, it is evaluted. /// @@ -138,10 +189,9 @@ public T GetValue(DotvvmProperty property, bool inherit = true) return property.GetValue(this, inherit); } - /// For internal use, public because it's used from our generated code. If want to use it, create the arguments using - public void MagicSetValue(DotvvmProperty[] keys, object[] values, int hashSeed) + public virtual object? GetValueRaw(DotvvmPropertyId property, bool inherit = true) { - this.properties.AssignBulk(keys, values, hashSeed); + return DotvvmPropertyIdAssignment.GetValueRaw(this, property, inherit); } /// Sets the value of a specified property. diff --git a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs index 580b3f6f7c..9b7768b93c 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs @@ -223,9 +223,9 @@ public static TControl AddAttributes(this TControl control, Vi public static TControl AddCssClass(this TControl control, ValueOrBinding className) where TControl : IControlWithHtmlAttributes { - var classNameObj = className.UnwrapToObject(); - if (classNameObj is null or "") return control; - return AddAttribute(control, "class", classNameObj); + if (className.ValueOrDefault is null or "") return control; + control.Attributes.AddInternal(DotvvmPropertyIdAssignment.GroupMembers.@class, className.UnwrapToObject()); + return control; } /// Appends a css class to this control. Note that it is currently not supported if multiple bindings would have to be joined together. Returns for fluent API usage. @@ -234,7 +234,8 @@ public static TControl AddCssClass(this TControl control, string? clas { if (className is null or "") return control; - return AddAttribute(control, "class", className); + control.Attributes.AddInternal(DotvvmPropertyIdAssignment.GroupMembers.@class, className); + return control; } /// Appends a list of css classes to this control. Returns for fluent API usage. diff --git a/src/Framework/Framework/Controls/DotvvmControl.cs b/src/Framework/Framework/Controls/DotvvmControl.cs index b03c192a39..1201afcfbd 100644 --- a/src/Framework/Framework/Controls/DotvvmControl.cs +++ b/src/Framework/Framework/Controls/DotvvmControl.cs @@ -234,16 +234,19 @@ protected struct RenderState } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static bool TouchProperty(DotvvmProperty property, object? val, ref RenderState r) + protected static bool TouchProperty(DotvvmPropertyId property, object? val, ref RenderState r) { - if (property == DotvvmControl.IncludeInPageProperty) + if (property == DotvvmControl.IncludeInPageProperty.Id) r.IncludeInPage = val; - else if (property == DotvvmControl.DataContextProperty) + else if (property == DotvvmControl.DataContextProperty.Id) r.DataContext = val as IValueBinding; - else if (property is ActiveDotvvmProperty) - r.HasActives = true; - else if (property is GroupedDotvvmProperty groupedProperty && groupedProperty.PropertyGroup is ActiveDotvvmPropertyGroup) - r.HasActiveGroups = true; + else if (DotvvmPropertyIdAssignment.IsActive(property)) + { + if (property.IsPropertyGroup) + r.HasActiveGroups = true; + else + r.HasActives = true; + } else return false; return true; } @@ -266,20 +269,20 @@ protected bool RenderBeforeControl(in RenderState r, IHtmlWriter writer, IDotvvm if (r.HasActives) foreach (var item in properties) { - if (item.Key is ActiveDotvvmProperty activeProp) + if (!item.Key.IsPropertyGroup && DotvvmPropertyIdAssignment.IsActive(item.Key)) { - activeProp.AddAttributesToRender(writer, context, this); + ((ActiveDotvvmProperty)item.Key.PropertyInstance).AddAttributesToRender(writer, context, this); } } if (r.HasActiveGroups) { var groups = properties - .Where(p => p.Key is GroupedDotvvmProperty gp && gp.PropertyGroup is ActiveDotvvmPropertyGroup) - .GroupBy(p => ((GroupedDotvvmProperty)p.Key).PropertyGroup); + .Where(p => p.Key.PropertyGroupInstance is ActiveDotvvmPropertyGroup) + .GroupBy(p => (ActiveDotvvmPropertyGroup)p.Key.PropertyGroupInstance!); foreach (var item in groups) { - ((ActiveDotvvmPropertyGroup)item.Key).AddAttributesToRender(writer, context, this, item.Select(i => i.Key)); + item.Key.AddAttributesToRender(writer, context, this, item.Select(i => i.Key.PropertyInstance)); } } diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index 468cb79e57..e73858d11d 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -3,25 +3,24 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Utils; namespace DotVVM.Framework.Controls { [StructLayout(LayoutKind.Explicit)] - internal struct DotvvmControlProperties : IEnumerable> + internal struct DotvvmControlProperties : IEnumerable> { // There are 3 possible states of this structure: // 1. keys == values == null --> it is empty - // 2. keys == null & values is Dictionary --> it falls back to traditional mutable property dictionary - // 3. keys is DotvvmProperty[] & values is object[] --> read-only perfect 2-slot hashing + // 2. keys == null & values is Dictionary --> it falls back to traditional mutable property dictionary + // 3. keys is DotvvmPropertyId[] & values is object[] --> read-only perfect 2-slot hashing [FieldOffset(0)] - private object? keys; - - [FieldOffset(0)] - private DotvvmProperty?[] keysAsArray; + private DotvvmPropertyId[]? keys; [FieldOffset(8)] private object? values; @@ -30,50 +29,82 @@ internal struct DotvvmControlProperties : IEnumerable valuesAsDictionary; + private Dictionary valuesAsDictionary; + /// + /// flags >> 31: 1bit - ownsValues + /// flags >> 30: 1bit - ownsKeys + /// flags >> 0: 30bits - hashSeed + /// [FieldOffset(16)] - private int hashSeed; + private uint flags; + private uint hashSeed + { + readonly get => flags & 0x3F_FF_FF_FF; + set => flags = (flags & ~0x3F_FF_FF_FFu) | value; + } + private bool ownsKeys + { + readonly get => (flags >> 30) != 0; + set => flags = (flags & ~(1u << 30)) | ((uint)BoolToInt(value) << 30); + } + private bool ownsValues + { + readonly get => (flags >> 31) != 0; + set => flags = (flags & ~(1u << 31)) | ((uint)BoolToInt(value) << 31); + } - public void AssignBulk(DotvvmProperty?[] keys, object?[] values, int hashSeed) + public void AssignBulk(DotvvmPropertyId[] keys, object?[] values, uint flags) { - // The explicit layout is quite likely to mess with array covariance, just make sure we don't encounter that + // The explicit layout is quite likely to mess up with array covariance, just make sure we don't encounter that Debug.Assert(values.GetType() == typeof(object[])); - Debug.Assert(keys.GetType() == typeof(DotvvmProperty[])); + Debug.Assert(keys.GetType() == typeof(DotvvmPropertyId[])); Debug.Assert(keys.Length == values.Length); - if (this.values == null || this.keys == keys) + if (this.values == null || Object.ReferenceEquals(this.keys, keys)) { this.valuesAsArray = values; - this.keysAsArray = keys; - this.hashSeed = hashSeed; + this.keys = keys; + this.flags = flags; } else { // we can just to check if all current properties are in the proposed set // if they are not we will have to copy it - if (this.keys == null) // TODO: is this heuristic actually useful? + for (int i = 0; i < keys.Length; i++) { - var ok = true; - foreach (var x in (Dictionary)this.values) - { - var e = PropertyImmutableHashtable.FindSlot(keys, hashSeed, x.Key); - if (e < 0 || !Object.Equals(values[e], x.Value)) - ok = false; - } - if (ok) + if (keys[i].Id != 0) + this.Set(keys[i]!, values[i]); + } + } + } + + public void AssignBulk(Dictionary values, bool owns) + { + if (this.values == null || object.ReferenceEquals(this.values, values)) + { + this.keys = null; + this.valuesAsDictionary = values; + this.flags = (uint)BoolToInt(owns) << 31; + } + else + { + if (owns) + { + foreach (var (k, v) in this) { - this.values = values; - this.keys = keys; - this.hashSeed = hashSeed; - return; + values.TryAdd(k, v); } + this.values = values; + this.keys = null; + this.flags = 1u << 31; } - - for (int i = 0; i < keys.Length; i++) + else { - if (keys[i] != null) - this.Set(keys[i]!, values[i]); + foreach (var (k, v) in values) + { + this.Set(k, v); + } } } } @@ -84,181 +115,268 @@ public void ClearEverything() keys = null; } - public bool Contains(DotvvmProperty p) + public readonly bool Contains(DotvvmProperty p) => Contains(p.Id); + public readonly bool Contains(DotvvmPropertyId p) { - if (values == null) { return false; } - - if (keys == null) + if (keys is {}) + { + Debug.Assert(values is object[]); + Debug.Assert(keys is DotvvmPropertyId[]); + return PropertyImmutableHashtable.ContainsKey(this.keys, this.hashSeed, p); + } + else if (values is null) { return false; } + else { - Debug.Assert(values is Dictionary); + Debug.Assert(values is Dictionary); return valuesAsDictionary.ContainsKey(p); } + } + + public readonly bool ContainsPropertyGroup(DotvvmPropertyGroup group) => ContainsPropertyGroup(group.Id); + public readonly bool ContainsPropertyGroup(ushort groupId) + { - Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmProperty[]); - return PropertyImmutableHashtable.ContainsKey(this.keysAsArray, this.hashSeed, p); + if (keys is {}) + { + return PropertyImmutableHashtable.ContainsPropertyGroup(this.keys, groupId); + } + else if (values is null) return false; + else + { + Debug.Assert(values is Dictionary); + foreach (var key in valuesAsDictionary.Keys) + { + if (key.IsInPropertyGroup(groupId)) + return true; + } + return false; + } + } + + public readonly int CountPropertyGroup(DotvvmPropertyGroup group) => CountPropertyGroup(group.Id); + public readonly int CountPropertyGroup(ushort groupId) + { + if (keys is {}) + { + return PropertyImmutableHashtable.CountPropertyGroup(this.keys, groupId); + } + else if (values is null) return 0; + else + { + Debug.Assert(values is Dictionary); + int count = 0; + foreach (var key in valuesAsDictionary.Keys) + { + if (key.IsInPropertyGroup(groupId)) + count++; + } + return count; + } } - public bool TryGet(DotvvmProperty p, out object? value) + public readonly bool TryGet(DotvvmProperty p, out object? value) => TryGet(p.Id, out value); + public readonly bool TryGet(DotvvmPropertyId p, out object? value) { - value = null; - if (values == null) { return false; } + if (keys != null) + { + Debug.Assert(values is object[]); + Debug.Assert(keys is DotvvmPropertyId[]); + var index = PropertyImmutableHashtable.FindSlot(this.keys, this.hashSeed, p); + if (index >= 0) + { + value = this.valuesAsArray[index]; + return true; + } + else + { + value = null; + return false; + } + } - if (keys == null) + else if (values == null) { value = null; return false; } + + else { - Debug.Assert(values is Dictionary); + Debug.Assert(values is Dictionary); return valuesAsDictionary.TryGetValue(p, out value); } - - Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmProperty[]); - var index = PropertyImmutableHashtable.FindSlot(this.keysAsArray, this.hashSeed, p); - if (index != -1) - value = this.valuesAsArray[index & (this.keysAsArray.Length - 1)]; - return index != -1; } - public object? GetOrThrow(DotvvmProperty p) + public readonly object? GetOrThrow(DotvvmProperty p) => GetOrThrow(p.Id); + public readonly object? GetOrThrow(DotvvmPropertyId p) { if (this.TryGet(p, out var x)) return x; throw new KeyNotFoundException(); } - public void Set(DotvvmProperty p, object? value) + public void Set(DotvvmProperty p, object? value) => Set(p.Id, value); + public void Set(DotvvmPropertyId p, object? value) { - if (values == null) + if (p.MemberId == 0) + throw new ArgumentException("Invalid (unitialized) property id cannot be set into the DotvvmControlProperties dictionary.", nameof(p)); + + if (keys != null) + { + Debug.Assert(values is object[]); + Debug.Assert(keys is DotvvmPropertyId[]); + var slot = PropertyImmutableHashtable.FindSlotOrFree(keys, hashSeed, p, out var exists); + if (slot >= 0) + { + if (!exists) + { + OwnKeys(); + OwnValues(); + keys[slot] = p; + valuesAsArray[slot] = value; + } + else if (Object.ReferenceEquals(valuesAsArray[slot], value)) + { + // no-op, we would be changing it to the same value + } + else + { + this.OwnValues(); + valuesAsArray[slot] = value; + } + } + else + { + SwitchToDictionary(); + Debug.Assert(values is Dictionary); + valuesAsDictionary[p] = value; + keys = null; + } + } + else if (values == null) { Debug.Assert(keys == null); - var d = new Dictionary(); - d[p] = value; - this.values = d; + this.flags = 0b11u << 30; + this.keys = new DotvvmPropertyId[PropertyImmutableHashtable.AdhocTableSize]; + this.keys[0] = p; + this.valuesAsArray = new object?[PropertyImmutableHashtable.AdhocTableSize]; + this.valuesAsArray[0] = value; } - else if (keys == null) + else { - Debug.Assert(values is Dictionary); + Debug.Assert(values is Dictionary); valuesAsDictionary[p] = value; } - else + } + + /// Tries to set value into the dictionary without overwriting anything. + public bool TryAdd(DotvvmProperty p, object? value) => TryAdd(p.Id, value); + + /// Tries to set value into the dictionary without overwriting anything. + public bool TryAdd(DotvvmPropertyId p, object? value) + { + if (keys != null) { Debug.Assert(this.values is object[]); - Debug.Assert(this.keys is DotvvmProperty[]); - var keys = this.keysAsArray; - var values = this.valuesAsArray; - var slot = PropertyImmutableHashtable.FindSlot(keys, this.hashSeed, p); - if (slot >= 0 && Object.Equals(values[slot], value)) + Debug.Assert(keys is DotvvmPropertyId[]); + var slot = PropertyImmutableHashtable.FindSlotOrFree(keys, this.hashSeed, p, out var exists); + if (slot >= 0) { - // no-op, we would be changing it to the same value + if (exists) + { + // value already exists + return Object.ReferenceEquals(valuesAsArray[slot], value); + } + else + { + OwnKeys(); + OwnValues(); + // set the value + keys[slot] = p; + valuesAsArray[slot] = value; + return true; + } } else { - var d = new Dictionary(); - for (int i = 0; i < keys.Length; i++) - { - if (keys[i] != null) - d[keys[i]!] = values[i]; - } - d[p] = value; - this.valuesAsDictionary = d; - this.keys = null; + // no free slots, move to standard Dictionary + SwitchToDictionary(); + this.valuesAsDictionary.Add(p, value); + return true; } } - } - - /// Tries to set value into the dictionary without overwriting anything. - public bool TryAdd(DotvvmProperty p, object? value) - { if (values == null) { + // empty dict -> initialize 8-slot array Debug.Assert(keys == null); - var d = new Dictionary(); - d[p] = value; - this.values = d; + this.flags = 0b11u << 30; + this.keys = new DotvvmPropertyId[PropertyImmutableHashtable.AdhocTableSize]; + this.keys[0] = p; + this.valuesAsArray = new object?[PropertyImmutableHashtable.AdhocTableSize]; + this.valuesAsArray[0] = value; return true; } - else if (keys == null) + else { - Debug.Assert(values is Dictionary); + // System.Dictionary backend + Debug.Assert(values is Dictionary); #if CSharp8Polyfill if (valuesAsDictionary.TryGetValue(p, out var existingValue)) - return Object.Equals(existingValue, value); + return Object.ReferenceEquals(existingValue, value); else { valuesAsDictionary.Add(p, value); return true; } #else - if (valuesAsDictionary.TryAdd(p, value)) - return true; - else - return Object.Equals(valuesAsDictionary[p], value); + return valuesAsDictionary.TryAdd(p, value) || Object.ReferenceEquals(valuesAsDictionary[p], value); #endif } - else - { - Debug.Assert(this.values is object[]); - Debug.Assert(this.keys is DotvvmProperty[]); - var keys = this.keysAsArray; - var values = this.valuesAsArray; - var slot = PropertyImmutableHashtable.FindSlot(keys, this.hashSeed, p); - if (slot >= 0) - { - // value already exists - return Object.Equals(values[slot], value); - } - else - { - var d = new Dictionary(); - for (int i = 0; i < keys.Length; i++) - { - if (keys[i] != null) - d[keys[i]!] = values[i]; - } - d[p] = value; - this.valuesAsDictionary = d; - this.keys = null; - return true; - } - } } - public DotvvmControlPropertiesEnumerator GetEnumerator() + public readonly DotvvmControlPropertyIdEnumerator GetEnumerator() { + if (keys != null) return new DotvvmControlPropertyIdEnumerator(keys, valuesAsArray); + if (values == null) return EmptyEnumerator; - if (keys == null) return new DotvvmControlPropertiesEnumerator(valuesAsDictionary.GetEnumerator()); - return new DotvvmControlPropertiesEnumerator(this.keysAsArray, this.valuesAsArray); + + Debug.Assert(values is Dictionary); + return new DotvvmControlPropertyIdEnumerator(valuesAsDictionary.GetEnumerator()); } - IEnumerator> IEnumerable>.GetEnumerator() => + readonly IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private static DotvvmControlPropertiesEnumerator EmptyEnumerator = new DotvvmControlPropertiesEnumerator(new DotvvmProperty[0], new object[0]); + public readonly PropertyGroupEnumerable PropertyGroup(ushort groupId) => new(in this, groupId); - public bool Remove(DotvvmProperty key) - { - if (!Contains(key)) return false; - if (this.keys == null && valuesAsDictionary != null) - { - return valuesAsDictionary.Remove(key); - } + readonly public DotvvmControlPropertyIdGroupEnumerator EnumeratePropertyGroup(ushort id) => + this.keys is {} keys ? new(keys, valuesAsArray, id) : + this.values is {} ? new(valuesAsDictionary.GetEnumerator(), id) : + default; - // move from read-only struct to mutable struct - { - var keysTmp = this.keysAsArray; - var valuesTmp = this.valuesAsArray; - var d = new Dictionary(); + private static readonly DotvvmControlPropertyIdEnumerator EmptyEnumerator = new DotvvmControlPropertyIdEnumerator(Array.Empty(), Array.Empty()); - for (int i = 0; i < keysTmp.Length; i++) + public bool Remove(DotvvmProperty key) => Remove(key.Id); + public bool Remove(DotvvmPropertyId key) + { + if (this.keys != null) + { + var slot = PropertyImmutableHashtable.FindSlot(this.keys, this.hashSeed, key); + if (slot < 0) + return false; + this.OwnKeys(); + this.keys[slot] = default; + if (this.ownsValues) { - if (keysTmp[i] != null && keysTmp[i] != key) - d[keysTmp[i]!] = valuesTmp[i]; + this.valuesAsArray[slot] = default; } - this.valuesAsDictionary = d; - this.keys = null; return true; } + if (this.values == null) + return false; + else + { + Debug.Assert(values is Dictionary); + return valuesAsDictionary.Remove(key); + } } private static object? CloneValue(object? value) @@ -268,21 +386,18 @@ public bool Remove(DotvvmProperty key) return null; } - public int Count() + public readonly int Count() { if (this.values == null) return 0; if (this.keys == null) return this.valuesAsDictionary.Count; - int count = 0; - for (int i = 0; i < this.keysAsArray.Length; i++) - { - // get rid of a branch which would be almost always misspredicted - var x = this.keysAsArray[i] is not null; - count += Unsafe.As(ref x); - } - return count; + + return PropertyImmutableHashtable.Count(this.keys); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte BoolToInt(bool x) => Unsafe.As(ref x); + internal void CloneInto(ref DotvvmControlProperties newDict) { if (this.values == null) @@ -296,48 +411,240 @@ internal void CloneInto(ref DotvvmControlProperties newDict) if (dictionary.Count > 30) { newDict = this; - newDict.valuesAsDictionary = new Dictionary(dictionary); + newDict.keys = null; + newDict.valuesAsDictionary = new Dictionary(dictionary); foreach (var (key, value) in dictionary) if (CloneValue(value) is {} newValue) newDict.valuesAsDictionary[key] = newValue; return; } // move to immutable version if it's reasonably small. It will be probably cloned multiple times again - var properties = new DotvvmProperty[dictionary.Count]; - var values = new object?[properties.Length]; - int j = 0; - foreach (var x in this.valuesAsDictionary) - { - (properties[j], values[j]) = x; - j++; - } - Array.Sort(properties, values, PropertyImmutableHashtable.DotvvmPropertyComparer.Instance); - (this.hashSeed, this.keysAsArray, this.valuesAsArray) = PropertyImmutableHashtable.CreateTableWithValues(properties, values); + SwitchToPerfectHashing(); } newDict = this; + newDict.ownsKeys = false; + newDict.ownsValues = false; for (int i = 0; i < newDict.valuesAsArray.Length; i++) { if (CloneValue(newDict.valuesAsArray[i]) is {} newValue) { // clone the array if we didn't do that already if (newDict.values == this.values) + { newDict.values = this.valuesAsArray.Clone(); + newDict.ownsValues = true; + } newDict.valuesAsArray[i] = newValue; } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void OwnKeys() + { + if (this.ownsKeys) return; + CloneKeys(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void OwnValues() + { + if (this.ownsValues) return; + CloneValues(); + } + void CloneKeys() + { + var oldKeys = this.keys; + var newKeys = new DotvvmPropertyId[oldKeys!.Length]; + MemoryExtensions.CopyTo(oldKeys, newKeys.AsSpan()); + this.keys = newKeys; + this.ownsKeys = true; + } + void CloneValues() + { + if (keys is {}) + { + var oldValues = this.valuesAsArray; + var newValues = new object?[oldValues.Length]; + MemoryExtensions.CopyTo(oldValues, newValues.AsSpan()); + this.valuesAsArray = newValues; + this.ownsValues = true; + } + else if (values is null) + return; + else + { + this.valuesAsDictionary = new Dictionary(this.valuesAsDictionary); + this.flags = 1u << 31; + } + } + + /// Converts the internal representation to System.Collections.Generic.Dictionary + void SwitchToDictionary() + { + if (this.keys is {}) + { + var keysTmp = this.keys; + var valuesTmp = this.valuesAsArray; + var d = new Dictionary(capacity: keysTmp.Length); + + for (int i = 0; i < keysTmp.Length; i++) + { + if (keysTmp[i].Id != 0) + d[keysTmp[i]] = valuesTmp[i]; + } + this.valuesAsDictionary = d; + this.keys = null; + this.flags = 1u << 31; + } + else if (this.values is null) + { + // already in the dictionary + return; + } + else + { + Debug.Assert(this.values is null); + // empty state + this.valuesAsDictionary = new Dictionary(); + this.flags = 1u << 31; + } + } + + /// Converts the internal representation to the DotVVM small dictionary implementation + void SwitchToPerfectHashing() + { + if (this.keys is {}) + { + // already in the perfect hashing + return; + } + else if (this.values is {}) + { + var properties = new DotvvmPropertyId[valuesAsDictionary.Count]; + var values = new object?[properties.Length]; + int j = 0; + foreach (var x in this.valuesAsDictionary) + { + (properties[j], values[j]) = x; + j++; + } + Array.Sort(properties, values); + (this.hashSeed, this.keys, this.valuesAsArray) = PropertyImmutableHashtable.CreateTableWithValues(properties, values); + this.ownsKeys = false; + this.ownsValues = true; + } + else + { + } + } + + public readonly struct PropertyGroupEnumerable: IEnumerable> + { + private readonly DotvvmControlProperties properties; + private readonly ushort groupId; + public PropertyGroupEnumerable(in DotvvmControlProperties properties, ushort groupId) + { + this.properties = properties; + this.groupId = groupId; + } + + public IEnumerator> GetEnumerator() => properties.EnumeratePropertyGroup(groupId); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } - public struct DotvvmControlPropertiesEnumerator : IEnumerator> + public struct DotvvmControlPropertyIdGroupEnumerator : IEnumerator> + { + private DotvvmPropertyId[]? keys; + private object?[]? values; + private int index; + private ushort groupId; + private ushort bitmap; // TODO!! + private Dictionary.Enumerator dictEnumerator; + + internal DotvvmControlPropertyIdGroupEnumerator(DotvvmPropertyId[] keys, object?[] values, ushort groupId) + { + this.keys = keys; + this.values = values; + this.index = -1; + this.groupId = groupId; + this.bitmap = 0; + dictEnumerator = default; + } + + internal DotvvmControlPropertyIdGroupEnumerator(Dictionary.Enumerator e, ushort groupId) + { + this.keys = null; + this.values = null; + this.index = 0; + this.groupId = groupId; + this.dictEnumerator = e; + } + + public KeyValuePair Current => this.keys is {} keys ? new(keys[index]!, values![index]) : dictEnumerator.Current; + + object IEnumerator.Current => this.Current; + + public void Dispose() { } + + public bool MoveNext() + { + var keys = this.keys; + if (keys is {}) + { + var index = (uint)(this.index + 1); + var bitmap = this.bitmap; + while (index < keys.Length) + { + if (index % 16 == 0) + { + bitmap = PropertyImmutableHashtable.FindGroupInNext16Slots(keys, index, groupId); + } + var localIndex = BitOperations.TrailingZeroCount(bitmap); + if (localIndex < 16) + { + this.index = (int)index + localIndex; + this.bitmap = (ushort)(bitmap >> (localIndex + 1)); + return true; + } + index += 16; + } + this.index = keys.Length; + return false; + } + else + { + // `default(T)` - empty collection + if (groupId == 0) + return false; + + while (dictEnumerator.MoveNext()) + { + if (dictEnumerator.Current.Key.IsInPropertyGroup(groupId)) + return true; + } + return false; + } + } + + public void Reset() + { + if (keys == null) + ((IEnumerator)dictEnumerator).Reset(); + else index = -1; + } + } + + public struct DotvvmControlPropertyIdEnumerator : IEnumerator> { - private DotvvmProperty?[]? keys; + private DotvvmPropertyId[]? keys; private object?[]? values; private int index; - private Dictionary.Enumerator dictEnumerator; + private Dictionary.Enumerator dictEnumerator; - internal DotvvmControlPropertiesEnumerator(DotvvmProperty?[] keys, object?[] values) + internal DotvvmControlPropertyIdEnumerator(DotvvmPropertyId[] keys, object?[] values) { this.keys = keys; this.values = values; @@ -345,7 +652,7 @@ internal DotvvmControlPropertiesEnumerator(DotvvmProperty?[] keys, object?[] val dictEnumerator = default; } - internal DotvvmControlPropertiesEnumerator(in Dictionary.Enumerator e) + internal DotvvmControlPropertyIdEnumerator(Dictionary.Enumerator e) { this.keys = null; this.values = null; @@ -353,7 +660,7 @@ internal DotvvmControlPropertiesEnumerator(in Dictionary Current => keys == null ? dictEnumerator.Current : new KeyValuePair(keys[index]!, values![index]); + public KeyValuePair Current => this.keys is {} keys ? new(keys[index]!, values![index]) : dictEnumerator.Current; object IEnumerator.Current => this.Current; @@ -363,22 +670,50 @@ public void Dispose() public bool MoveNext() { - if (keys == null) + var keys = this.keys; + if (keys is null) return dictEnumerator.MoveNext(); - while (++index < keys.Length && keys[index] == null) { } + var index = this.index; + while (++index < keys.Length && keys[index].Id == 0) { } + this.index = index; return index < keys.Length; } public void Reset() { - if (keys == null) + if (keys is null) ((IEnumerator)dictEnumerator).Reset(); else index = -1; } } + public struct DotvvmControlPropertiesEnumerator : IEnumerator> + { + DotvvmControlPropertyIdEnumerator idEnumerator; + public DotvvmControlPropertiesEnumerator(DotvvmControlPropertyIdEnumerator idEnumerator) + { + this.idEnumerator = idEnumerator; + } + + public KeyValuePair Current + { + get + { + var x = idEnumerator.Current; + return new KeyValuePair(x.Key.PropertyInstance, x.Value); + } + } + + object IEnumerator.Current => this.Current; + + public void Dispose() => idEnumerator.Dispose(); + public bool MoveNext() => idEnumerator.MoveNext(); + public void Reset() => idEnumerator.Reset(); + } + public readonly struct DotvvmPropertyDictionary : IDictionary { + private readonly DotvvmBindableObject control; public DotvvmPropertyDictionary(DotvvmBindableObject control) @@ -418,13 +753,13 @@ public void Clear() public void CopyTo(KeyValuePair[] array, int arrayIndex) { - foreach (var x in control.properties) + foreach (var x in this) { array[arrayIndex++] = x; } } - public DotvvmControlPropertiesEnumerator GetEnumerator() => control.properties.GetEnumerator(); + public DotvvmControlPropertiesEnumerator GetEnumerator() => new(control.properties.GetEnumerator()); public bool Remove(DotvvmProperty key) { diff --git a/src/Framework/Framework/Controls/DotvvmControlPropertyIdGroupEnumerator.cs b/src/Framework/Framework/Controls/DotvvmControlPropertyIdGroupEnumerator.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Framework/Framework/Controls/HtmlGenericControl.cs b/src/Framework/Framework/Controls/HtmlGenericControl.cs index b441ff5c63..cb8230eb47 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -181,25 +181,25 @@ public bool RenderOnServer(HtmlGenericControl @this) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected bool TouchProperty(DotvvmProperty prop, object? value, ref RenderState r) + protected bool TouchProperty(DotvvmPropertyId prop, object? value, ref RenderState r) { - if (prop == VisibleProperty) + if (prop == VisibleProperty.Id) r.Visible = value; - else if (prop == ClientIDProperty) + else if (prop == ClientIDProperty.Id) r.ClientId = value; - else if (prop == IDProperty && value != null) + else if (prop == IDProperty.Id && value != null) r.HasId = true; - else if (prop == InnerTextProperty) + else if (prop == InnerTextProperty.Id) r.InnerText = value; - else if (prop == PostBack.UpdateProperty) + else if (prop == PostBack.UpdateProperty.Id) r.HasPostbackUpdate = (bool)this.EvalPropertyValue(prop, value)!; - else if (prop is GroupedDotvvmProperty gp) + else if (prop.IsPropertyGroup) { - if (gp.PropertyGroup == CssClassesGroupDescriptor) + if (prop.IsInPropertyGroup(CssClassesGroupDescriptor.Id)) r.HasClass = true; - else if (gp.PropertyGroup == CssStylesGroupDescriptor) + else if (prop.IsInPropertyGroup(CssStylesGroupDescriptor.Id)) r.HasStyle = true; - else if (gp.PropertyGroup == AttributesGroupDescriptor) + else if (prop.IsInPropertyGroup(AttributesGroupDescriptor.Id)) r.HasAttributes = true; else return false; } @@ -409,14 +409,10 @@ private void AddHtmlAttributesToRender(ref RenderState r, IHtmlWriter writer) { KnockoutBindingGroup? attributeBindingGroup = null; - if (r.HasAttributes) foreach (var (prop, valueRaw) in this.properties) + if (r.HasAttributes) foreach (var (attributeName, valueRaw) in this.Attributes.RawValues) { - if (prop is not GroupedDotvvmProperty gprop || gprop.PropertyGroup != AttributesGroupDescriptor) - continue; - - var attributeName = gprop.GroupMemberName; var knockoutExpression = valueRaw switch { - AttributeList list => list.GetKnockoutBindingExpression(this, HtmlWriter.GetSeparatorForAttribute(gprop.GroupMemberName)), + AttributeList list => list.GetKnockoutBindingExpression(this, HtmlWriter.GetSeparatorForAttribute(attributeName)), IValueBinding binding => binding.GetKnockoutBindingExpression(this), _ => null }; diff --git a/src/Framework/Framework/Controls/Literal.cs b/src/Framework/Framework/Controls/Literal.cs index a8f310e450..f5db466025 100644 --- a/src/Framework/Framework/Controls/Literal.cs +++ b/src/Framework/Framework/Controls/Literal.cs @@ -119,13 +119,13 @@ bool isFormattedType(Type? type) => } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TouchProperty(DotvvmProperty prop, object? value, ref RenderState r) + private bool TouchProperty(DotvvmPropertyId prop, object? value, ref RenderState r) { - if (prop == TextProperty) + if (prop == TextProperty.Id) r.Text = value; - else if (prop == RenderSpanElementProperty) + else if (prop == RenderSpanElementProperty.Id) r.RenderSpanElement = (bool)EvalPropertyValue(RenderSpanElementProperty, value)!; - else if (prop == FormatStringProperty) + else if (prop == FormatStringProperty.Id) r.HasFormattingStuff = true; else if (base.TouchProperty(prop, value, ref r.HtmlState)) { } else if (DotvvmControl.TouchProperty(prop, value, ref r.BaseState)) { } diff --git a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs index 41215deb0d..e46e2d08d1 100644 --- a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs +++ b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs @@ -3,59 +3,365 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using DotVVM.Framework.Binding; +using DotVVM.Framework.Hosting.ErrorPages; +using RecordExceptions; + +#if NET6_0_OR_GREATER +using System.Runtime.Intrinsics; +#endif namespace DotVVM.Framework.Controls { internal static class PropertyImmutableHashtable { - static int HashCombine(int a, int b) => a + b; + /// Up to this size, we don't bother with hashing as all keys can just be compared and searched with a single AVX instruction. + public const int AdhocTableSize = 8; + public const int ArrayMultipleSize = 8; + + static int HashCombine(int a, int b) => HashCode.Combine(a, b); - public static bool ContainsKey(DotvvmProperty?[] keys, int hashSeed, DotvvmProperty p) + public static bool ContainsKey(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p) { - var len = keys.Length; - if (len == 4) +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + Debug.Assert(Vector256.Count == AdhocTableSize); + var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + if (Vector256.EqualsAny(v, Vector256.Create(p.Id))) + { + return true; + } + } +#else + if (false) { } +#endif + else + { + if (keys[7].Id == p.Id || keys[6].Id == p.Id || keys[5].Id == p.Id || keys[4].Id == p.Id || keys[3].Id == p.Id || keys[2].Id == p.Id || keys[1].Id == p.Id || keys[0] == p) + { + return true; + } + } + + if (keys.Length == AdhocTableSize) { - return keys[0] == p | keys[1] == p | keys[2] == p | keys[3] == p; + return false; } - var lengthMap = len - 1; // trims the hash to be in bounds of the array - var hash = HashCombine(p.GetHashCode(), hashSeed) & lengthMap; + var hashSeed = flags & 0x3FFF_FFFF; + var lengthMap = keys.Length - 1; // trims the hash to be in bounds of the array + var hash = HashCombine(p.GetHashCode(), (int)hashSeed) & lengthMap; var i1 = hash & -2; // hash with last bit == 0 (-2 is something like ff...fe because two's complement) var i2 = hash | 1; // hash with last bit == 1 - return keys[i1] == p | keys[i2] == p; + return keys[i1].Id == p.Id | keys[i2].Id == p.Id; } - public static int FindSlot(DotvvmProperty?[] keys, int hashSeed, DotvvmProperty p) + public static int FindSlot(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p) { - var len = keys.Length; - if (len == 4) +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) { - for (int i = 0; i < 4; i++) + Debug.Assert(Vector256.Count == AdhocTableSize); + var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + if (eq != 0) { - if (keys[i] == p) return i; + return BitOperations.TrailingZeroCount(eq); } + } +#else + if (false) { } +#endif + else + { + if (keys[7].Id == p.Id) return 7; + if (keys[6].Id == p.Id) return 6; + if (keys[5].Id == p.Id) return 5; + if (keys[4].Id == p.Id) return 4; + if (keys[3].Id == p.Id) return 3; + if (keys[2].Id == p.Id) return 2; + if (keys[1].Id == p.Id) return 1; + if (keys[0].Id == p.Id) return 0; + } + + if (keys.Length == AdhocTableSize) + { return -1; } - var lengthMap = len - 1; // trims the hash to be in bounds of the array - var hash = HashCombine(p.GetHashCode(), hashSeed) & lengthMap; + var lengthMap = keys.Length - 1; // trims the hash to be in bounds of the array + var hashSeed = flags & 0x3FFF_FFFF; + var hash = HashCombine(p.GetHashCode(), (int)hashSeed) & lengthMap; var i1 = hash & -2; // hash with last bit == 0 (-2 is something like ff...fe because two's complement) var i2 = hash | 1; // hash with last bit == 1 - if (keys[i1] == p) return i1; - if (keys[i2] == p) return i2; + if (keys[i1].Id == p.Id) return i1; + if (keys[i2].Id == p.Id) return i2; return -1; } - static ConcurrentDictionary tableCache = new ConcurrentDictionary(new EqCmp()); + public static int FindSlotOrFree(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p, out bool exists) + { + int free = -1; + exists = true; + +#if NET8_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + if (eq != 0) + { + return BitOperations.TrailingZeroCount(eq); + } + var eq0 = Vector256.Equals(v, Vector256.Create(0u)).ExtractMostSignificantBits(); + if (eq0 != 0) + { + free = BitOperations.TrailingZeroCount(eq0); + } + } +#else + if (false) { } +#endif + else + { + if (keys[7].Id == p.Id) return 7; + if (keys[6].Id == p.Id) return 6; + if (keys[5].Id == p.Id) return 5; + if (keys[4].Id == p.Id) return 4; + if (keys[3].Id == p.Id) return 3; + if (keys[2].Id == p.Id) return 2; + if (keys[1].Id == p.Id) return 1; + if (keys[0].Id == p.Id) return 0; + if (keys[7].Id == 0) free = 7; + else if (keys[6].Id == 0) free = 6; + else if (keys[5].Id == 0) free = 5; + else if (keys[4].Id == 0) free = 4; + else if (keys[3].Id == 0) free = 3; + else if (keys[2].Id == 0) free = 2; + else if (keys[1].Id == 0) free = 1; + else if (keys[0].Id == 0) free = 0; + } + + if (keys.Length == 8) + { + exists = false; + return free; + } + + var lengthMap = keys.Length - 1; // trims the hash to be in bounds of the array + var hashSeed = flags & 0x3FFF_FFFF; + var hash = HashCombine(p.GetHashCode(), (int)hashSeed) & lengthMap; - class EqCmp : IEqualityComparer + var i1 = hash & -2; // hash with last bit == 0 (-2 is something like ff...fe because two's complement) + var i2 = hash | 1; // hash with last bit == 1 + + if (keys[i1].Id == p.Id) return i1; + if (keys[i2].Id == p.Id) return i2; + exists = false; + if (keys[i1].Id == 0) return i1; + if (keys[i2].Id == 0) return i2; + return free; + } + + + public static int FindFreeAdhocSlot(DotvvmPropertyId[] keys) + { + Debug.Assert(keys.Length >= AdhocTableSize); +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + Debug.Assert(Vector256.Count == AdhocTableSize); + var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + var eq = Vector256.Equals(v, Vector256.Create(0u)).ExtractMostSignificantBits(); + if (eq != 0) + { + return BitOperations.TrailingZeroCount(eq); + } + } +#else + if (false) { } +#endif + else + { + if (keys[7].Id == 0) return 7; + if (keys[6].Id == 0) return 6; + if (keys[5].Id == 0) return 5; + if (keys[4].Id == 0) return 4; + if (keys[3].Id == 0) return 3; + if (keys[2].Id == 0) return 2; + if (keys[1].Id == 0) return 1; + if (keys[0].Id == 0) return 0; + } + return -1; + } + + public static ushort FindGroupInNext16Slots(DotvvmPropertyId[] keys, uint startIndex, ushort groupId) { - public bool Equals(DotvvmProperty[]? x, DotvvmProperty[]? y) + Debug.Assert(keys.Length % ArrayMultipleSize == 0); + Debug.Assert(keys.Length >= AdhocTableSize); + + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; + ushort bitmap = 0; + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + var v1 = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, (int)startIndex))); + bitmap = (ushort)Vector256.Equals(v1 >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits(); + if (keys.Length > startIndex + 8) + { + var v2 = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, (int)startIndex + 8))); + bitmap |= (ushort)(Vector256.Equals(v2 >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits() << 8); + } + } +#else + if (false) { } +#endif + else + { + bitmap |= (ushort)((keys[startIndex + 7].TypeId == idPrefix ? 1 : 0) << 7); + bitmap |= (ushort)((keys[startIndex + 6].TypeId == idPrefix ? 1 : 0) << 6); + bitmap |= (ushort)((keys[startIndex + 5].TypeId == idPrefix ? 1 : 0) << 5); + bitmap |= (ushort)((keys[startIndex + 4].TypeId == idPrefix ? 1 : 0) << 4); + bitmap |= (ushort)((keys[startIndex + 3].TypeId == idPrefix ? 1 : 0) << 3); + bitmap |= (ushort)((keys[startIndex + 2].TypeId == idPrefix ? 1 : 0) << 2); + bitmap |= (ushort)((keys[startIndex + 1].TypeId == idPrefix ? 1 : 0) << 1); + bitmap |= (ushort)((keys[startIndex + 0].TypeId == idPrefix ? 1 : 0) << 0); + if (keys.Length > startIndex + 8) + { + bitmap |= (ushort)((keys[startIndex + 15].TypeId == idPrefix ? 1 : 0) << 15); + bitmap |= (ushort)((keys[startIndex + 14].TypeId == idPrefix ? 1 : 0) << 14); + bitmap |= (ushort)((keys[startIndex + 13].TypeId == idPrefix ? 1 : 0) << 13); + bitmap |= (ushort)((keys[startIndex + 12].TypeId == idPrefix ? 1 : 0) << 12); + bitmap |= (ushort)((keys[startIndex + 11].TypeId == idPrefix ? 1 : 0) << 11); + bitmap |= (ushort)((keys[startIndex + 10].TypeId == idPrefix ? 1 : 0) << 10); + bitmap |= (ushort)((keys[startIndex + 9].TypeId == idPrefix ? 1 : 0) << 9); + bitmap |= (ushort)((keys[startIndex + 8].TypeId == idPrefix ? 1 : 0) << 8); + } + } + return bitmap; + } + + public static bool ContainsPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) + { + Debug.Assert(keys.Length % ArrayMultipleSize == 0 && Vector256.Count == ArrayMultipleSize); + Debug.Assert(keys.Length >= AdhocTableSize); + + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + Debug.Assert(keys.Length % 8 == 0); + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + for (int i = 0; i < keys.Length; i += 8) + { + var v = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, i))); + if (Vector256.EqualsAny(v >> 16, Vector256.Create((uint)idPrefix))) + { + return true; + } + } + return false; + } +#else + if (false) { } +#endif + else + { + for (int i = 0; i < keys.Length; i++) + { + if (keys[i].TypeId == idPrefix) + { + return true; + } + } + return false; + } + } + + public static int Count(DotvvmPropertyId[] keys) + { + Debug.Assert(keys.Length % ArrayMultipleSize == 0 && Vector256.Count == ArrayMultipleSize); + Debug.Assert(keys.Length >= AdhocTableSize); + + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + Debug.Assert(keys.Length % 8 == 0); + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + int zeroCount = 0; + for (int i = 0; i < keys.Length; i += Vector256.Count) + { + var v = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, i))); + var isZero = Vector256.Equals(v, Vector256.Create(0u)).ExtractMostSignificantBits(); + zeroCount += BitOperations.PopCount(isZero); + } + return keys.Length - zeroCount; + } +#else + if (false) { } +#endif + else + { + int count = 0; + for (int i = 0; i < keys.Length; i++) + { + count += BoolToInt(keys[i].Id == 0); + } + return count; + } + + } + + public static int CountPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) + { + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + Debug.Assert(keys.Length % ArrayMultipleSize == 0); + + int count = 0; + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + for (int i = 0; i < keys.Length; i += Vector256.Count) + { + var v = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, i))); + count += BitOperations.PopCount(Vector256.Equals(v >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits()); + } + } +#else + if (false) { } +#endif + else + { + for (int i = 0; i < keys.Length; i++) + { + count += BoolToInt(keys[i].TypeId == idPrefix); + } + } + return count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte BoolToInt(bool x) => Unsafe.As(ref x); + + static ConcurrentDictionary tableCache = new(new EqCmp()); + + class EqCmp : IEqualityComparer + { + public bool Equals(DotvvmPropertyId[]? x, DotvvmPropertyId[]? y) { if (object.ReferenceEquals(x, y)) return true; if (x == null || y == null) return false; @@ -66,7 +372,7 @@ public bool Equals(DotvvmProperty[]? x, DotvvmProperty[]? y) return true; } - public int GetHashCode(DotvvmProperty[] obj) + public int GetHashCode(DotvvmPropertyId[] obj) { var h = obj.Length; foreach (var i in obj) @@ -76,25 +382,44 @@ public int GetHashCode(DotvvmProperty[] obj) } // Some primes. Numbers not divisible by 2 should help shuffle the table in a different way every time. - public static int[] hashSeeds = new [] {0, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541}; + public static uint[] hashSeeds = [0, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541]; - public static (int hashSeed, DotvvmProperty?[] keys) BuildTable(DotvvmProperty[] a) + private static bool IsOrderedWithoutDuplicatesAndZero(DotvvmPropertyId[] keys) { - Debug.Assert(a.OrderBy(x => x.FullName, StringComparer.Ordinal).SequenceEqual(a)); + uint last = 0; + foreach (var k in keys) + { + if (k.Id <= last) return false; + if (k.MemberId == 0) return false; + last = k.Id; + } + return true; + } + + public static (uint hashSeed, DotvvmPropertyId[] keys) BuildTable(DotvvmPropertyId[] keys) + { + if (!IsOrderedWithoutDuplicatesAndZero(keys)) + { + throw new ArgumentException("Keys must be ordered, without duplicates and without zero.", nameof(keys)); + } // make sure that all tables have the same keys so that they don't take much RAM (and remain in cache and make things go faster) - return tableCache.GetOrAdd(a, keys => { - if (keys.Length < 4) + return tableCache.GetOrAdd(keys, static keys => { + if (keys.Length <= 8) { // just pad them to make things regular - var result = new DotvvmProperty[4]; + var result = new DotvvmPropertyId[8]; Array.Copy(keys, result, keys.Length); return (0, result); } else { // first try closest size of power two - var size = 1 << (int)Math.Log(keys.Length + 1, 2); + var size = 1 << (int)Math.Ceiling(Math.Log(keys.Length, 2)); + + // all vector optimizations assume length at least 8 + size = Math.Max(size, AdhocTableSize); + Debug.Assert(size % ArrayMultipleSize == 0); while(true) { @@ -111,40 +436,40 @@ public static (int hashSeed, DotvvmProperty?[] keys) BuildTable(DotvvmProperty[] size *= 2; - if (size <= 4) throw new InvalidOperationException("Could not build hash table"); + if (size <= 4) throw new CannotMakeHashtableException(); } } }); } - static bool TestTableCorrectness(DotvvmProperty[] keys, int hashSeed, DotvvmProperty?[] table) + static bool TestTableCorrectness(DotvvmPropertyId[] keys, uint hashSeed, DotvvmPropertyId[] table) { - return keys.All(k => FindSlot(table, hashSeed, k) >= 0); + return keys.All(k => FindSlot(table, hashSeed, k) >= 0) && keys.Select(k => FindSlot(table, hashSeed, k)).Distinct().Count() == keys.Length; } /// Builds the core of the property hash table. Returns null if the table cannot be built due to collisions. - static DotvvmProperty?[]? TryBuildTable(DotvvmProperty[] a, int size, int hashSeed) + static DotvvmPropertyId[]? TryBuildTable(DotvvmPropertyId[] a, int size, uint hashSeed) { - var t = new DotvvmProperty?[size]; - var lengthMap = (size) - 1; // trims the hash to be in bounds of the array + var t = new DotvvmPropertyId[size]; + var lengthMap = size - 1; // trims the hash to be in bounds of the array foreach (var k in a) { - var hash = HashCombine(k.GetHashCode(), hashSeed) & lengthMap; + var hash = HashCombine(k.GetHashCode(), (int)hashSeed) & lengthMap; var i1 = hash & -2; // hash with last bit == 0 (-2 is something like ff...fe because two's complement) var i2 = hash | 1; // hash with last bit == 1 - if (t[i1] == null) + if (t[i1].IsZero) t[i1] = k; - else if (t[i2] == null) + else if (t[i2].IsZero) t[i2] = k; else return null; // if neither of these slots work, we can't build the table } return t; } - public static (int hashSeed, DotvvmProperty?[] keys, T[] valueTable) CreateTableWithValues(DotvvmProperty[] properties, T[] values) + public static (uint hashSeed, DotvvmPropertyId[] keys, T[] valueTable) CreateTableWithValues(DotvvmPropertyId[] properties, T[] values) { var (hashSeed, keys) = BuildTable(properties); var valueTable = new T[keys.Length]; @@ -155,18 +480,43 @@ public static (int hashSeed, DotvvmProperty?[] keys, T[] valueTable) CreateTable return (hashSeed, keys, valueTable); } - public static Action CreateBulkSetter(DotvvmProperty[] properties, object[] values) + public static Action CreateBulkSetter(DotvvmProperty[] properties, object?[] values) { - var (hashSeed, keys, valueTable) = CreateTableWithValues(properties, values); - return (obj) => obj.properties.AssignBulk(keys, valueTable, hashSeed); + var ids = properties.Select(p => p.Id).ToArray(); + Array.Sort(ids); + return CreateBulkSetter(ids, values); + } + public static Action CreateBulkSetter(DotvvmPropertyId[] properties, object?[] values) + { + if (properties.Length > 30) + { + var dict = new Dictionary(capacity: properties.Length); + for (int i = 0; i < properties.Length; i++) + { + dict[properties[i]] = values[i]; + } + return (obj) => obj.properties.AssignBulk(dict, false); + } + else + { + var (hashSeed, keys, valueTable) = CreateTableWithValues(properties, values); + return (obj) => obj.properties.AssignBulk(keys, valueTable, hashSeed); + } } - public class DotvvmPropertyComparer : IComparer + public static void SetValuesToDotvvmControl(DotvvmBindableObject obj, DotvvmPropertyId[] properties, object?[] values, uint flags) { - public int Compare(DotvvmProperty? a, DotvvmProperty? b) => - string.Compare(a?.FullName, b?.FullName, StringComparison.Ordinal); + obj.properties.AssignBulk(properties, values, flags); + } - public static readonly DotvvmPropertyComparer Instance = new(); + public static void SetValuesToDotvvmControl(DotvvmBindableObject obj, Dictionary values, bool owns) + { + obj.properties.AssignBulk(values, owns); + } + + public record CannotMakeHashtableException: RecordException + { + public override string Message => "Cannot make hashtable"; } } } diff --git a/src/Framework/Framework/Controls/RouteLink.cs b/src/Framework/Framework/Controls/RouteLink.cs index f4044a96c6..7beabaf2ac 100644 --- a/src/Framework/Framework/Controls/RouteLink.cs +++ b/src/Framework/Framework/Controls/RouteLink.cs @@ -139,6 +139,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest if (GetValue(EnabledProperty) == false) { writer.AddAttribute("disabled", "disabled"); + // this.CssClasses["a"] = true; if (enabledBinding is null) WriteEnabledBinding(writer, false); diff --git a/src/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index fef60275b3..3f26b2a6c5 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -356,5 +356,20 @@ public void DotvvmProperty_CheckCorrectValueInDataBinding() } } } + + [TestMethod] + public void DotvvmProperty_ManyItemsSetter() + { + var properties = Enumerable.Range(0, 1000).Select(i => HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-" + i.ToString())).ToArray(); + + var setter = PropertyImmutableHashtable.CreateBulkSetter(properties, Enumerable.Range(0, 1000).Select(i => (object?)i).ToArray()); + + var control1 = new HtmlGenericControl("div"); + setter(control1); + var control2 = new HtmlGenericControl("div"); + setter(control2); + + Assert.AreEqual(1000, control1.Properties.Count); + } } } diff --git a/src/Tests/Runtime/PropertyGroupTests.cs b/src/Tests/Runtime/PropertyGroupTests.cs new file mode 100644 index 0000000000..8e68d8d231 --- /dev/null +++ b/src/Tests/Runtime/PropertyGroupTests.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using DotVVM.Framework.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotVVM.Framework.Tests.Runtime +{ + [TestClass] + public class PropertyGroupTests + { + [TestMethod] + public void PGroup_Enumerate() + { + var el = new HtmlGenericControl("div"); + el.Attributes.Add("a", "1"); + el.Attributes.Add("b", "2"); + el.Attributes.Add("c", "3"); + + var expected = new[] { "1", "2", "3" }; + XAssert.Equal(expected, el.Attributes.Select(p => p.Value).OrderBy(a => a)); + XAssert.Equal(expected, el.Attributes.RawValues.Select(p => p.Value).OrderBy(a => a)); + XAssert.Equal(expected, el.Attributes.Values.OrderBy(a => a)); + XAssert.Equal(expected, el.Attributes.Properties.Select(p => el.GetValue(p)).OrderBy(a => a)); + XAssert.Equal(expected, el.Attributes.Keys.Select(k => el.Attributes[k]).OrderBy(a => a)); + XAssert.Equal(["a", "b", "c"], el.Attributes.Keys.OrderBy(a => a)); + } + + [TestMethod] + public void PGroup_AddMergeValues() + { + var el = new HtmlGenericControl("div"); + el.Attributes.Add("a", "1"); + el.Attributes.Add("a", "2"); + + XAssert.Equal("1;2", el.Attributes["a"]); + + el.Attributes.Add("class", "c1"); + el.Attributes.Add("class", "c2"); + + XAssert.Equal("c1 c2", el.Attributes["class"]); + + el.Attributes.Add("data-bind", "a: 1"); + el.Attributes.Add("data-bind", "b: 2"); + + XAssert.Equal("a: 1,b: 2", el.Attributes["data-bind"]); + } + } +}