From 70e5a7a745fb038071dc1f930c6e939af14f7cd2 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 01/11] 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. --- .../Framework/Binding/ActiveDotvvmProperty.cs | 4 + .../Binding/CompileTimeOnlyDotvvmProperty.cs | 4 +- .../Binding/DelegateActionProperty.cs | 7 +- ...DotvvmCapabilityProperty.CodeGeneration.cs | 34 +- .../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 | 681 +++++++++++++----- .../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 ++ 32 files changed, 2121 insertions(+), 515 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/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 a0b7500aad..7472a30342 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,13 +207,13 @@ 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)); + getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), [ 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)); getValue = Convert(getValue, type); } return ( diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs index a276c1b989..0375742a28 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs @@ -12,7 +12,7 @@ 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)) { @@ -25,13 +25,13 @@ internal static class Helpers } 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)) { @@ -45,42 +45,42 @@ public static ValueOrBinding GetValueOrBinding(DotvvmBindableObject c, Dot } 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 } @@ -90,15 +90,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 { // T being a struct allows us to invert the rather expensive `is IBinding` typecheck in EvalPropertyValue @@ -111,11 +111,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 c6846b28af..dfcb6b23fd 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 5bdfc5ef21..7d3bccef59 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) { @@ -123,12 +127,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 @@ -137,7 +153,8 @@ internal DotvvmProperty( Type declaringType, object? defaultValue, bool isValueInherited, - ICustomAttributeProvider attributeProvider + ICustomAttributeProvider attributeProvider, + DotvvmPropertyId id = default ) { this.Name = name ?? throw new ArgumentNullException(nameof(name)); @@ -146,6 +163,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); } @@ -162,6 +182,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; @@ -258,14 +295,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); } @@ -288,7 +343,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)) @@ -373,8 +432,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); @@ -396,6 +455,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) @@ -409,25 +470,27 @@ 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.MarkupOptions.AllowResourceBinding = !typeof(IValueBinding).IsAssignableFrom(property.PropertyType); } + + 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 bf06d6d635..54f8b24f25 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 System.Text.Json; using DotVVM.Framework.Binding.Expressions; @@ -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 db88f85242..a45e954028 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.Runtime.ExplicitAssemblyLoading.Enabled) - { + var dotvvmAssembly = typeof(DotvvmControl).Assembly.GetName().Name!; + + var assemblies = configuration.Runtime.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 96c84f10cb..dad95f81c7 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Collections.Immutable; using System.Threading; +using DotVVM.Framework.Binding.Expressions; namespace DotVVM.Framework.Compilation.ControlTree { @@ -39,8 +40,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; } @@ -62,7 +65,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?) @@ -93,7 +98,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 e927a514d6..59e2d596bc 100644 --- a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs +++ b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs @@ -53,7 +53,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( @@ -82,7 +82,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 d0dd7252f9..a6ac5f473a 100644 --- a/src/Framework/Framework/Controls/CompositeControl.cs +++ b/src/Framework/Framework/Controls/CompositeControl.cs @@ -155,10 +155,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) @@ -181,10 +181,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); @@ -194,7 +194,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) { @@ -202,7 +202,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 b42b8e20db..1aab689860 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; @@ -123,6 +124,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. /// @@ -137,10 +188,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 d973295d26..72088d67a7 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs @@ -224,9 +224,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. @@ -235,7 +235,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 c9db28e7e0..f09f0f7202 100644 --- a/src/Framework/Framework/Controls/DotvvmControl.cs +++ b/src/Framework/Framework/Controls/DotvvmControl.cs @@ -231,16 +231,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; } @@ -263,20 +266,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 5a254b4b10..c5b6d4811d 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 Dictionary); + 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); return valuesAsDictionary.ContainsKey(p); } + } - Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmProperty[]); - return PropertyImmutableHashtable.ContainsKey(this.keysAsArray, this.hashSeed, p); + public readonly bool ContainsPropertyGroup(DotvvmPropertyGroup group) => ContainsPropertyGroup(group.Id); + public readonly bool ContainsPropertyGroup(ushort groupId) + { + + 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 bool TryGet(DotvvmProperty p, out object? value) + public readonly int CountPropertyGroup(DotvvmPropertyGroup group) => CountPropertyGroup(group.Id); + public readonly int CountPropertyGroup(ushort groupId) { - value = null; - if (values == null) { return false; } + 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; + } + } - if (keys == null) + public readonly bool TryGet(DotvvmProperty p, out object? value) => TryGet(p.Id, out value); + public readonly bool TryGet(DotvvmPropertyId p, out object? value) + { + if (keys != null) { - Debug.Assert(values is Dictionary); - return valuesAsDictionary.TryGetValue(p, out value); + 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; + } } - 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; + else if (values == null) { value = null; return false; } + + else + { + Debug.Assert(values is Dictionary); + return valuesAsDictionary.TryGetValue(p, out value); + } } - 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 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 DotvvmControlPropertiesEnumerator : IEnumerator> + 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) @@ -420,13 +755,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) { @@ -461,14 +796,14 @@ public void CopyTo(DotvvmProperty[] array, int arrayIndex) { foreach (var x in control.properties) { - array[arrayIndex++] = x.Key; + array[arrayIndex++] = x.Key.PropertyInstance; } } public IEnumerator GetEnumerator() { foreach (var x in control.properties) { - yield return x.Key; + yield return x.Key.PropertyInstance; } } public bool Remove(DotvvmProperty item) => throw new NotSupportedException("Explicitly use control.Properties.Remove() instead."); 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 c2d69ea937..a0c795f071 100644 --- a/src/Framework/Framework/Controls/Literal.cs +++ b/src/Framework/Framework/Controls/Literal.cs @@ -118,13 +118,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"]); + } + } +} From 767ddbdefd557c4dfed896a9e669515eafbf62bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 7 Dec 2024 09:12:23 +0100 Subject: [PATCH 02/11] DotvvmPropertyId: encode in ID if we can use direct dict access or virtual GetValue This is now frequently needed when working only with the ID instead of working with DotvvmProperty. Without this information, we pay not only for virtual dispatch, but also the (more expensive) cost of looking up the DotvvmProperty instance. Since most properties use plain DotvvmProperty and don't need the polymorphism, we now avoid this cost by using the last bit of the property ID to indicate whether the property can use direct access. --- ...DotvvmCapabilityProperty.CodeGeneration.cs | 6 +- .../Framework/Binding/DotvvmProperty.cs | 21 +++- .../Binding/DotvvmPropertyIdAssignment.cs | 105 +++++++++++++++--- .../Controls/DotvvmControlProperties.cs | 2 +- .../DotVVMServiceCollectionExtensions.cs | 2 +- 5 files changed, 106 insertions(+), 30 deletions(-) diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs index 7472a30342..6c3d6c737b 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs @@ -116,11 +116,7 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA // if the property does not override GetValue/SetValue, we'll use // control.properties dictionary directly to avoid virtual method calls - var canUseDirectAccess = - !property.IsValueInherited && ( - property.GetType() == typeof(DotvvmProperty) || - property.GetType().GetMethod(nameof(DotvvmProperty.GetValue), new [] { typeof(DotvvmBindableObject), typeof(bool) })!.DeclaringType == typeof(DotvvmProperty) && - property.GetType().GetMethod(nameof(DotvvmProperty.SetValue), new [] { typeof(DotvvmBindableObject), typeof(object) })!.DeclaringType == typeof(DotvvmProperty)); + var canUseDirectAccess = !property.IsValueInherited && DotvvmPropertyIdAssignment.TypeCanUseAnyDirectAccess(property.GetType()); var valueParameter = Expression.Parameter(type, "value"); var unwrappedType = type.UnwrapNullableType(); diff --git a/src/Framework/Framework/Binding/DotvvmProperty.cs b/src/Framework/Framework/Binding/DotvvmProperty.cs index 7d3bccef59..61073d36f4 100644 --- a/src/Framework/Framework/Binding/DotvvmProperty.cs +++ b/src/Framework/Framework/Binding/DotvvmProperty.cs @@ -211,7 +211,7 @@ public bool IsOwnedByCapability(DotvvmCapabilityProperty capability) => { for (var p = control.Parent; p is not null; p = p.Parent) { - if (p.properties.TryGet(this, out var v)) + if (p.properties.TryGet(Id, out var v)) return v; } return DefaultValue; @@ -222,7 +222,7 @@ public bool IsOwnedByCapability(DotvvmCapabilityProperty capability) => /// public virtual object? GetValue(DotvvmBindableObject control, bool inherit = true) { - if (control.properties.TryGet(this, out var value)) + if (control.properties.TryGet(Id, out var value)) { return value; } @@ -233,20 +233,29 @@ public bool IsOwnedByCapability(DotvvmCapabilityProperty capability) => return DefaultValue; } + private bool IsSetInHierarchy(DotvvmBindableObject control) + { + for (var p = control.Parent; p is not null; p = p.Parent) + { + if (p.properties.Contains(Id)) + return true; + } + return false; + } /// /// Gets whether the value of the property is set /// public virtual bool IsSet(DotvvmBindableObject control, bool inherit = true) { - if (control.properties.Contains(this)) + if (control.properties.Contains(Id)) { return true; } - if (IsValueInherited && inherit && control.Parent != null) + if (IsValueInherited && inherit) { - return IsSet(control.Parent); + return IsSetInHierarchy(control); } return false; @@ -258,7 +267,7 @@ public virtual bool IsSet(DotvvmBindableObject control, bool inherit = true) /// public virtual void SetValue(DotvvmBindableObject control, object? value) { - control.properties.Set(this, value); + control.properties.Set(Id, value); } /// diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs index 9cf5aad070..ad93bd5e05 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -9,6 +9,7 @@ using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Controls; using DotVVM.Framework.Controls.Infrastructure; +using FastExpressionCompiler; namespace DotVVM.Framework.Binding { @@ -31,6 +32,19 @@ public DotvvmPropertyId(ushort typeOrGroupId, ushort memberId) public ushort GroupId => (ushort)((Id >> 16) ^ 0x80_00); public ushort MemberId => (ushort)(Id & 0xFFFF); + /// Returns true if the property does not have GetValue/SetValue overrides and is not inherited. That means it is sufficient + public bool CanUseFastAccessors + { + get + { + // properties: we encode this information as the LSB bit of the member ID (i.e. odd/even numbers) + // property groups: always true, i.e. + const uint mask = (1u << 31) | (1u); + const uint targetValue = 1u; + return (Id & mask) != targetValue; + } + } + public bool IsZero => Id == 0; public DotvvmProperty PropertyInstance => DotvvmPropertyIdAssignment.GetProperty(Id) ?? throw new Exception($"Property with ID {Id} not registered."); @@ -105,16 +119,16 @@ static DotvvmPropertyIdAssignment() #region Optimized metadata accessors public static bool IsInherited(DotvvmPropertyId propertyId) { - if (propertyId.IsPropertyGroup) + if (propertyId.CanUseFastAccessors) return false; + return BitmapRead(controls[propertyId.TypeId].inheritedBitmap, propertyId.MemberId); } public static bool UsesStandardAccessors(DotvvmPropertyId propertyId) { - if (propertyId.IsPropertyGroup) + if (propertyId.CanUseFastAccessors) { - // property groups can't override GetValue, otherwise VirtualPropertyGroupDictionary wouldn't work either return true; } else @@ -191,9 +205,8 @@ public static bool IsActive(DotvvmPropertyId propertyId) public static object? GetValueRaw(DotvvmBindableObject obj, DotvvmPropertyId id, bool inherit = true) { - if (id.IsPropertyGroup) + if (id.CanUseFastAccessors) { - // property groups can't override GetValue if (obj.properties.TryGet(id, out var value)) return value; @@ -201,7 +214,6 @@ public static bool IsActive(DotvvmPropertyId propertyId) } 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); } @@ -300,16 +312,37 @@ public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) if (property.GetType() == typeof(GroupedDotvvmProperty)) throw new ArgumentException("RegisterProperty cannot be called with GroupedDotvvmProperty!"); + var typeCanUseDirectAccess = TypeCanUseAnyDirectAccess(property.GetType()); + var canUseDirectAccess = !property.IsValueInherited && typeCanUseDirectAccess; + var typeId = RegisterType(property.DeclaringType); ref ControlTypeInfo control = ref controls[typeId]; - lock (control.locker) + lock (control.locker) // single control registrations are sequential anyway { - var id = ++control.counter; + uint id; + if (canUseDirectAccess) + { + control.counterStandard += 1; + id = control.counterStandard * 2; + } + else + { + control.counterNonStandard += 1; + id = control.counterNonStandard * 2 + 1; + } if (id > ushort.MaxValue) - throw new Exception("Too many properties registered for a single control type."); + ThrowTooManyException(property); + + // resize arrays (we hold a write lock, but others may be reading in parallel) if (id >= control.properties.Length) { VolatileResize(ref control.properties, control.properties.Length * 2); + } + if (id / 64 >= control.inheritedBitmap.Length) + { + Debug.Assert(control.inheritedBitmap.Length == control.standardBitmap.Length); + Debug.Assert(control.inheritedBitmap.Length == control.activeBitmap.Length); + VolatileResize(ref control.inheritedBitmap, control.inheritedBitmap.Length * 2); VolatileResize(ref control.standardBitmap, control.standardBitmap.Length * 2); VolatileResize(ref control.activeBitmap, control.activeBitmap.Length * 2); @@ -317,7 +350,7 @@ public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) if (property.IsValueInherited) BitmapSet(control.inheritedBitmap, (uint)id); - if (property.GetType() == typeof(DotvvmProperty)) + if (typeCanUseDirectAccess) BitmapSet(control.standardBitmap, (uint)id); if (property is ActiveDotvvmProperty) BitmapSet(control.activeBitmap, (uint)id); @@ -325,6 +358,39 @@ public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) control.properties[id] = property; return new DotvvmPropertyId(typeId, (ushort)id); } + + static void ThrowTooManyException(DotvvmProperty property) => + throw new Exception($"Too many properties registered for a single control type ({property.DeclaringType.ToCode()}). Trying to register property '{property.Name}: {property.PropertyType.ToCode()}'"); + } + + private static readonly ConcurrentDictionary cacheTypeCanUseDirectAccess = new(concurrencyLevel: 1, capacity: 10); + /// Does the property use the default GetValue/SetValue methods? + public static (bool getter, bool setter) TypeCanUseDirectAccess(Type propertyType) + { + if (propertyType == typeof(DotvvmProperty) || propertyType == typeof(GroupedDotvvmProperty)) + return (true, true); + + if (propertyType == typeof(DotvvmCapabilityProperty) || propertyType == typeof(DotvvmPropertyAlias) || propertyType == typeof(CompileTimeOnlyDotvvmProperty)) + return (false, false); + + if (propertyType.IsGenericType) + { + propertyType = propertyType.GetGenericTypeDefinition(); + if (propertyType == typeof(DelegateActionProperty<>)) + return (true, true); + } + + return cacheTypeCanUseDirectAccess.GetOrAdd(propertyType, static t => + { + var getter = t.GetMethod(nameof(DotvvmProperty.GetValue), new [] { typeof(DotvvmBindableObject), typeof(bool) })!.DeclaringType == typeof(DotvvmProperty); + var setter = t.GetMethod(nameof(DotvvmProperty.SetValue), new [] { typeof(DotvvmBindableObject), typeof(object) })!.DeclaringType == typeof(DotvvmProperty); + return (getter, setter); + }); + } + public static bool TypeCanUseAnyDirectAccess(Type propertyType) + { + var (getter, setter) = TypeCanUseDirectAccess(propertyType); + return getter && setter; } public static ushort RegisterPropertyGroup(DotvvmPropertyGroup group) @@ -351,10 +417,10 @@ public static ushort RegisterPropertyGroup(DotvvmPropertyGroup group) /// 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; + var localRef = array; + var newArray = new T[newSize]; + localRef.AsSpan().CopyTo(newArray.AsSpan(0, localRef.Length)); + Volatile.Write(ref array, newArray); } #endregion Registration @@ -423,13 +489,14 @@ static void BitmapSet(ulong[] bitmap, uint index) 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 object locker; + public Type controlType; + public uint counterStandard; + public uint counterNonStandard; } public static class GroupMembers @@ -461,5 +528,9 @@ public static class TypeIds // public const short Internal = 4; } + public static class PropertyIds + { + public const uint DotvvmBindableObject_DataContext = TypeIds.DotvvmBindableObject << 16 | 1; + } } } diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index c5b6d4811d..439d426dbb 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -180,7 +180,7 @@ public readonly bool TryGet(DotvvmPropertyId p, out object? value) if (keys != null) { Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmPropertyId[]); + Debug.Assert(keys is not null); var index = PropertyImmutableHashtable.FindSlot(this.keys, this.hashSeed, p); if (index >= 0) { diff --git a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index fd08c1b49a..9a207bc696 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -122,7 +122,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi var requiredResourceControl = controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(RequiredResource))); o.TreeVisitors.Add(() => new StyleTreeShufflingVisitor(controlResolver)); o.TreeVisitors.Add(() => new ControlPrecompilationVisitor(s)); - o.TreeVisitors.Add(() => new LiteralOptimizationVisitor()); + // o.TreeVisitors.Add(() => new LiteralOptimizationVisitor()); o.TreeVisitors.Add(() => new BindingRequiredResourceVisitor((ControlResolverMetadata)requiredResourceControl)); var requiredGlobalizeControl = controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(GlobalizeResource))); o.TreeVisitors.Add(() => new GlobalizeResourceVisitor((ControlResolverMetadata)requiredGlobalizeControl)); From ff1dbd3b64dd4200d0af92f8b61a97a22cf434d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 7 Dec 2024 23:07:18 +0100 Subject: [PATCH 03/11] Fix leaking of dictionary changes between cloned controls --- .../Controls/DotvvmControlProperties.cs | 38 +++++++++++-- .../DotVVMServiceCollectionExtensions.cs | 2 +- src/Tests/Runtime/DotvvmPropertyTests.cs | 55 +++++++++++++++++++ 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index 439d426dbb..c1c74b9df5 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -45,12 +45,12 @@ private uint hashSeed } private bool ownsKeys { - readonly get => (flags >> 30) != 0; + readonly get => ((flags >> 30) & 1) != 0; set => flags = (flags & ~(1u << 30)) | ((uint)BoolToInt(value) << 30); } private bool ownsValues { - readonly get => (flags >> 31) != 0; + readonly get => ((flags >> 31) & 1) != 0; set => flags = (flags & ~(1u << 31)) | ((uint)BoolToInt(value) << 31); } @@ -260,6 +260,7 @@ public void Set(DotvvmPropertyId p, object? value) else { Debug.Assert(values is Dictionary); + OwnValues(); valuesAsDictionary[p] = value; } } @@ -320,10 +321,12 @@ public bool TryAdd(DotvvmPropertyId p, object? value) return Object.ReferenceEquals(existingValue, value); else { + OwnValues(); valuesAsDictionary.Add(p, value); return true; } #else + OwnValues(); return valuesAsDictionary.TryAdd(p, value) || Object.ReferenceEquals(valuesAsDictionary[p], value); #endif } @@ -408,23 +411,40 @@ internal void CloneInto(ref DotvvmControlProperties newDict) else if (this.keys == null) { var dictionary = this.valuesAsDictionary; - if (dictionary.Count > 30) + if (dictionary.Count > 8) { newDict = this; newDict.keys = null; - newDict.valuesAsDictionary = new Dictionary(dictionary); + Dictionary? newValues = null; foreach (var (key, value) in dictionary) if (CloneValue(value) is {} newValue) + { + if (newValues is null) + // ok, we have to copy it + newValues = new Dictionary(dictionary); newDict.valuesAsDictionary[key] = newValue; + } + + if (newValues is null) + { + newDict.valuesAsDictionary = dictionary; + newDict.ownsValues = false; + this.ownsValues = false; + } + else + { + newDict.valuesAsDictionary = newValues; + newDict.ownsValues = true; + } return; } - // move to immutable version if it's reasonably small. It will be probably cloned multiple times again + // move to immutable version if it's small. It will be probably cloned multiple times again SwitchToPerfectHashing(); } newDict = this; newDict.ownsKeys = false; - newDict.ownsValues = false; + this.ownsKeys = false; for (int i = 0; i < newDict.valuesAsArray.Length; i++) { if (CloneValue(newDict.valuesAsArray[i]) is {} newValue) @@ -439,6 +459,12 @@ internal void CloneInto(ref DotvvmControlProperties newDict) newDict.valuesAsArray[i] = newValue; } } + + if (newDict.values == this.values) + { + this.ownsValues = false; + newDict.ownsValues = false; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index 9a207bc696..fd08c1b49a 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -122,7 +122,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi var requiredResourceControl = controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(RequiredResource))); o.TreeVisitors.Add(() => new StyleTreeShufflingVisitor(controlResolver)); o.TreeVisitors.Add(() => new ControlPrecompilationVisitor(s)); - // o.TreeVisitors.Add(() => new LiteralOptimizationVisitor()); + o.TreeVisitors.Add(() => new LiteralOptimizationVisitor()); o.TreeVisitors.Add(() => new BindingRequiredResourceVisitor((ControlResolverMetadata)requiredResourceControl)); var requiredGlobalizeControl = controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(GlobalizeResource))); o.TreeVisitors.Add(() => new GlobalizeResourceVisitor((ControlResolverMetadata)requiredGlobalizeControl)); diff --git a/src/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index 3f26b2a6c5..9e15c03556 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -303,6 +303,8 @@ public void DotvvmProperty_CorrectGetAndSet() foreach (var example in GetExampleValues(p.PropertyType)) { instance.SetValue(p, example); + Assert.AreEqual(example, instance.GetValue(p), $"GetValue is behaving weird {p}"); + Assert.AreEqual(example, instance.GetValueRaw(p.Id), $"GetValue(id) is behaving weird {p}"); Assert.AreEqual(example, p.PropertyInfo.GetValue(instance), $"Getter is broken in {p}"); } @@ -371,5 +373,58 @@ public void DotvvmProperty_ManyItemsSetter() Assert.AreEqual(1000, control1.Properties.Count); } + + [TestMethod] + [DataRow(false, false)] + [DataRow(false, true)] + [DataRow(true, false)] + [DataRow(true, true)] + public void DotvvmProperty_ControlClone(bool manyAttributes, bool nestedControl) + { + var control = new HtmlGenericControl("div"); + control.CssStyles.Set("color", "red"); + control.Attributes.Set("something", "value"); + + if (manyAttributes) + for (int i = 0; i < 60; i++) + control.Attributes.Set("data-" + i.ToString(), i.ToString()); + + if (nestedControl) + { + control.Properties.Add(Styles.ReplaceWithProperty, new HtmlGenericControl("span") { InnerText = "23" }); + } + + var clone = (HtmlGenericControl)control.CloneControl(); + + Assert.AreEqual(control.TagName, clone.TagName); + Assert.AreEqual(control.CssStyles["color"], "red"); + + // change original + Assert.IsFalse(clone.CssStyles.ContainsKey("abc")); + control.CssStyles.Set("color", "blue"); + control.CssStyles.Set("abc", "1"); + Assert.AreEqual("red", clone.CssStyles["color"]); + Assert.IsFalse(clone.CssStyles.ContainsKey("abc")); + + if (nestedControl) + { + var nestedClone = (HtmlGenericControl)clone.Properties[Styles.ReplaceWithProperty]!; + var nestedOriginal = (HtmlGenericControl)control.Properties[Styles.ReplaceWithProperty]!; + Assert.AreEqual("23", nestedClone.InnerText); + // change clone this time + nestedClone.InnerText = "24"; + Assert.AreEqual("23", nestedOriginal.InnerText); + Assert.AreEqual("24", nestedClone.InnerText); + } + + if (manyAttributes) + { + for (int i = 0; i < 60; i++) + { + Assert.AreEqual(i.ToString(), control.Attributes["data-" + i.ToString()]); + Assert.AreEqual(i.ToString(), clone.Attributes["data-" + i.ToString()]); + } + } + } } } From c85840af107b6a3919bbbd206db4d31772a7fdbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 18 Jan 2025 00:14:50 +0100 Subject: [PATCH 04/11] Remove custom DotvvmProperty hashtable (in favor of the "SIMDict") --- .../Binding/DotvvmPropertyIdAssignment.cs | 22 +- .../Binding/VirtualPropertyGroupDictionary.cs | 33 +- .../DefaultViewCompilerCodeEmitter.cs | 49 +- .../Controls/DotvvmBindableObject.cs | 2 +- .../Controls/DotvvmControlProperties.cs | 652 +++++++++++++----- .../Controls/PropertyImmutableHashtable.cs | 601 ++++++++-------- src/Tests/Runtime/DotvvmPropertyTests.cs | 34 + 7 files changed, 899 insertions(+), 494 deletions(-) diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs index ad93bd5e05..ab31b2ce1f 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -51,6 +51,9 @@ public bool CanUseFastAccessors public DotvvmPropertyGroup? PropertyGroupInstance => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetPropertyGroup(GroupId); public string? GroupMemberName => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetGroupMemberName(MemberId); + public Type PropertyType => IsPropertyGroup ? PropertyGroupInstance.PropertyType : PropertyInstance.PropertyType; + public Type DeclaringType => IsPropertyGroup ? PropertyGroupInstance.DeclaringType : DotvvmPropertyIdAssignment.GetControlType(TypeId); + 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); @@ -65,7 +68,18 @@ public bool CanUseFastAccessors 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 override string ToString() + { + if (IsPropertyGroup) + { + var pg = PropertyGroupInstance; + return $"[{Id:x8}]{pg.DeclaringType.Name}.{pg.Name}:{GroupMemberName}"; + } + else + { + return $"[{Id:x8}]{PropertyInstance.FullName}"; + } + } public int CompareTo(DotvvmPropertyId other) => Id.CompareTo(other.Id); } @@ -259,6 +273,12 @@ public static bool IsBindingProperty(DotvvmPropertyId id) #endregion #region Registration + public static Type GetControlType(ushort id) + { + if (id == 0 || id >= controls.Length) + throw new ArgumentOutOfRangeException(nameof(id), id, "Control type ID is invalid."); + return controls[id].controlType; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort RegisterType(Type type) { diff --git a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs index f967e80f9b..42326ac175 100644 --- a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs +++ b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs @@ -9,6 +9,7 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Utils; using System.Diagnostics.CodeAnalysis; +using System.Runtime.Intrinsics; namespace DotVVM.Framework.Binding { @@ -189,7 +190,21 @@ public void CopyFrom(IEnumerable>> v } public static IDictionary CreateValueDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) { - var result = new Dictionary(); + Dictionary result; +#if NET8_0_OR_GREATER + // don't bother counting without vector instructions + if (Vector256.IsHardwareAccelerated) + { + var count = control.properties.CountPropertyGroup(group.Id); + result = new(count); + if (count == 0) + return result; + } + else + result = new(); +#else + result = new(); +#endif foreach (var (p, valueRaw) in control.properties) { if (p.IsInPropertyGroup(group.Id)) @@ -207,7 +222,21 @@ public static IDictionary CreateValueDictionary(DotvvmBindableOb public static IDictionary> CreatePropertyDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) { - var result = new Dictionary>(); + Dictionary> result; +#if NET8_0_OR_GREATER + // don't bother counting without vector instructions + if (Vector256.IsHardwareAccelerated) + { + var count = control.properties.CountPropertyGroup(group.Id); + result = new(count); + if (count == 0) + return result; + } + else + result = new(); +#else + result = new(); +#endif foreach (var (p, valRaw) in control.properties) { if (p.IsInPropertyGroup(group.Id)) diff --git a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs index e1fee4bacc..804ee41727 100644 --- a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs +++ b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs @@ -188,41 +188,35 @@ public void CommitDotvvmProperties(string name) /// 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) + if (properties.Count > PropertyImmutableHashtable.MaxArrayTableSize) { return false; } - try - { - 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); + var (_, keys, values) = PropertyImmutableHashtable.CreateTableWithValues(properties.Select(p => p.prop.Id).ToArray(), properties.Select(p => p.value).ToArray()); - // 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); + Expression valueExpr; + bool ownsValues; + if (TryCreateArrayOfConstants(values, out var invertedValues)) + { + valueExpr = EmitValue(invertedValues); + ownsValues = false; } - catch (PropertyImmutableHashtable.CannotMakeHashtableException) + else { - return false; + valueExpr = EmitCreateArray( + typeof(object), + values.Select(v => v ?? EmitValue(null)) + ); + ownsValues = true; } + + 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(false), EmitValue(ownsValues)); + + EmitStatement(magicSetValueCall); return true; } @@ -258,7 +252,6 @@ private void EmitDictionaryAssignment(ParameterExpression control, List<(DotvvmP } else { - throw new Exception("kokoooot"); var variable = Expression.Parameter(typeof(Dictionary), "props_" + control.Name); // var dict = new Dictionary(constants); diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index 1aab689860..58fccd96f9 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -461,7 +461,7 @@ public DotvvmBindableObject GetRoot() protected internal virtual DotvvmBindableObject CloneControl() { var newThis = (DotvvmBindableObject)this.MemberwiseClone(); - this.properties.CloneInto(ref newThis.properties); + this.properties.CloneInto(ref newThis.properties, newThis); return newThis; } diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index c1c74b9df5..25c33bdba0 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -1,91 +1,173 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Utils; +using Impl = DotVVM.Framework.Controls.PropertyImmutableHashtable; namespace DotVVM.Framework.Controls { - [StructLayout(LayoutKind.Explicit)] 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 DotvvmPropertyId[] & values is object[] --> read-only perfect 2-slot hashing - [FieldOffset(0)] + // 1. Empty -> keys == values == null + // 2. Dictinary -> keys == null & values is Dictionary --> it falls back to traditional mutable property dictionary + // 3. Array8 or Array16 -> keys is DotvvmPropertyId[] & values is object[] -- small linear search array + + // Note about unsafe code: + // There is always the possibility that the structure may get into an invalid state, for example due to multithreded writes. + // That is obviously not supported and it's a user error, and crashes are expected, but we want to avoid critical security issues like RCE. + // The idea is that "reading random memory is OK-ish, writing is not", so we insert runtime type checks + // to the places where this could happen: + // * we write into keys, but it has insufficient length (8 instead of expected 16) + // * we write into values, - it has insufficient length + // - it is of different type (array vs dictionary) + // * we return an invalid object reference and the client writes into it + + private const MethodImplOptions Inline = MethodImplOptions.AggressiveInlining; + private const MethodImplOptions NoInlining = MethodImplOptions.NoInlining; + + private DotvvmPropertyId[]? keys; - [FieldOffset(8)] - private object? values; + private object? values; // either object?[] or Dictionary + + private TableState state; - [FieldOffset(8)] - private object?[] valuesAsArray; + /// If the keys array is owned by this structure and can be modified. Not used in the dictionary mode. + private bool ownsKeys; + /// If the values array or the values Dictionary is owned by this structure and can be modified. + private bool ownsValues; - [FieldOffset(8)] - private Dictionary valuesAsDictionary; + private readonly bool IsArrayState => ((byte)state | 1) == 3; // state == TableState.Array8 || state == TableState.Array16; - /// - /// flags >> 31: 1bit - ownsValues - /// flags >> 30: 1bit - ownsKeys - /// flags >> 0: 30bits - hashSeed - /// - [FieldOffset(16)] - private uint flags; - private uint hashSeed + + private object?[] valuesAsArray { - readonly get => flags & 0x3F_FF_FF_FF; - set => flags = (flags & ~0x3F_FF_FF_FFu) | value; + [MethodImpl(Inline)] + readonly get + { + var value = this.values; + Impl.Assert(value!.GetType() == typeof(object[])); // safety runtime check + return Unsafe.As(value); + } + [MethodImpl(Inline)] + set => this.values = value; } - private bool ownsKeys + private readonly object?[]? valuesAsArrayUnsafe { - readonly get => ((flags >> 30) & 1) != 0; - set => flags = (flags & ~(1u << 30)) | ((uint)BoolToInt(value) << 30); + [MethodImpl(Inline)] + get => Unsafe.As(values); } - private bool ownsValues + + private Dictionary valuesAsDictionary { - readonly get => ((flags >> 31) & 1) != 0; - set => flags = (flags & ~(1u << 31)) | ((uint)BoolToInt(value) << 31); + [MethodImpl(Inline)] + readonly get + { + var value = this.values; + Impl.Assert(value!.GetType() == typeof(Dictionary)); // safety runtime check + return Unsafe.As>(value); + } + [MethodImpl(Inline)] + set => this.values = value; + } + private readonly Dictionary? valuesAsDictionaryUnsafe + { + [MethodImpl(Inline)] + get => Unsafe.As>(values); } - public void AssignBulk(DotvvmPropertyId[] keys, object?[] values, uint flags) + [Conditional("DEBUG")] + private readonly void CheckInvariant() { - // The explicit layout is quite likely to mess up with array covariance, just make sure we don't encounter that + switch (state) + { + case TableState.Empty: + Debug.Assert(keys is null && values is null); + break; + case TableState.Array8: + case TableState.Array16: + Debug.Assert(keys is {}); + Debug.Assert(values is object[]); + Debug.Assert(keys.Length == valuesAsArray.Length); + Debug.Assert(keys.Length == (state == TableState.Array8 ? 8 : 16)); + for (int i = keys.Length - 1; i >= 0 ; i--) + { + var value = valuesAsArray[i]; + var p = keys[i]; + Debug.Assert(p.Id == 0 || keys.AsSpan().IndexOf(p) == i, $"Duplicate property {p} at index {i} and {keys.AsSpan().IndexOf(keys[i])}"); + if (p.Id != 0) + { + // TODO: check currently causes issues in unrelated code + // var propType = p.PropertyType; + // Debug.Assert(value is null || value is IBinding || propType.IsInstanceOfType(value), $"Property {p} has value {value} of type {value?.GetType()} which is not assignable to {propType}"); + } + else + { + Debug.Assert(valuesAsArray[i] is null, $"Zero property id at index {i} has non-null value: {valuesAsArray[i]}"); + } + } + break; + case TableState.Dictinary: + Debug.Assert(keys is null); + Debug.Assert(values is Dictionary); + break; + default: + Impl.Fail(); + break; + } + } + + public void AssignBulk(DotvvmPropertyId[] keys, object?[] values, bool ownsKeys, bool ownsValues) + { + CheckInvariant(); + // The our unsafe memory accesses are 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(DotvvmPropertyId[])); Debug.Assert(keys.Length == values.Length); if (this.values == null || Object.ReferenceEquals(this.keys, keys)) { + // empty -> fast assignment this.valuesAsArray = values; this.keys = keys; - this.flags = flags; + this.state = keys.Length switch { + 8 => TableState.Array8, + 16 => TableState.Array16, + _ => throw new NotSupportedException("Only 8 and 16 elements are supported") + }; + this.ownsKeys = ownsKeys; + this.ownsValues = ownsValues; } 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 - for (int i = 0; i < keys.Length; i++) { if (keys[i].Id != 0) this.Set(keys[i]!, values[i]); } } + CheckInvariant(); } public void AssignBulk(Dictionary values, bool owns) { + CheckInvariant(); if (this.values == null || object.ReferenceEquals(this.values, values)) { this.keys = null; this.valuesAsDictionary = values; - this.flags = (uint)BoolToInt(owns) << 31; + this.ownsValues = owns; + this.state = TableState.Dictinary; } else { @@ -97,7 +179,7 @@ public void AssignBulk(Dictionary values, bool owns) } this.values = values; this.keys = null; - this.flags = 1u << 31; + this.ownsValues = true; } else { @@ -111,34 +193,58 @@ public void AssignBulk(Dictionary values, bool owns) public void ClearEverything() { + CheckInvariant(); values = null; keys = null; + state = TableState.Empty; } public readonly bool Contains(DotvvmProperty p) => Contains(p.Id); public readonly bool Contains(DotvvmPropertyId p) { - if (keys is {}) + CheckInvariant(); + if (state == TableState.Array8) { - Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmPropertyId[]); - return PropertyImmutableHashtable.ContainsKey(this.keys, this.hashSeed, p); + Debug.Assert(values!.GetType() == typeof(object[])); + Debug.Assert(keys is {}); + return Impl.ContainsKey8(this.keys, p); } - else if (values is null) { return false; } - else + return ContainsOutlined(p); + } + + private readonly bool ContainsOutlined(DotvvmPropertyId p) // doesn't need to be inlined + { + if (state == TableState.Empty) + return false; + if (state == TableState.Array16) { - Debug.Assert(values is Dictionary); - return valuesAsDictionary.ContainsKey(p); + return Impl.ContainsKey16(this.keys!, p); } + if (state == TableState.Dictinary) + { + return valuesAsDictionary!.ContainsKey(p); + } + return Impl.Fail(); } public readonly bool ContainsPropertyGroup(DotvvmPropertyGroup group) => ContainsPropertyGroup(group.Id); public readonly bool ContainsPropertyGroup(ushort groupId) { + CheckInvariant(); + if (state == TableState.Array8) + { + return Impl.ContainsPropertyGroup(this.keys!, groupId); + } + return ContainsPropertyGroupOutlined(groupId); + } - if (keys is {}) + private readonly bool ContainsPropertyGroupOutlined(ushort groupId) + { + if (state == TableState.Empty) + return false; + if (state == TableState.Array16) { - return PropertyImmutableHashtable.ContainsPropertyGroup(this.keys, groupId); + return Impl.ContainsPropertyGroup(this.keys!, groupId); } else if (values is null) return false; else @@ -151,40 +257,55 @@ public readonly bool ContainsPropertyGroup(ushort groupId) } return false; } + } public readonly int CountPropertyGroup(DotvvmPropertyGroup group) => CountPropertyGroup(group.Id); public readonly int CountPropertyGroup(ushort groupId) { - if (keys is {}) + CheckInvariant(); + if (state == TableState.Array8) { - return PropertyImmutableHashtable.CountPropertyGroup(this.keys, groupId); + return Impl.CountPropertyGroup8(this.keys, groupId); } - else if (values is null) return 0; - else + return CountPropertyGroupOutlined(groupId); + } + + private readonly int CountPropertyGroupOutlined(ushort groupId) + { + switch (state) { - Debug.Assert(values is Dictionary); - int count = 0; - foreach (var key in valuesAsDictionary.Keys) + case TableState.Empty: + return 0; + case TableState.Array16: + return Impl.CountPropertyGroup(this.keys!, groupId); + case TableState.Dictinary: { - if (key.IsInPropertyGroup(groupId)) - count++; + int count = 0; + foreach (var key in valuesAsDictionary.Keys) + { + if (key.IsInPropertyGroup(groupId)) + count++; + } + return count; } - return count; + default: + return Impl.Fail(); } } + [MethodImpl(Inline)] public readonly bool TryGet(DotvvmProperty p, out object? value) => TryGet(p.Id, out value); + [MethodImpl(Inline)] public readonly bool TryGet(DotvvmPropertyId p, out object? value) { - if (keys != null) + CheckInvariant(); + if (state == TableState.Array8) { - Debug.Assert(values is object[]); - Debug.Assert(keys is not null); - var index = PropertyImmutableHashtable.FindSlot(this.keys, this.hashSeed, p); + var index = Impl.FindSlot8(this.keys!, p); if (index >= 0) { - value = this.valuesAsArray[index]; + value = valuesAsArray[index]; return true; } else @@ -193,13 +314,34 @@ public readonly bool TryGet(DotvvmPropertyId p, out object? value) return false; } } - - else if (values == null) { value = null; return false; } - - else + return TryGetOutlined(p, out value); + } + private readonly bool TryGetOutlined(DotvvmPropertyId p, out object? value) + { + switch (state) { - Debug.Assert(values is Dictionary); - return valuesAsDictionary.TryGetValue(p, out value); + case TableState.Empty: + value = null; + return false; + case TableState.Array16: + { + var index = Impl.FindSlot16(this.keys!, p); + if (index >= 0) + { + value = valuesAsArray[index]; + return true; + } + else + { + value = null; + return false; + } + } + case TableState.Dictinary: + return valuesAsDictionary.TryGetValue(p, out value); + default: + value = null; + return Impl.Fail(); } } @@ -207,55 +349,83 @@ public readonly bool TryGet(DotvvmPropertyId p, out object? value) public readonly object? GetOrThrow(DotvvmPropertyId p) { if (this.TryGet(p, out var x)) return x; - throw new KeyNotFoundException(); + return ThrowKeyNotFound(p); } + [MethodImpl(NoInlining), DoesNotReturn] + private readonly object? ThrowKeyNotFound(DotvvmPropertyId p) => throw new KeyNotFoundException($"Property {p} was not found."); + [MethodImpl(Inline)] public void Set(DotvvmProperty p, object? value) => Set(p.Id, value); + // not necessarily great for inlining public void Set(DotvvmPropertyId p, object? value) { - if (p.MemberId == 0) - throw new ArgumentException("Invalid (unitialized) property id cannot be set into the DotvvmControlProperties dictionary.", nameof(p)); + CheckInvariant(); + if (p.MemberId == 0) ThrowZeroPropertyId(); - if (keys != null) + if (state == TableState.Array8) + { + var keys = this.keys!; + var slot = Impl.FindSlotOrFree8(keys, p, out var exists); + if (slot >= 0) + { + if (!exists) + { + if (!ownsKeys) + keys = CloneKeys(); + // arrays are always size >= 8 + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(keys), slot) = p; + } + this.OwnValues(); + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(this.valuesAsArray), slot) = value; // avoid covariance check + CheckInvariant(); + Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); + return; + } + } + + SetOutlined(p, value); + } + private void SetOutlined(DotvvmPropertyId p, object? value) + { + TailRecursion: + + var keys = this.keys; + if (keys is {}) { Debug.Assert(values is object[]); - Debug.Assert(keys is DotvvmPropertyId[]); - var slot = PropertyImmutableHashtable.FindSlotOrFree(keys, hashSeed, p, out var exists); + var slot = state == TableState.Array8 + ? Impl.FindSlotOrFree8(keys, p, out var exists) + : Impl.FindSlotOrFree16(keys, p, out exists); if (slot >= 0) { + Debug.Assert(slot < keys.Length && slot < valuesAsArray.Length, $"Slot {slot} is out of range for keys {keys.Length} and values {valuesAsArray.Length} (prop={p}, value={value})"); if (!exists) { - OwnKeys(); + if (!this.ownsKeys) + keys = CloneKeys(); OwnValues(); keys[slot] = p; valuesAsArray[slot] = value; } - else if (Object.ReferenceEquals(valuesAsArray[slot], value)) + else if (Object.ReferenceEquals(valuesAsArrayUnsafe![slot], value)) { // no-op, we would be changing it to the same value } else { - this.OwnValues(); + OwnValues(); valuesAsArray[slot] = value; } } else { - SwitchToDictionary(); - Debug.Assert(values is Dictionary); - valuesAsDictionary[p] = value; - keys = null; + IncreaseSize(); + goto TailRecursion; } } else if (values == null) { - Debug.Assert(keys == null); - this.flags = 0b11u << 30; - this.keys = new DotvvmPropertyId[PropertyImmutableHashtable.AdhocTableSize]; - this.keys[0] = p; - this.valuesAsArray = new object?[PropertyImmutableHashtable.AdhocTableSize]; - this.valuesAsArray[0] = value; + SetEmptyToSingle(p, value); } else { @@ -263,59 +433,97 @@ public void Set(DotvvmPropertyId p, object? value) OwnValues(); valuesAsDictionary[p] = value; } + Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); + CheckInvariant(); } + [MethodImpl(NoInlining), DoesNotReturn] + private static void ThrowZeroPropertyId() => throw new ArgumentException("Invalid (unitialized) property id cannot be set into the DotvvmControlProperties dictionary.", "p"); + /// Tries to set value into the dictionary without overwriting anything. + /// True if the value was added, false if the key was already present with a different value. public bool TryAdd(DotvvmProperty p, object? value) => TryAdd(p.Id, value); /// Tries to set value into the dictionary without overwriting anything. + /// True if the value was added, false if the key was already present with a different value. public bool TryAdd(DotvvmPropertyId p, object? value) { + CheckInvariant(); + if (p.MemberId == 0) ThrowZeroPropertyId(); + + if (state == TableState.Array8) + { + Debug.Assert(values!.GetType() == typeof(object[])); + Debug.Assert(keys is {}); + var slot = Impl.FindSlotOrFree8(this.keys, p, out var exists); + if (slot >= 0) + { + if (exists) + { + return Object.ReferenceEquals(valuesAsArrayUnsafe![slot], value); + } + OwnValues(); + OwnKeys(); + // arrays are always length >= 8 + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(keys), slot) = p; + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArray), slot) = value; // avoid covariance check + CheckInvariant(); + Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); + return true; + } + } + return TryAddOulined(p, value); + } + + private bool TryAddOulined(DotvvmPropertyId p, object? value) + { + + TailRecursion: + + var keys = this.keys; if (keys != null) { Debug.Assert(this.values is object[]); Debug.Assert(keys is DotvvmPropertyId[]); - var slot = PropertyImmutableHashtable.FindSlotOrFree(keys, this.hashSeed, p, out var exists); + var slot = state == TableState.Array8 + ? Impl.FindSlotOrFree8(keys, p, out var exists) + : Impl.FindSlotOrFree16(keys, p, out exists); if (slot >= 0) { if (exists) { // value already exists - return Object.ReferenceEquals(valuesAsArray[slot], value); + return Object.ReferenceEquals(valuesAsArrayUnsafe![slot], value); } else { - OwnKeys(); + if (!this.ownsKeys) + keys = CloneKeys(); OwnValues(); - // set the value keys[slot] = p; - valuesAsArray[slot] = value; + var valuesAsArray = this.valuesAsArray; + Impl.Assert(valuesAsArray.Length > slot); + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArray), slot) = value; // avoid covariance check + CheckInvariant(); + Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); return true; } } else { - // no free slots, move to standard Dictionary - SwitchToDictionary(); - this.valuesAsDictionary.Add(p, value); - return true; + IncreaseSize(); + goto TailRecursion; } } if (values == null) { - // empty dict -> initialize 8-slot array - Debug.Assert(keys == null); - this.flags = 0b11u << 30; - this.keys = new DotvvmPropertyId[PropertyImmutableHashtable.AdhocTableSize]; - this.keys[0] = p; - this.valuesAsArray = new object?[PropertyImmutableHashtable.AdhocTableSize]; - this.valuesAsArray[0] = value; + SetEmptyToSingle(p, value); return true; } else { // System.Dictionary backend - Debug.Assert(values is Dictionary); + var valuesAsDictionary = this.valuesAsDictionary; #if CSharp8Polyfill if (valuesAsDictionary.TryGetValue(p, out var existingValue)) return Object.ReferenceEquals(existingValue, value); @@ -332,14 +540,31 @@ public bool TryAdd(DotvvmPropertyId p, object? value) } } + private void SetEmptyToSingle(DotvvmPropertyId p, object? value) + { + Debug.Assert(this.keys == null); + Debug.Assert(this.values == null); + Debug.Assert(this.state == TableState.Empty); + var newKeys = new DotvvmPropertyId[Impl.AdhocTableSize]; + newKeys[0] = p; + var newValues = new object?[Impl.AdhocTableSize]; + newValues[0] = value; + + this.keys = newKeys; + this.values = newValues; + this.ownsKeys = this.ownsValues = true; + this.state = TableState.Array8; + CheckInvariant(); + } + [MethodImpl(Inline)] public readonly DotvvmControlPropertyIdEnumerator GetEnumerator() { + CheckInvariant(); if (keys != null) return new DotvvmControlPropertyIdEnumerator(keys, valuesAsArray); if (values == null) return EmptyEnumerator; - Debug.Assert(values is Dictionary); return new DotvvmControlPropertyIdEnumerator(valuesAsDictionary.GetEnumerator()); } @@ -348,9 +573,10 @@ public readonly DotvvmControlPropertyIdEnumerator GetEnumerator() readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + [MethodImpl(Inline)] public readonly PropertyGroupEnumerable PropertyGroup(ushort groupId) => new(in this, groupId); - readonly public DotvvmControlPropertyIdGroupEnumerator EnumeratePropertyGroup(ushort id) => + public readonly DotvvmControlPropertyIdGroupEnumerator EnumeratePropertyGroup(ushort id) => this.keys is {} keys ? new(keys, valuesAsArray, id) : this.values is {} ? new(valuesAsDictionary.GetEnumerator(), id) : default; @@ -360,17 +586,17 @@ readonly public DotvvmControlPropertyIdGroupEnumerator EnumeratePropertyGroup(us public bool Remove(DotvvmProperty key) => Remove(key.Id); public bool Remove(DotvvmPropertyId key) { + CheckInvariant(); if (this.keys != null) { - var slot = PropertyImmutableHashtable.FindSlot(this.keys, this.hashSeed, key); + var slot = Impl.FindSlot(this.keys, key); if (slot < 0) return false; this.OwnKeys(); this.keys[slot] = default; - if (this.ownsValues) - { - this.valuesAsArray[slot] = default; - } + this.OwnValues(); + this.valuesAsArray[slot] = default; + CheckInvariant(); return true; } if (this.values == null) @@ -382,27 +608,33 @@ public bool Remove(DotvvmPropertyId key) } } - private static object? CloneValue(object? value) + private static object? CloneValue(object? value, DotvvmBindableObject? newParent) { if (value is DotvvmBindableObject bindableObject) - return bindableObject.CloneControl(); + { + bindableObject = bindableObject.CloneControl(); + bindableObject.Parent = newParent; + return bindableObject; + } return null; } public readonly int Count() { + CheckInvariant(); if (this.values == null) return 0; if (this.keys == null) return this.valuesAsDictionary.Count; - return PropertyImmutableHashtable.Count(this.keys); + return Impl.Count(this.keys); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static byte BoolToInt(bool x) => Unsafe.As(ref x); - internal void CloneInto(ref DotvvmControlProperties newDict) + internal void CloneInto(ref DotvvmControlProperties newDict, DotvvmBindableObject newParent) { + CheckInvariant(); if (this.values == null) { newDict = default; @@ -411,13 +643,13 @@ internal void CloneInto(ref DotvvmControlProperties newDict) else if (this.keys == null) { var dictionary = this.valuesAsDictionary; - if (dictionary.Count > 8) + if (dictionary.Count > 16) { newDict = this; newDict.keys = null; Dictionary? newValues = null; foreach (var (key, value) in dictionary) - if (CloneValue(value) is {} newValue) + if (CloneValue(value, newParent) is {} newValue) { if (newValues is null) // ok, we have to copy it @@ -436,27 +668,30 @@ internal void CloneInto(ref DotvvmControlProperties newDict) newDict.valuesAsDictionary = newValues; newDict.ownsValues = true; } + newDict.CheckInvariant(); + CheckInvariant(); return; } // move to immutable version if it's small. It will be probably cloned multiple times again - SwitchToPerfectHashing(); + SwitchToSimdDict(); } newDict = this; newDict.ownsKeys = false; this.ownsKeys = false; - for (int i = 0; i < newDict.valuesAsArray.Length; i++) + var valuesAsArray = newDict.valuesAsArray; + for (int i = 0; i < valuesAsArray.Length; i++) { - if (CloneValue(newDict.valuesAsArray[i]) is {} newValue) + if (!newDict.keys![i].IsZero && valuesAsArray[i] is {} && CloneValue(valuesAsArray[i], newParent) is {} newValue) { // clone the array if we didn't do that already if (newDict.values == this.values) { - newDict.values = this.valuesAsArray.Clone(); + newDict.values = valuesAsArray = valuesAsArray.AsSpan().ToArray(); newDict.ownsValues = true; } - newDict.valuesAsArray[i] = newValue; + valuesAsArray[i] = newValue; } } @@ -465,30 +700,38 @@ internal void CloneInto(ref DotvvmControlProperties newDict) this.ownsValues = false; newDict.ownsValues = false; } + newDict.CheckInvariant(); + CheckInvariant(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(Inline)] void OwnKeys() { if (this.ownsKeys) return; CloneKeys(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(Inline)] void OwnValues() { if (this.ownsValues) return; CloneValues(); } - void CloneKeys() + [MethodImpl(NoInlining)] + DotvvmPropertyId[] CloneKeys() { + CheckInvariant(); var oldKeys = this.keys; var newKeys = new DotvvmPropertyId[oldKeys!.Length]; MemoryExtensions.CopyTo(oldKeys, newKeys.AsSpan()); this.keys = newKeys; this.ownsKeys = true; + CheckInvariant(); + return newKeys; } + [MethodImpl(NoInlining)] void CloneValues() { + CheckInvariant(); if (keys is {}) { var oldValues = this.valuesAsArray; @@ -502,92 +745,151 @@ void CloneValues() else { this.valuesAsDictionary = new Dictionary(this.valuesAsDictionary); - this.flags = 1u << 31; + this.ownsValues = true; + } + CheckInvariant(); + } + + /// Switch Empty -> Array8, Array8 -> Array16, or Array16 -> Dictionary + void IncreaseSize() + { + CheckInvariant(); + switch (state) + { + case TableState.Empty: + this.keys = new DotvvmPropertyId[Impl.AdhocTableSize]; + this.valuesAsArray = new object?[Impl.AdhocTableSize]; + this.state = TableState.Array8; + this.ownsKeys = this.ownsValues = true; + break; + case TableState.Array8: + var newKeys = new DotvvmPropertyId[16]; + var newValues = new object?[16]; + MemoryExtensions.CopyTo(this.keys, newKeys.AsSpan()); + MemoryExtensions.CopyTo(this.valuesAsArray, newValues.AsSpan()); + this.keys = newKeys; + this.valuesAsArray = newValues; + this.state = TableState.Array16; + this.ownsKeys = this.ownsValues = true; + break; + case TableState.Array16: + SwitchToDictionary(); + break; + case TableState.Dictinary: + break; + default: + Impl.Fail(); + break; } + CheckInvariant(); } /// Converts the internal representation to System.Collections.Generic.Dictionary void SwitchToDictionary() { - if (this.keys is {}) + CheckInvariant(); + switch (state) { - var keysTmp = this.keys; - var valuesTmp = this.valuesAsArray; - var d = new Dictionary(capacity: keysTmp.Length); + case TableState.Array8: + case TableState.Array16: + 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; + for (int i = 0; i < keysTmp.Length; i++) + { + if (keysTmp[i].Id != 0) + d[keysTmp[i]] = valuesTmp[i]; + } + this.state = TableState.Dictinary; + this.valuesAsDictionary = d; + this.keys = null; + this.ownsValues = true; + break; + case TableState.Empty: + this.state = TableState.Dictinary; + this.valuesAsDictionary = new Dictionary(); + this.ownsValues = true; + break; + case TableState.Dictinary: + break; + default: + Impl.Fail(); + break; } + CheckInvariant(); } /// Converts the internal representation to the DotVVM small dictionary implementation - void SwitchToPerfectHashing() + void SwitchToSimdDict() { + CheckInvariant(); if (this.keys is {}) { - // already in the perfect hashing + // already in the small dictionary format return; } else if (this.values is {}) { - var properties = new DotvvmPropertyId[valuesAsDictionary.Count]; - var values = new object?[properties.Length]; + var valuesAsDictionary = this.valuesAsDictionary; + + if (valuesAsDictionary.Count > 16) + { + return; + } + + var properties = new DotvvmPropertyId[valuesAsDictionary.Count >= 8 ? 16 : 8]; + var values = new object?[properties.Length >= 8 ? 16 : 8]; int j = 0; - foreach (var x in this.valuesAsDictionary) + foreach (var x in 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.keys = properties; + this.valuesAsArray = values; + this.state = properties.Length == 8 ? TableState.Array8 : TableState.Array16; + this.ownsKeys = true; this.ownsValues = true; } else { } + CheckInvariant(); } public readonly struct PropertyGroupEnumerable: IEnumerable> { private readonly DotvvmControlProperties properties; private readonly ushort groupId; + [MethodImpl(Inline)] public PropertyGroupEnumerable(in DotvvmControlProperties properties, ushort groupId) { this.properties = properties; this.groupId = groupId; } + [MethodImpl(Inline)] public IEnumerator> GetEnumerator() => properties.EnumeratePropertyGroup(groupId); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + public enum TableState : byte + { + Empty = 0, + Dictinary = 1, + Array8 = 2, + Array16 = 3, + } } public struct DotvvmControlPropertyIdGroupEnumerator : IEnumerator> { - private DotvvmPropertyId[]? keys; - private object?[]? values; + private readonly DotvvmPropertyId[]? keys; + private readonly object?[]? values; + private readonly ushort groupId; + private readonly ushort bitmap; private int index; - private ushort groupId; - private ushort bitmap; // TODO!! private Dictionary.Enumerator dictEnumerator; internal DotvvmControlPropertyIdGroupEnumerator(DotvvmPropertyId[] keys, object?[] values, ushort groupId) @@ -596,7 +898,7 @@ internal DotvvmControlPropertyIdGroupEnumerator(DotvvmPropertyId[] keys, object? this.values = values; this.index = -1; this.groupId = groupId; - this.bitmap = 0; + this.bitmap = Impl.FindGroupBitmap(keys, groupId); dictEnumerator = default; } @@ -609,7 +911,10 @@ internal DotvvmControlPropertyIdGroupEnumerator(Dictionary Current => this.keys is {} keys ? new(keys[index]!, values![index]) : dictEnumerator.Current; + public KeyValuePair Current => + this.keys is {} keys + ? new(keys[index]!, values![index]) + : dictEnumerator.Current; object IEnumerator.Current => this.Current; @@ -620,25 +925,10 @@ 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; + var index = this.index + 1; + var bitmap = this.bitmap >> index; + this.index = index + BitOperations.TrailingZeroCount(bitmap); + return bitmap != 0; } else { diff --git a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs index e46e2d08d1..d19dfa34eb 100644 --- a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs +++ b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs @@ -1,3 +1,4 @@ +#define Vectorize // for easier testing on without old Framework using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -8,260 +9,327 @@ using System.Runtime.InteropServices; using DotVVM.Framework.Binding; using DotVVM.Framework.Hosting.ErrorPages; -using RecordExceptions; +using System.Security; +using System.Diagnostics.CodeAnalysis; -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER && Vectorize using System.Runtime.Intrinsics; #endif +#if NET7_0_OR_GREATER +using UnreachableException = System.Diagnostics.UnreachableException; +#else +using UnreachableException = System.Exception; +#endif + namespace DotVVM.Framework.Controls { internal static class PropertyImmutableHashtable { /// 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(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p) + public const int AdhocLargeTableSize = 16; + public const int MaxArrayTableSize = 16; + + // General implementation notes: + // * The most heavily used methods are specialized to 8-wide and 16-wide arrays, + // others might work with variable size, but divisibility by 8 is always assumed. + // 8 is the number of property IDs which fit into a 256-bit AVX register, which + // is currently available in most CPUs. + // * We check for HW support of 128-bit registers, but then use 256-bit - this is on purpose: + // Vector256 automatically falls back onto 128, if support for it isn't available, which + // is still advantageous over 8 scalar operations. Main reason is that we can use + // ARM CPUs currently have 128-bit wide SIMD registers, but can generally issue + // more vector instructions in parallel (so the compute power is similar to x86). + + private const MethodImplOptions Inline = MethodImplOptions.AggressiveInlining; + private const MethodImplOptions NoInlining = MethodImplOptions.NoInlining; + + + [MethodImpl(Inline)] + private static bool ContainsKey8(ref DotvvmPropertyId keys, DotvvmPropertyId p) { -#if NET7_0_OR_GREATER - if (Vector256.IsHardwareAccelerated) +#if NET8_0_OR_GREATER && Vectorize + if (Vector128.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; - } + var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + return Vector256.EqualsAny(v, Vector256.Create(p.Id)); } -#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; - } - } + return + Unsafe.Add(ref keys, 0).Id == p.Id | + Unsafe.Add(ref keys, 1).Id == p.Id | + Unsafe.Add(ref keys, 2).Id == p.Id | + Unsafe.Add(ref keys, 3).Id == p.Id | + Unsafe.Add(ref keys, 4).Id == p.Id | + Unsafe.Add(ref keys, 5).Id == p.Id | + Unsafe.Add(ref keys, 6).Id == p.Id | + Unsafe.Add(ref keys, 7).Id == p.Id; + } - if (keys.Length == AdhocTableSize) + [MethodImpl(Inline)] + public static bool ContainsKey8(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + Debug.Assert(keys.Length == AdhocTableSize); + return ContainsKey8(ref MemoryMarshal.GetArrayDataReference(keys), p); + } + + [MethodImpl(Inline)] + private static bool ContainsKey16(ref DotvvmPropertyId keys, DotvvmPropertyId p) + { +#if NET8_0_OR_GREATER && Vectorize + if (Vector128.IsHardwareAccelerated) { - return false; + Debug.Assert(Vector256.Count == AdhocTableSize); + // most likely, dictionary does not contain the value, so we will need to read the second vector in almost all cases + var v1 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var v2 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref keys, 8))); + return Vector256.EqualsAny(v1, Vector256.Create(p.Id)) || Vector256.EqualsAny(v2, Vector256.Create(p.Id)); } +#endif + return ContainsKey8(ref keys, p) || ContainsKey8(ref Unsafe.Add(ref keys, 8), p); + } - 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].Id == p.Id | keys[i2].Id == p.Id; + public static bool ContainsKey16(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + Debug.Assert(keys.Length == AdhocLargeTableSize); + return ContainsKey16(ref MemoryMarshal.GetArrayDataReference(keys), p); } - public static int FindSlot(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p) + [MethodImpl(Inline)] + private static int FindSlot8(ref DotvvmPropertyId keys, DotvvmPropertyId p) { -#if NET7_0_OR_GREATER - if (Vector256.IsHardwareAccelerated) +#if NET8_0_OR_GREATER && Vectorize + if (Vector128.IsHardwareAccelerated) { Debug.Assert(Vector256.Count == AdhocTableSize); - var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); + var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); if (eq != 0) { return BitOperations.TrailingZeroCount(eq); } + else + { + return -1; + } } -#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 = 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 (Unsafe.Add(ref keys, 0).Id == p.Id) return 0; + if (Unsafe.Add(ref keys, 1).Id == p.Id) return 1; + if (Unsafe.Add(ref keys, 2).Id == p.Id) return 2; + if (Unsafe.Add(ref keys, 3).Id == p.Id) return 3; + if (Unsafe.Add(ref keys, 4).Id == p.Id) return 4; + if (Unsafe.Add(ref keys, 5).Id == p.Id) return 5; + if (Unsafe.Add(ref keys, 6).Id == p.Id) return 6; + if (Unsafe.Add(ref keys, 7).Id == p.Id) return 7; - if (keys[i1].Id == p.Id) return i1; - if (keys[i2].Id == p.Id) return i2; return -1; } - public static int FindSlotOrFree(DotvvmPropertyId[] keys, uint flags, DotvvmPropertyId p, out bool exists) + [MethodImpl(Inline)] + public static int FindSlot8(DotvvmPropertyId[] keys, DotvvmPropertyId p) { - int free = -1; - exists = true; + Debug.Assert(keys.Length == AdhocTableSize); + return FindSlot8(ref MemoryMarshal.GetArrayDataReference(keys), p); + } -#if NET8_0_OR_GREATER - if (Vector256.IsHardwareAccelerated) + [MethodImpl(Inline)] + private static int FindSlot16(ref DotvvmPropertyId keys, DotvvmPropertyId p) + { +#if NET8_0_OR_GREATER && Vectorize + if (Vector128.IsHardwareAccelerated) { - var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); - var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var v1 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var v2 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref keys, 8))); + var eq1 = Vector256.Equals(v1, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var eq2 = Vector256.Equals(v2, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var eq = eq1 | (eq2 << 8); if (eq != 0) { return BitOperations.TrailingZeroCount(eq); } - var eq0 = Vector256.Equals(v, Vector256.Create(0u)).ExtractMostSignificantBits(); - if (eq0 != 0) + else { - free = BitOperations.TrailingZeroCount(eq0); + return -1; } } -#else - if (false) { } #endif - else + var ix = FindSlot8(ref keys, p); + if (ix >= 0) return ix; + + ix = FindSlot8(ref Unsafe.Add(ref keys, 8), p); + if (ix >= 0) return ix + 8; + + return -1; + } + + [MethodImpl(Inline)] + public static int FindSlot16(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + Debug.Assert(keys.Length == AdhocLargeTableSize); + return FindSlot16(ref MemoryMarshal.GetArrayDataReference(keys), p); + } + + public static int FindSlot(DotvvmPropertyId[] keys, DotvvmPropertyId p) + { + if (keys.Length == AdhocTableSize) { - 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; + return FindSlot8(keys, p); } - - if (keys.Length == 8) + else if (keys.Length == AdhocLargeTableSize) + { + return FindSlot16(keys, p); + } + else { - exists = false; - return free; + throw new ArgumentException("Keys must have 8 or 16 elements.", nameof(keys)); } + } - 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; + [MethodImpl(Inline)] + private static int FindSlotOrFree8(ref DotvvmPropertyId keys, DotvvmPropertyId p, out bool exists) + { +#if NET8_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) + { + var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + exists = eq != 0; + if (eq != 0) + { + return BitOperations.TrailingZeroCount(eq); + } + var empty = Vector256.Equals(v, Vector256.Zero).ExtractMostSignificantBits(); + if (empty != 0) + { + return BitOperations.TrailingZeroCount(empty); + } - 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; + return -1; + } +#endif + exists = true; + if (Unsafe.Add(ref keys, 0).Id == p.Id) return 0; + if (Unsafe.Add(ref keys, 1).Id == p.Id) return 1; + if (Unsafe.Add(ref keys, 2).Id == p.Id) return 2; + if (Unsafe.Add(ref keys, 3).Id == p.Id) return 3; + if (Unsafe.Add(ref keys, 4).Id == p.Id) return 4; + if (Unsafe.Add(ref keys, 5).Id == p.Id) return 5; + if (Unsafe.Add(ref keys, 6).Id == p.Id) return 6; + if (Unsafe.Add(ref keys, 7).Id == p.Id) return 7; exists = false; - if (keys[i1].Id == 0) return i1; - if (keys[i2].Id == 0) return i2; - return free; - } + if (Unsafe.Add(ref keys, 0).Id == 0) return 0; + if (Unsafe.Add(ref keys, 1).Id == 0) return 1; + if (Unsafe.Add(ref keys, 2).Id == 0) return 2; + if (Unsafe.Add(ref keys, 3).Id == 0) return 3; + if (Unsafe.Add(ref keys, 4).Id == 0) return 4; + if (Unsafe.Add(ref keys, 5).Id == 0) return 5; + if (Unsafe.Add(ref keys, 6).Id == 0) return 6; + if (Unsafe.Add(ref keys, 7).Id == 0) return 7; + return -1; + } - public static int FindFreeAdhocSlot(DotvvmPropertyId[] keys) + [MethodImpl(Inline)] + private static int FindSlotOrFree16(ref DotvvmPropertyId keys, DotvvmPropertyId p, out bool exists) { - Debug.Assert(keys.Length >= AdhocTableSize); -#if NET7_0_OR_GREATER - if (Vector256.IsHardwareAccelerated) +#if NET8_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) { - Debug.Assert(Vector256.Count == AdhocTableSize); - var v = Unsafe.ReadUnaligned>(ref MemoryMarshal.GetArrayDataReference((Array)keys)); - var eq = Vector256.Equals(v, Vector256.Create(0u)).ExtractMostSignificantBits(); + var v1 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + var v2 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref keys, 8))); + var eq1 = Vector256.Equals(v1, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var eq2 = Vector256.Equals(v2, Vector256.Create(p.Id)).ExtractMostSignificantBits(); + var eq = eq1 | (eq2 << 8); + exists = eq != 0; if (eq != 0) - { return BitOperations.TrailingZeroCount(eq); - } + + var empty1 = Vector256.Equals(v1, Vector256.Zero).ExtractMostSignificantBits(); + var empty2 = Vector256.Equals(v2, Vector256.Zero).ExtractMostSignificantBits(); + var empty = empty1 | (empty2 << 8); + if (empty != 0) + return BitOperations.TrailingZeroCount(empty); + + return -1; } -#else - if (false) { } #endif - else + var ix = FindSlot16(ref keys, p); + exists = ix >= 0; + if (ix >= 0) + { + return ix; + } + ix = FindSlot16(ref keys, 0); + if (ix >= 0) { - 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 ix; } + return -1; } - public static ushort FindGroupInNext16Slots(DotvvmPropertyId[] keys, uint startIndex, ushort groupId) + + [MethodImpl(Inline)] + public static int FindSlotOrFree8(DotvvmPropertyId[] keys, DotvvmPropertyId p, out bool exists) => + FindSlotOrFree8(ref MemoryMarshal.GetArrayDataReference(keys), p, out exists); + [MethodImpl(NoInlining)] + public static int FindSlotOrFree16(DotvvmPropertyId[] keys, DotvvmPropertyId p, out bool exists) => + FindSlotOrFree16(ref MemoryMarshal.GetArrayDataReference(keys), p, out exists); + + public static ushort FindGroupBitmap(ref DotvvmPropertyId keys, int length, ushort groupId) { - Debug.Assert(keys.Length % ArrayMultipleSize == 0); - Debug.Assert(keys.Length >= AdhocTableSize); + Debug.Assert(length is AdhocTableSize or AdhocLargeTableSize); - ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; - ushort bitmap = 0; - ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; // groupId ^ 0x8000 -#if NET7_0_OR_GREATER - if (Vector256.IsHardwareAccelerated) + ushort bitmap = 0; +#if NET8_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) { - var v1 = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, (int)startIndex))); + var v1 = Unsafe.ReadUnaligned>(in Unsafe.As(ref keys)); bitmap = (ushort)Vector256.Equals(v1 >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits(); - if (keys.Length > startIndex + 8) + if (length == 16) { - var v2 = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keysRef, (int)startIndex + 8))); + var v2 = Unsafe.ReadUnaligned>(in Unsafe.As(ref Unsafe.Add(ref keys, 8))); bitmap |= (ushort)(Vector256.Equals(v2 >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits() << 8); } + return bitmap; } -#else - if (false) { } #endif - else + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 0).TypeId == idPrefix) << 0); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 1).TypeId == idPrefix) << 1); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 2).TypeId == idPrefix) << 2); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 3).TypeId == idPrefix) << 3); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 4).TypeId == idPrefix) << 4); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 5).TypeId == idPrefix) << 5); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 6).TypeId == idPrefix) << 6); + bitmap |= (ushort)(BoolToInt(Unsafe.Add(ref keys, 7).TypeId == idPrefix) << 7); + if (length == 16) { - 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); - } + bitmap |= (ushort)(FindGroupBitmap(ref Unsafe.Add(ref keys, 8), AdhocTableSize, groupId) << 8); } return bitmap; } + public static ushort FindGroupBitmap(DotvvmPropertyId[] keys, ushort groupId) + { + return FindGroupBitmap(ref MemoryMarshal.GetArrayDataReference(keys), keys.Length, groupId); + } + public static bool ContainsPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) { - Debug.Assert(keys.Length % ArrayMultipleSize == 0 && Vector256.Count == ArrayMultipleSize); + Debug.Assert(keys.Length % Vector256.Count == 0); Debug.Assert(keys.Length >= AdhocTableSize); + Debug.Assert(keys.Length % 8 == 0); 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) +#if NET8_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) { for (int i = 0; i < keys.Length; i += 8) { @@ -273,32 +341,27 @@ public static bool ContainsPropertyGroup(DotvvmPropertyId[] keys, ushort groupId } return false; } -#else - if (false) { } #endif - else + for (int i = 0; i < keys.Length; i++) { - for (int i = 0; i < keys.Length; i++) + if (keys[i].TypeId == idPrefix) { - if (keys[i].TypeId == idPrefix) - { - return true; - } + return true; } - return false; } + return false; } public static int Count(DotvvmPropertyId[] keys) { - Debug.Assert(keys.Length % ArrayMultipleSize == 0 && Vector256.Count == ArrayMultipleSize); + Debug.Assert(keys.Length % Vector256.Count == 0); 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) +#if NET8_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) { int zeroCount = 0; for (int i = 0; i < keys.Length; i += Vector256.Count) @@ -309,53 +372,68 @@ public static int Count(DotvvmPropertyId[] keys) } return keys.Length - zeroCount; } -#else - if (false) { } #endif - else + int count = 0; + for (int i = 0; i < keys.Length; i++) { - int count = 0; - for (int i = 0; i < keys.Length; i++) - { - count += BoolToInt(keys[i].Id == 0); - } - return count; + count += BoolToInt(keys[i].Id == 0); } - + return count; } - public static int CountPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) + [MethodImpl(Inline)] + private static int CountPropertyGroup8(ref 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; + ref var keysInts = ref Unsafe.As(ref keys); -#if NET7_0_OR_GREATER - if (Vector256.IsHardwareAccelerated) +#if NET8_0_OR_GREATER && Vectorize + if (Vector128.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()); - } + var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); + return BitOperations.PopCount(Vector256.Equals(v >> 16, Vector256.Create((uint)idPrefix)).ExtractMostSignificantBits()); } -#else - if (false) { } #endif - else + int count = 0; + count += BoolToInt(Unsafe.Add(ref keysInts, 0) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 1) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 2) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 3) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 4) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 5) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 6) >> 16 == idPrefix); + count += BoolToInt(Unsafe.Add(ref keysInts, 7) >> 16 == idPrefix); + return count; + } + + [MethodImpl(Inline)] + public static int CountPropertyGroup8(DotvvmPropertyId[] keys, ushort groupId) + { + Debug.Assert(keys.Length == 8); + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + return CountPropertyGroup8(ref keysRef, groupId); + } + + public static int CountPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) + { + Debug.Assert(keys.Length % 8 == 0); + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + + int count = 0; + for (int i = 0; i < keys.Length; i += 8) { - for (int i = 0; i < keys.Length; i++) - { - count += BoolToInt(keys[i].TypeId == idPrefix); - } + count += CountPropertyGroup8(ref Unsafe.Add(ref keysRef, i), groupId); } return count; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static byte BoolToInt(bool x) => Unsafe.As(ref x); + [MethodImpl(Inline)] + private static byte BoolToInt(bool x) => +#if NET8_0_OR_GREATER // JIT can finally optimize this to branchless code + x ? (byte)1 : (byte)0; +#else + Unsafe.As(ref x); +#endif static ConcurrentDictionary tableCache = new(new EqCmp()); @@ -381,8 +459,20 @@ public int GetHashCode(DotvvmPropertyId[] obj) } } - // Some primes. Numbers not divisible by 2 should help shuffle the table in a different way every time. - 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]; + [MethodImpl(Inline)] + public static void Assert([DoesNotReturnIf(false)] bool condition) + { + if (!condition) + Fail(); + } + + [DoesNotReturn] + [MethodImpl(NoInlining)] + public static void Fail() => throw new UnreachableException("Assertion failed in DotVVM property dictionary. This is a serious bug, please report it."); + + [DoesNotReturn] + [MethodImpl(NoInlining)] + public static T Fail() => throw new UnreachableException("Assertion failed in DotVVM property dictionary. This is a serious bug, please report it."); private static bool IsOrderedWithoutDuplicatesAndZero(DotvvmPropertyId[] keys) { @@ -402,80 +492,34 @@ public static (uint hashSeed, DotvvmPropertyId[] keys) BuildTable(DotvvmProperty { throw new ArgumentException("Keys must be ordered, without duplicates and without zero.", nameof(keys)); } + if (keys.Length > 16) + { + throw new ArgumentException("Keys must have at most 16 elements.", 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(keys, static keys => { - if (keys.Length <= 8) - { - // just pad them to make things regular - 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.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) - { - Debug.Assert((size & (size - 1)) == 0); - foreach (var hashSeed in hashSeeds) - { - var result = TryBuildTable(keys, size, hashSeed); - if (result != null) - { - Debug.Assert(TestTableCorrectness(keys, hashSeed, result)); - return (hashSeed, result); - } - } - - size *= 2; - - if (size <= 4) throw new CannotMakeHashtableException(); - } - - } + // pad result to 8 or 16 elements + var result = new DotvvmPropertyId[keys.Length <= 8 ? 8 : 16]; + Array.Copy(keys, result, keys.Length); + return (0, result); }); } - static bool TestTableCorrectness(DotvvmPropertyId[] keys, uint hashSeed, DotvvmPropertyId[] table) - { - 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 DotvvmPropertyId[]? TryBuildTable(DotvvmPropertyId[] a, int size, uint hashSeed) + public static (uint hashSeed, DotvvmPropertyId[] keys, T[] valueTable) CreateTableWithValues(DotvvmPropertyId[] properties, T[] values) { - 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(), (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 (properties.Length != values.Length) + throw new ArgumentException("Properties and values must have the same length.", nameof(properties)); + if (properties.Length > 16) + throw new ArgumentException("Properties and values must have at most 16 elements, otherwise create a dictionary.", nameof(properties)); - if (t[i1].IsZero) - t[i1] = k; - 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 (uint hashSeed, DotvvmPropertyId[] keys, T[] valueTable) CreateTableWithValues(DotvvmPropertyId[] properties, T[] values) - { var (hashSeed, keys) = BuildTable(properties); var valueTable = new T[keys.Length]; for (int i = 0; i < properties.Length; i++) { - valueTable[FindSlot(keys, hashSeed, properties[i])] = values[i]; + valueTable[FindSlot(keys, properties[i])] = values[i]; } return (hashSeed, keys, valueTable); } @@ -488,7 +532,7 @@ public static Action CreateBulkSetter(DotvvmProperty[] pro } public static Action CreateBulkSetter(DotvvmPropertyId[] properties, object?[] values) { - if (properties.Length > 30) + if (properties.Length > MaxArrayTableSize) { var dict = new Dictionary(capacity: properties.Length); for (int i = 0; i < properties.Length; i++) @@ -500,23 +544,18 @@ public static Action CreateBulkSetter(DotvvmPropertyId[] p else { var (hashSeed, keys, valueTable) = CreateTableWithValues(properties, values); - return (obj) => obj.properties.AssignBulk(keys, valueTable, hashSeed); + return (obj) => obj.properties.AssignBulk(keys, valueTable, false, false); } } - public static void SetValuesToDotvvmControl(DotvvmBindableObject obj, DotvvmPropertyId[] properties, object?[] values, uint flags) + public static void SetValuesToDotvvmControl(DotvvmBindableObject obj, DotvvmPropertyId[] properties, object?[] values, bool ownsKeys, bool ownsValues) { - obj.properties.AssignBulk(properties, values, flags); + obj.properties.AssignBulk(properties, values, ownsKeys, ownsValues); } 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/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index 9e15c03556..00fb46565e 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -426,5 +426,39 @@ public void DotvvmProperty_ControlClone(bool manyAttributes, bool nestedControl) } } } + + [TestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(2)] + public void DotvvmProperty_VirtualDictionary_Append(int testClone) + { + var control = new HtmlGenericControl("div"); + + foreach (var i in Enumerable.Range(0, 50)) + { + control.Attributes.Set($"data-{i}", i); + + if (testClone > 0) + { + var clone = (HtmlGenericControl)control.CloneControl(); + if (testClone == 2) + (control, clone) = (clone, control); + + clone.Attributes.Set("something-else", "abc"); + Assert.AreEqual("abc", clone.Attributes["something-else"]); + clone.Attributes.Set("data-5", -1); + Assert.AreEqual(-1, clone.Attributes["data-5"]); + } + + Assert.AreEqual(i + 1, control.properties.Count()); + Assert.AreEqual(i + 1, control.Attributes.Count); + Assert.IsTrue(control.Attributes.ContainsKey("data-" + i.ToString())); + Assert.IsFalse(control.Attributes.ContainsKey("something-else")); + Assert.AreEqual(i, control.Attributes["data-" + i.ToString()]); + + XAssert.Equal(Enumerable.Range(0, i+1).Cast(), control.Attributes.Values); + } + } } } From 714997e777a8e7234acb19242596d614bd289f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 15 Mar 2025 23:28:32 +0100 Subject: [PATCH 05/11] Cleanup of DotvvmPropertyIdAssignment, add support for manualy assigned IDs --- .../Framework/Binding/DotvvmPropertyId.cs | 76 ++++++++ ...DotvvmPropertyIdAssignment.GroupMembers.cs | 36 ++++ .../DotvvmPropertyIdAssignment.PropertyIds.cs | 72 +++++++ .../DotvvmPropertyIdAssignment.TypeIds.cs | 50 +++++ .../Binding/DotvvmPropertyIdAssignment.cs | 183 ++++-------------- 5 files changed, 276 insertions(+), 141 deletions(-) create mode 100644 src/Framework/Framework/Binding/DotvvmPropertyId.cs create mode 100644 src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs create mode 100644 src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs create mode 100644 src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs diff --git a/src/Framework/Framework/Binding/DotvvmPropertyId.cs b/src/Framework/Framework/Binding/DotvvmPropertyId.cs new file mode 100644 index 0000000000..416bafd4cd --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyId.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using DotVVM.Framework.Compilation.ControlTree; + +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); + + /// Returns true if the property does not have GetValue/SetValue overrides and is not inherited. That means it is sufficient + public bool CanUseFastAccessors + { + get + { + // properties: we encode this information as the LSB bit of the member ID (i.e. odd/even numbers) + // property groups: always true, i.e. + const uint mask = (1u << 31) | (1u); + const uint targetValue = 1u; + return (Id & mask) != targetValue; + } + } + + 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 Type PropertyType => IsPropertyGroup ? PropertyGroupInstance.PropertyType : PropertyInstance.PropertyType; + public Type DeclaringType => IsPropertyGroup ? PropertyGroupInstance.DeclaringType : DotvvmPropertyIdAssignment.GetControlType(TypeId); + + 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() + { + if (IsPropertyGroup) + { + var pg = PropertyGroupInstance; + return $"[{Id:x8}]{pg.DeclaringType.Name}.{pg.Name}:{GroupMemberName}"; + } + else + { + return $"[{Id:x8}]{PropertyInstance.FullName}"; + } + } + public int CompareTo(DotvvmPropertyId other) => Id.CompareTo(other.Id); + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs new file mode 100644 index 0000000000..0d8e94de96 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Immutable; + +namespace DotVVM.Framework.Binding +{ + + static partial class DotvvmPropertyIdAssignment + { + 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 data_bind = 5; + + public static readonly ImmutableArray<(string Name, ushort ID)> List = ImmutableArray.Create( + ("id", id), + ("class", @class), + ("style", style), + ("name", name), + ("data-bind", data_bind) + ); + + public static ushort TryGetId(ReadOnlySpan attr) => + attr switch { + "id" => id, + "class" => @class, + "style" => style, + "name" => name, + "data-bind" => data_bind, + _ => 0, + }; + } + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs new file mode 100644 index 0000000000..f70033c439 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs @@ -0,0 +1,72 @@ +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; + +namespace DotVVM.Framework.Binding +{ + + static partial class DotvvmPropertyIdAssignment + { + public static class PropertyIds + { + // fields are looked-up automatically for type registered in TypeIds + + /// + public const uint DotvvmBindableObject_DataContext = TypeIds.DotvvmBindableObject << 16 | 1; + + /// + public const uint DotvvmControl_ID = TypeIds.DotvvmControl << 16 | 2; + /// + public const uint DotvvmControl_ClientID = TypeIds.DotvvmControl << 16 | 4; + /// + public const uint DotvvmControl_IncludeInPage = TypeIds.DotvvmControl << 16 | 6; + + /// + public const uint DotvvmControl_ClientIDMode = TypeIds.DotvvmControl << 16 | 1; + + /// + public const uint HtmlGenericControl_Visible = TypeIds.HtmlGenericControl << 16 | 2; + /// + public const uint HtmlGenericControl_InnerText = TypeIds.HtmlGenericControl << 16 | 4; + /// + public const uint HtmlGenericControl_HtmlCapability = TypeIds.HtmlGenericControl << 16 | 1; + + /// + public const uint Literal_Text = TypeIds.Literal << 16 | 2; + /// + public const uint Literal_FormatString = TypeIds.Literal << 16 | 4; + /// + public const uint Literal_RenderSpanElement = TypeIds.Literal << 16 | 6; + /// + public const uint ButtonBase_Click = TypeIds.ButtonBase << 16 | 2; + /// + public const uint ButtonBase_ClickArguments = TypeIds.ButtonBase << 16 | 4; + /// + public const uint ButtonBase_Text = TypeIds.ButtonBase << 16 | 8; + /// + public const uint ButtonBase_Enabled = TypeIds.ButtonBase << 16 | 1; + /// + public const uint ButtonBase_TextOrContentCapability = TypeIds.ButtonBase << 16 | 3; + + /// + public const uint Button_ButtonTagName = TypeIds.Button << 16 | 2; + /// + public const uint Button_IsSubmitButton = TypeIds.Button << 16 | 4; + /// + public const uint TextBox_Text = TypeIds.TextBox << 16 | 2; + /// + public const uint TextBox_Changed = TypeIds.TextBox << 16 | 4; + /// + public const uint TextBox_Type = TypeIds.TextBox << 16 | 6; + /// + public const uint TextBox_TextInput = TypeIds.TextBox << 16 | 8; + /// + public const uint TextBox_FormatString = TypeIds.TextBox << 16 | 10; + /// + public const uint TextBox_SelectAllOnFocus = TypeIds.TextBox << 16 | 12; + /// + public const uint TextBox_Enabled = TypeIds.TextBox << 16 | 1; + /// + public const uint TextBox_UpdateTextOnInput = TypeIds.TextBox << 16 | 3; + } + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs new file mode 100644 index 0000000000..f5ae9d9993 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Immutable; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; + +namespace DotVVM.Framework.Binding +{ + + static partial class DotvvmPropertyIdAssignment + { + 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 ushort Internal = 16; + + public static readonly ImmutableArray<(Type type, ushort id)> List = ImmutableArray.Create( + (typeof(DotvvmBindableObject), DotvvmBindableObject), + (typeof(DotvvmControl), DotvvmControl), + (typeof(HtmlGenericControl), HtmlGenericControl), + (typeof(RawLiteral), RawLiteral), + (typeof(Literal), Literal), + (typeof(ButtonBase), ButtonBase), + (typeof(Button), Button), + (typeof(LinkButton), LinkButton), + (typeof(TextBox), TextBox), + (typeof(RouteLink), RouteLink), + (typeof(CheckableControlBase), CheckableControlBase), + (typeof(CheckBox), CheckBox), + (typeof(Validator), Validator), + (typeof(Validation), Validation), + (typeof(ValidationSummary), ValidationSummary), + (typeof(Internal), Internal) + ); + } + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs index ab31b2ce1f..9b78ae6902 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -1,133 +1,48 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Numerics; +using System.Reflection; 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; using FastExpressionCompiler; 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); - - /// Returns true if the property does not have GetValue/SetValue overrides and is not inherited. That means it is sufficient - public bool CanUseFastAccessors - { - get - { - // properties: we encode this information as the LSB bit of the member ID (i.e. odd/even numbers) - // property groups: always true, i.e. - const uint mask = (1u << 31) | (1u); - const uint targetValue = 1u; - return (Id & mask) != targetValue; - } - } - 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 Type PropertyType => IsPropertyGroup ? PropertyGroupInstance.PropertyType : PropertyInstance.PropertyType; - public Type DeclaringType => IsPropertyGroup ? PropertyGroupInstance.DeclaringType : DotvvmPropertyIdAssignment.GetControlType(TypeId); - - 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() - { - if (IsPropertyGroup) - { - var pg = PropertyGroupInstance; - return $"[{Id:x8}]{pg.DeclaringType.Name}.{pg.Name}:{GroupMemberName}"; - } - else - { - return $"[{Id:x8}]{PropertyInstance.FullName}"; - } - } - public int CompareTo(DotvvmPropertyId other) => Id.CompareTo(other.Id); - } - - static class DotvvmPropertyIdAssignment + static partial class DotvvmPropertyIdAssignment { + /// Type and property group IDs bellow this are reserved for manual ID assignment + const int RESERVED_CONTROL_TYPES = 256; + /// Properties with ID bellow this are reserved for manual ID assignment (only makes sense for controls with manual type ID) + const int RESERVED_PROPERTY_COUNT = 32; const int DEFAULT_PROPERTY_COUNT = 16; - static readonly ConcurrentDictionary typeIds; + static readonly ConcurrentDictionary typeIds = new(concurrencyLevel: 1, capacity: 256); private static readonly object controlTypeRegisterLock = new object(); - private static int controlCounter = 256; // first 256 types are reserved for DotVVM controls + private static int controlCounter = RESERVED_CONTROL_TYPES; // 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 int groupCounter = RESERVED_CONTROL_TYPES; // 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, - }; + static readonly ConcurrentDictionary propertyGroupMemberIds = new(concurrencyLevel: 1, capacity: 256); private static readonly object groupMemberRegisterLock = new object(); static string?[] propertyGroupMemberNames = new string[1024]; static DotvvmPropertyIdAssignment() { - foreach (var n in propertyGroupMemberIds) + foreach (var (type, id) in TypeIds.List) { - propertyGroupMemberNames[n.Value] = n.Key; + typeIds[type] = id; + } + foreach (var (name, id) in GroupMembers.List) + { + propertyGroupMemberIds[name] = id; + propertyGroupMemberNames[id] = name; } - - 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 @@ -321,6 +236,11 @@ public static void RegisterTypes(ReadOnlySpan types, Span ids) 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]; + if (id < RESERVED_CONTROL_TYPES) + { + controls[id].counterStandard = DEFAULT_PROPERTY_COUNT; + controls[id].counterNonStandard = DEFAULT_PROPERTY_COUNT; + } typeIds[type] = id; } ids[i] = id; @@ -337,10 +257,24 @@ public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) var typeId = RegisterType(property.DeclaringType); ref ControlTypeInfo control = ref controls[typeId]; - lock (control.locker) // single control registrations are sequential anyway + lock (control.locker) // single control registrations are sequential anyway (most likely) { uint id; - if (canUseDirectAccess) + if (typeId < RESERVED_CONTROL_TYPES && + typeof(PropertyIds).GetField(property.DeclaringType.Name + "_" + property.Name, BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is {} predefinedId) + { + id = (uint)predefinedId; + if ((id & 0xffff) == 0) + throw new InvalidOperationException($"Predefined property ID of {property} cannot be 0."); + if (id >> 16 != typeId) + throw new InvalidOperationException($"Predefined property ID of {property} does not match the property declaring type ID."); + if ((id & 0xffff) > DEFAULT_PROPERTY_COUNT) + throw new InvalidOperationException($"Predefined property ID of {property} is too high (there is only {RESERVED_PROPERTY_COUNT} reserved slots)."); + if (canUseDirectAccess != (id % 2 == 0)) + throw new InvalidOperationException($"Predefined property ID of {property} does not match the property canUseDirectAccess={canUseDirectAccess}. The ID must be {(canUseDirectAccess ? "even" : "odd")} number."); + id = id & 0xffff; + } + else if (canUseDirectAccess) { control.counterStandard += 1; id = control.counterStandard * 2; @@ -354,11 +288,11 @@ public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) ThrowTooManyException(property); // resize arrays (we hold a write lock, but others may be reading in parallel) - if (id >= control.properties.Length) + while (id >= control.properties.Length) { VolatileResize(ref control.properties, control.properties.Length * 2); } - if (id / 64 >= control.inheritedBitmap.Length) + while (id / 64 >= control.inheritedBitmap.Length) { Debug.Assert(control.inheritedBitmap.Length == control.standardBitmap.Length); Debug.Assert(control.inheritedBitmap.Length == control.activeBitmap.Length); @@ -454,7 +388,7 @@ private static ushort PredefinedPropertyGroupMemberId(ReadOnlySpan name) case "id": return GroupMembers.id; case "style": return GroupMembers.style; case "name": return GroupMembers.name; - case "data-bind": return GroupMembers.databind; + case "data-bind": return GroupMembers.data_bind; default: return 0; } } @@ -513,44 +447,11 @@ private struct ControlTypeInfo public ulong[] inheritedBitmap; public ulong[] standardBitmap; public ulong[] activeBitmap; + /// TODO split struct to part used during registration and part at runtime for lookups public object locker; public Type controlType; public uint counterStandard; public uint counterNonStandard; } - - 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; - } - - public static class PropertyIds - { - public const uint DotvvmBindableObject_DataContext = TypeIds.DotvvmBindableObject << 16 | 1; - } } } From 598b8521f7f2d793ac2af036c27d841fa7cf451c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 15 Mar 2025 23:34:02 +0100 Subject: [PATCH 06/11] PoC usage of manual property IDs in TouchProperty --- src/Framework/Framework/Controls/DotvvmControl.cs | 4 ++-- src/Framework/Framework/Controls/HtmlGenericControl.cs | 8 ++++---- src/Framework/Framework/Controls/Literal.cs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Framework/Framework/Controls/DotvvmControl.cs b/src/Framework/Framework/Controls/DotvvmControl.cs index f09f0f7202..dd575af9d2 100644 --- a/src/Framework/Framework/Controls/DotvvmControl.cs +++ b/src/Framework/Framework/Controls/DotvvmControl.cs @@ -233,9 +233,9 @@ protected struct RenderState [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static bool TouchProperty(DotvvmPropertyId property, object? val, ref RenderState r) { - if (property == DotvvmControl.IncludeInPageProperty.Id) + if (property == DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_IncludeInPage) r.IncludeInPage = val; - else if (property == DotvvmControl.DataContextProperty.Id) + else if (property == DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext) r.DataContext = val as IValueBinding; else if (DotvvmPropertyIdAssignment.IsActive(property)) { diff --git a/src/Framework/Framework/Controls/HtmlGenericControl.cs b/src/Framework/Framework/Controls/HtmlGenericControl.cs index cb8230eb47..d5c819a162 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -183,13 +183,13 @@ public bool RenderOnServer(HtmlGenericControl @this) [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool TouchProperty(DotvvmPropertyId prop, object? value, ref RenderState r) { - if (prop == VisibleProperty.Id) + if (prop == DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_Visible) r.Visible = value; - else if (prop == ClientIDProperty.Id) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ClientID) r.ClientId = value; - else if (prop == IDProperty.Id && value != null) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ID && value != null) r.HasId = true; - else if (prop == InnerTextProperty.Id) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_InnerText) r.InnerText = value; else if (prop == PostBack.UpdateProperty.Id) r.HasPostbackUpdate = (bool)this.EvalPropertyValue(prop, value)!; diff --git a/src/Framework/Framework/Controls/Literal.cs b/src/Framework/Framework/Controls/Literal.cs index a0c795f071..293dd46d68 100644 --- a/src/Framework/Framework/Controls/Literal.cs +++ b/src/Framework/Framework/Controls/Literal.cs @@ -120,11 +120,11 @@ bool isFormattedType(Type? type) => [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool TouchProperty(DotvvmPropertyId prop, object? value, ref RenderState r) { - if (prop == TextProperty.Id) + if (prop == DotvvmPropertyIdAssignment.PropertyIds.Literal_Text) r.Text = value; - else if (prop == RenderSpanElementProperty.Id) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.Literal_RenderSpanElement) r.RenderSpanElement = (bool)EvalPropertyValue(RenderSpanElementProperty, value)!; - else if (prop == FormatStringProperty.Id) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.Literal_FormatString) r.HasFormattingStuff = true; else if (base.TouchProperty(prop, value, ref r.HtmlState)) { } else if (DotvvmControl.TouchProperty(prop, value, ref r.BaseState)) { } From ddf4d6a3c421ebb4bed06e2e4e672b85756ed0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 26 Apr 2025 00:58:47 +0200 Subject: [PATCH 07/11] Fix build on net4, add some comments, improve readablity --- .../Framework/Binding/DotvvmProperty.cs | 4 +- .../Framework/Binding/DotvvmPropertyId.cs | 86 ++++++++++-- ...DotvvmPropertyIdAssignment.GroupMembers.cs | 2 + .../DotvvmPropertyIdAssignment.PropertyIds.cs | 4 + .../DotvvmPropertyIdAssignment.TypeIds.cs | 4 +- .../Binding/DotvvmPropertyIdAssignment.cs | 45 ++++--- .../Binding/VirtualPropertyGroupDictionary.cs | 62 ++++++--- .../ControlTree/DefaultControlResolver.cs | 42 ++---- .../ControlTree/DotvvmPropertyGroup.cs | 21 ++- .../DefaultViewCompilerCodeEmitter.cs | 8 +- .../Controls/DotvvmBindableObject.cs | 123 +++++++++--------- .../Controls/DotvvmControlProperties.cs | 59 +++++++-- .../Controls/PropertyImmutableHashtable.cs | 76 ++++++----- src/Framework/Framework/Controls/RouteLink.cs | 1 - .../Framework/DotVVM.Framework.csproj | 5 +- .../DotVVM.Samples.BasicSamples.Owin.csproj | 2 +- src/Tests/Runtime/CapabilityPropertyTests.cs | 42 ++++++ src/Tests/Runtime/DotvvmPropertyTests.cs | 86 +++++++++++- ...imeErrorTests.CantFindDataContextSpace.txt | 2 +- 19 files changed, 463 insertions(+), 211 deletions(-) diff --git a/src/Framework/Framework/Binding/DotvvmProperty.cs b/src/Framework/Framework/Binding/DotvvmProperty.cs index 61073d36f4..d4ced3b1d6 100644 --- a/src/Framework/Framework/Binding/DotvvmProperty.cs +++ b/src/Framework/Framework/Binding/DotvvmProperty.cs @@ -233,7 +233,7 @@ public bool IsOwnedByCapability(DotvvmCapabilityProperty capability) => return DefaultValue; } - private bool IsSetInHierarchy(DotvvmBindableObject control) + private bool IsSetInherited(DotvvmBindableObject control) { for (var p = control.Parent; p is not null; p = p.Parent) { @@ -255,7 +255,7 @@ public virtual bool IsSet(DotvvmBindableObject control, bool inherit = true) if (IsValueInherited && inherit) { - return IsSetInHierarchy(control); + return IsSetInherited(control); } return false; diff --git a/src/Framework/Framework/Binding/DotvvmPropertyId.cs b/src/Framework/Framework/Binding/DotvvmPropertyId.cs index 416bafd4cd..4fe42c5bf7 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyId.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyId.cs @@ -1,31 +1,67 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Controls; namespace DotVVM.Framework.Binding { + /// + /// Represents a unique ID, used as a key for . + /// + /// + /// The ID is a 32-bit unsigned integer, where: + /// - the most significant bit indicates whether the ID is of a property group (1) or a classic property (0) + /// - the next upper 15 bits are the (for classic properties) or (for property groups) + /// - the lower 16 bits are the , ID of the string key for property groups or the ID of the property for classic properties + /// - in case of classic properties, the LSB bit of the member ID indicates whether the property has GetValue/SetValue overrides and is not inherited (see ) + /// public readonly struct DotvvmPropertyId: IEquatable, IEquatable, IComparable { + /// Numeric representation of the property ID. public readonly uint Id; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public DotvvmPropertyId(uint id) { Id = id; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public DotvvmPropertyId(ushort typeOrGroupId, ushort memberId) { Id = ((uint)typeOrGroupId << 16) | memberId; } + /// Returns true if the property is a other type of property. [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 IsPropertyGroup + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (int)Id < 0; + } + /// Returns the ID of the property declaring type + public ushort TypeId + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ushort)(Id >> 16); + } + /// Returns the ID of the property group. + public ushort GroupId + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ushort)((Id >> 16) ^ 0x8000); + } + /// Returns the ID of the property member, i.e. property-in-type id for classic properties, or the name ID for property groups. + public ushort MemberId + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ushort)(Id & 0xFFFF); + } - /// Returns true if the property does not have GetValue/SetValue overrides and is not inherited. That means it is sufficient + /// Returns true if the property does not have GetValue/SetValue overrides and is not inherited. That means it is sufficient to call properties.TryGet instead going through the DotvvmProperty.GetValue dynamic dispatch public bool CanUseFastAccessors { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { // properties: we encode this information as the LSB bit of the member ID (i.e. odd/even numbers) @@ -36,27 +72,59 @@ public bool CanUseFastAccessors } } - public bool IsZero => Id == 0; + /// Returns true if the ID is default. This ID is invalid for most purposes and can be used as a sentinel value. + public bool IsZero + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Id == 0; + } + /// Returns the instance for this ID. Note that a new might need to be allocated. public DotvvmProperty PropertyInstance => DotvvmPropertyIdAssignment.GetProperty(Id) ?? throw new Exception($"Property with ID {Id} not registered."); + + /// Returns the instance for this ID, or null if the ID is of a classic property. + public DotvvmPropertyGroup? PropertyGroupInstance => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetPropertyGroup(GroupId); + + /// Returns the name (string dictionary key) of the property group member, or null if the ID is of a classic property. public string? GroupMemberName => !IsPropertyGroup ? null : DotvvmPropertyIdAssignment.GetGroupMemberName(MemberId); + /// Returns the type of the property. public Type PropertyType => IsPropertyGroup ? PropertyGroupInstance.PropertyType : PropertyInstance.PropertyType; + + /// Returns the property declaring type. public Type DeclaringType => IsPropertyGroup ? PropertyGroupInstance.DeclaringType : DotvvmPropertyIdAssignment.GetControlType(TypeId); + /// Returns the property declaring type. + [MemberNotNullWhen(true, nameof(PropertyGroupInstance), nameof(GroupMemberName))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsInPropertyGroup(ushort id) => (this.Id >> 16) == ((uint)id | 0x80_00u); + /// Constucts property ID from a property group name and a name ID. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DotvvmPropertyId CreatePropertyGroupId(ushort groupId, ushort memberId) => new DotvvmPropertyId((ushort)(groupId | 0x80_00), memberId); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator DotvvmPropertyId(uint id) => new DotvvmPropertyId(id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator uint(DotvvmPropertyId id) => id.Id; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(DotvvmPropertyId other) => Id == other.Id; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(uint other) => Id == other; + public override bool Equals(object? obj) => obj is DotvvmPropertyId id && Equals(id); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() => (int)Id; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(DotvvmPropertyId left, DotvvmPropertyId right) => left.Equals(right); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(DotvvmPropertyId left, DotvvmPropertyId right) => !left.Equals(right); public override string ToString() @@ -64,13 +132,15 @@ public override string ToString() if (IsPropertyGroup) { var pg = PropertyGroupInstance; - return $"[{Id:x8}]{pg.DeclaringType.Name}.{pg.Name}:{GroupMemberName}"; + return $"[{TypeId:x4}_{MemberId:x4}]{pg.DeclaringType.Name}.{pg.Name}:{GroupMemberName}"; } else { - return $"[{Id:x8}]{PropertyInstance.FullName}"; + return $"[{TypeId:x4}_{MemberId:x4}]{PropertyInstance.FullName}"; } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int CompareTo(DotvvmPropertyId other) => Id.CompareTo(other.Id); } } diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs index 0d8e94de96..60ba6b8a1a 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs @@ -31,6 +31,8 @@ public static ushort TryGetId(ReadOnlySpan attr) => "data-bind" => data_bind, _ => 0, }; + + // TODO } } } diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs index f70033c439..0059499d72 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs @@ -67,6 +67,10 @@ public static class PropertyIds public const uint TextBox_Enabled = TypeIds.TextBox << 16 | 1; /// public const uint TextBox_UpdateTextOnInput = TypeIds.TextBox << 16 | 3; + /// + public const uint RenderSettings_Mode = TypeIds.RenderSettings << 16 | 1; + + // TODO } } } diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs index f5ae9d9993..92df51bbfd 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs @@ -26,6 +26,7 @@ public static class TypeIds public const ushort Validation = 14; public const ushort ValidationSummary = 15; public const ushort Internal = 16; + public const ushort RenderSettings = 17; public static readonly ImmutableArray<(Type type, ushort id)> List = ImmutableArray.Create( (typeof(DotvvmBindableObject), DotvvmBindableObject), @@ -43,7 +44,8 @@ public static class TypeIds (typeof(Validator), Validator), (typeof(Validation), Validation), (typeof(ValidationSummary), ValidationSummary), - (typeof(Internal), Internal) + (typeof(Internal), Internal), + (typeof(RenderSettings), RenderSettings) ); } } diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs index 9b78ae6902..0f6e694541 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -13,7 +13,7 @@ namespace DotVVM.Framework.Binding { - static partial class DotvvmPropertyIdAssignment + internal static partial class DotvvmPropertyIdAssignment { /// Type and property group IDs bellow this are reserved for manual ID assignment const int RESERVED_CONTROL_TYPES = 256; @@ -46,6 +46,7 @@ static DotvvmPropertyIdAssignment() } #region Optimized metadata accessors + /// Equivalent to public static bool IsInherited(DotvvmPropertyId propertyId) { if (propertyId.CanUseFastAccessors) @@ -54,6 +55,8 @@ public static bool IsInherited(DotvvmPropertyId propertyId) return BitmapRead(controls[propertyId.TypeId].inheritedBitmap, propertyId.MemberId); } + /// Returns if the DotvvmProperty uses standard GetValue/SetValue method and we can avoid the dynamic dispatch + /// public static bool UsesStandardAccessors(DotvvmPropertyId propertyId) { if (propertyId.CanUseFastAccessors) @@ -68,9 +71,10 @@ public static bool UsesStandardAccessors(DotvvmPropertyId propertyId) } } + /// Returns if the given property is of the or type public static bool IsActive(DotvvmPropertyId propertyId) { - Debug.Assert(DotvvmPropertyIdAssignment.GetProperty(propertyId) != null, $"Property {propertyId} not registered."); + Debug.Assert(GetProperty(propertyId) != null, $"Property {propertyId} not registered."); ulong[] bitmap; uint index; if (propertyId.IsPropertyGroup) @@ -85,7 +89,8 @@ public static bool IsActive(DotvvmPropertyId propertyId) } return BitmapRead(bitmap, index); } - + + /// Returns the DotvvmProperty with a given ID, or returns null if no such property exists. New instance of might be created. public static DotvvmProperty? GetProperty(DotvvmPropertyId id) { if (id.IsPropertyGroup) @@ -111,6 +116,7 @@ public static bool IsActive(DotvvmPropertyId propertyId) } } + /// Returns the or with the given id public static Compilation.IControlAttributeDescriptor? GetPropertyOrPropertyGroup(DotvvmPropertyId id) { if (id.IsPropertyGroup) @@ -132,6 +138,7 @@ public static bool IsActive(DotvvmPropertyId propertyId) } } + /// Returns the value of the property or property group. If the property is not set, returns the default value. public static object? GetValueRaw(DotvvmBindableObject obj, DotvvmPropertyId id, bool inherit = true) { if (id.CanUseFastAccessors) @@ -148,6 +155,7 @@ public static bool IsActive(DotvvmPropertyId propertyId) } } + /// Returns the value of the property or property group. If the property is not set, returns the default value. public static MarkupOptionsAttribute GetMarkupOptions(DotvvmPropertyId id) { if (id.IsPropertyGroup) @@ -164,6 +172,7 @@ public static MarkupOptionsAttribute GetMarkupOptions(DotvvmPropertyId id) } /// 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) @@ -202,6 +211,7 @@ public static ushort RegisterType(Type type) return unlikely(type); + [MethodImpl(MethodImplOptions.NoInlining)] static ushort unlikely(Type type) { var types = MemoryMarshal.CreateReadOnlySpan(ref type, 1); @@ -219,7 +229,14 @@ public static void RegisterTypes(ReadOnlySpan types, Span ids) { if (controlCounter + types.Length >= controls.Length) { - VolatileResize(ref controls, 1 << (BitOperations.Log2((uint)(controlCounter + types.Length)) + 1)); +#if NET6_0_OR_GREATER + var nextPow2 = 1 << (BitOperations.Log2((uint)(controlCounter + types.Length)) + 1); +#else + var nextPow2 = types.Length * 2; + while (nextPow2 < controlCounter + types.Length) + nextPow2 *= 2; +#endif + VolatileResize(ref controls, nextPow2); } for (int i = 0; i < types.Length; i++) { @@ -247,6 +264,7 @@ public static void RegisterTypes(ReadOnlySpan types, Span ids) } } } + public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) { if (property.GetType() == typeof(GroupedDotvvmProperty)) @@ -318,6 +336,7 @@ static void ThrowTooManyException(DotvvmProperty property) => } private static readonly ConcurrentDictionary cacheTypeCanUseDirectAccess = new(concurrencyLevel: 1, capacity: 10); + /// Does the property use the default GetValue/SetValue methods? public static (bool getter, bool setter) TypeCanUseDirectAccess(Type propertyType) { @@ -380,22 +399,9 @@ private static void VolatileResize(ref T[] array, int newSize) #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.data_bind; - default: return 0; - } - } - public static ushort GetGroupMemberId(string name, bool registerIfNotFound) { - var id = PredefinedPropertyGroupMemberId(name); + var id = GroupMembers.TryGetId(name); if (id != 0) return id; if (propertyGroupMemberIds.TryGetValue(name, out id)) @@ -444,8 +450,11 @@ static void BitmapSet(ulong[] bitmap, uint index) private struct ControlTypeInfo { public DotvvmProperty?[] properties; + /// Bitmap for public ulong[] inheritedBitmap; + /// Bitmap for public ulong[] standardBitmap; + /// Bitmap storing if property is public ulong[] activeBitmap; /// TODO split struct to part used during registration and part at runtime for lookups public object locker; diff --git a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs index 42326ac175..8722f37415 100644 --- a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs +++ b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs @@ -9,7 +9,10 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Utils; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +#if Vectorize using System.Runtime.Intrinsics; +#endif namespace DotVVM.Framework.Binding { @@ -31,18 +34,13 @@ DotvvmPropertyId GetMemberId(string key, bool createNew = false) 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.PropertyGroup(group.Id)) { - yield return GetMemberName(p); + yield return DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; } } } @@ -191,7 +189,7 @@ public void CopyFrom(IEnumerable>> v public static IDictionary CreateValueDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) { Dictionary result; -#if NET8_0_OR_GREATER +#if Vectorize // don't bother counting without vector instructions if (Vector256.IsHardwareAccelerated) { @@ -209,7 +207,7 @@ public static IDictionary CreateValueDictionary(DotvvmBindableOb { if (p.IsInPropertyGroup(group.Id)) { - var name = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + var name = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; var valueObj = control.EvalPropertyValue(group, valueRaw); if (valueObj is TValue value) result.Add(name, value); @@ -223,7 +221,7 @@ public static IDictionary CreateValueDictionary(DotvvmBindableOb public static IDictionary> CreatePropertyDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) { Dictionary> result; -#if NET8_0_OR_GREATER +#if Vectorize // don't bother counting without vector instructions if (Vector256.IsHardwareAccelerated) { @@ -241,7 +239,7 @@ public static IDictionary> CreatePropertyDictiona { if (p.IsInPropertyGroup(group.Id)) { - var name = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + var name = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; result.Add(name, ValueOrBinding.FromBoxedValue(valRaw)); } } @@ -332,14 +330,10 @@ 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.PropertyGroup(group.Id)) - { - var name = GetMemberName(p); - yield return new KeyValuePair(name, (TValue)control.EvalPropertyValue(group, value)!); - } - } + public Enumerator GetEnumerator() => + new Enumerator(control, group, control.properties.EnumeratePropertyGroup(group.Id)); + + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// Enumerates all keys and values, without evaluating the bindings. @@ -386,7 +380,7 @@ public struct RawValuesEnumerator : IEnumerator> get { var (p, value) = inner.Current; - var mem = DotvvmPropertyIdAssignment.GetGroupMemberName((ushort)(p.Id & 0xFF_FF))!; + var mem = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; return new KeyValuePair(mem, value); } } @@ -398,6 +392,36 @@ public RawValuesEnumerator(DotvvmControlPropertyIdGroupEnumerator dotvvmControlP this.inner = dotvvmControlPropertyIdEnumerator; } + public bool MoveNext() => inner.MoveNext(); + public void Reset() => inner.Reset(); + public void Dispose() => inner.Dispose(); + } + public struct Enumerator : IEnumerator> + { + private readonly DotvvmPropertyGroup group; + private readonly DotvvmBindableObject control; + private DotvvmControlPropertyIdGroupEnumerator inner; + + public KeyValuePair Current + { + get + { + var (p, valueRaw) = inner.Current; + var mem = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; + var value = (TValue)control.EvalPropertyValue(group, valueRaw)!; + return new KeyValuePair(mem, value); + } + } + + object IEnumerator.Current => Current; + + public Enumerator(DotvvmBindableObject control, DotvvmPropertyGroup group, DotvvmControlPropertyIdGroupEnumerator dotvvmControlPropertyIdEnumerator) + { + this.control = control; + this.group = group; + 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/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs index a45e954028..7b3fdf11e6 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs @@ -190,7 +190,7 @@ private void InvokeStaticConstructorsOnAllControls(List assemblies) var paralelismLimiter = new SemaphoreSlim(Environment.ProcessorCount); - var controlIdsAssigned = Enumerable.Range(0, assemblies.Count).Select(_ => new TaskCompletionSource()).ToArray(); + 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(); @@ -207,10 +207,16 @@ private void InvokeStaticConstructorsOnAllControls(List assemblies) if (i > 0) await controlIdsAssigned[i - 1].Task; var controlIds = new ushort[controls.Count]; - DotvvmPropertyIdAssignment.RegisterTypes(CollectionsMarshal.AsSpan(controls), controlIds); + DotvvmPropertyIdAssignment.RegisterTypes( +#if NET6_0_OR_GREATER + CollectionsMarshal.AsSpan(controls), +#else + controls.ToArray().AsSpan(), +#endif + controlIds); // let the next assembly run - controlIdsAssigned[i].SetResult(); + controlIdsAssigned[i].SetResult(true); foreach (var type in controls) { @@ -287,36 +293,6 @@ private static void RegisterCapabilitiesFromInterfaces(Type type) } } - private Assembly[] GetAllRelevantAssemblies(string dotvvmAssembly) - { -#if DotNetCore - var assemblies = compiledAssemblyCache.GetAllAssemblies(); -#else - var loadedAssemblies = compiledAssemblyCache.GetAllAssemblies() - .Where(a => ReferencesAssembly(a.GetReferencedAssemblies(), dotvvmAssembly)); - - var visitedAssemblies = new HashSet(); - - // ReflectionUtils.GetAllAssemblies() in netframework returns only assemblies which have already been loaded into - // the current AppDomain, to return all assemblies we traverse recursively all referenced Assemblies - var assemblies = loadedAssemblies - .SelectRecursively(a => a.GetReferencedAssemblies().Where(an => visitedAssemblies.Add(an.FullName)).Select(an => { - try - { - return Assembly.Load(an); - } - catch (Exception ex) - { - throw new Exception($"Unable to load assembly '{an.FullName}' referenced by '{a.FullName}'.", ex); - } - })) - .Where(a => ReferencesAssembly(a.GetReferencedAssemblies(), dotvvmAssembly)) - .Distinct() - .ToArray(); -#endif - return assemblies; - } - /// /// After all DotvvmProperties have been registered, those marked with PropertyAliasAttribute can be resolved. /// diff --git a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs index dad95f81c7..ad71ace00d 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs @@ -43,7 +43,7 @@ public class DotvvmPropertyGroup : IPropertyGroupDescriptor public bool IsBindingProperty { get; } internal ushort Id { get; } - private readonly 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; } @@ -104,19 +104,14 @@ public GroupedDotvvmProperty GetDotvvmProperty(string name) public GroupedDotvvmProperty GetDotvvmProperty(ushort nameId) { - while (true) + if (generatedProperties.TryGetValue(nameId, out var result)) { - 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))); - } + return result; + } + else + { + generatedProperties.TryAdd(nameId, CreateMemberProperty(nameId)); + return generatedProperties[nameId]; } } diff --git a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs index 804ee41727..8e6af33439 100644 --- a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs +++ b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompilerCodeEmitter.cs @@ -188,12 +188,12 @@ public void CommitDotvvmProperties(string name) /// Set DotVVM properties as array of keys and array of values private bool TryEmitPerfectHashAssignment(ParameterExpression control, List<(DotvvmProperty prop, Expression value)> properties) { - if (properties.Count > PropertyImmutableHashtable.MaxArrayTableSize) + if (properties.Count > PropertyDictionaryImpl.MaxArrayTableSize) { return false; } - var (_, keys, values) = PropertyImmutableHashtable.CreateTableWithValues(properties.Select(p => p.prop.Id).ToArray(), properties.Select(p => p.value).ToArray()); + var (_, keys, values) = PropertyDictionaryImpl.CreateTableWithValues(properties.Select(p => p.prop.Id).ToArray(), properties.Select(p => p.value).ToArray()); Expression valueExpr; bool ownsValues; @@ -214,7 +214,7 @@ private bool TryEmitPerfectHashAssignment(ParameterExpression control, List<(Dot 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(false), EmitValue(ownsValues)); + var magicSetValueCall = Expression.Call(typeof(PropertyDictionaryImpl), nameof(PropertyDictionaryImpl.SetValuesToDotvvmControl), emptyTypeArguments, Expression.Convert(control, typeof(DotvvmBindableObject)), keyExpr, valueExpr, EmitValue(false), EmitValue(ownsValues)); EmitStatement(magicSetValueCall); return true; @@ -269,7 +269,7 @@ private void EmitDictionaryAssignment(ParameterExpression control, List<(DotvvmP } // PropertyImmutableHashtable.SetValuesToDotvvmControl(control, dict) - var magicSetValueCall = Expression.Call(typeof(PropertyImmutableHashtable), nameof(PropertyImmutableHashtable.SetValuesToDotvvmControl), emptyTypeArguments, + var magicSetValueCall = Expression.Call(typeof(PropertyDictionaryImpl), nameof(PropertyDictionaryImpl.SetValuesToDotvvmControl), emptyTypeArguments, /*control:*/ Expression.Convert(control, typeof(DotvvmBindableObject)), /*dict:*/ dict, /*owns:*/ EmitValue(variables.Count > 0)); diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index 58fccd96f9..355f8768cf 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; +using DotVVM.Core.Storage; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; @@ -34,7 +36,19 @@ public abstract class DotvvmBindableObject: IDotvvmObjectLike /// public virtual bool RenderOnServer { - get { return (RenderMode)GetValue(RenderSettings.ModeProperty)! == RenderMode.Server; } + get + { + for (var c = this; c != null; c = c.Parent) + { + if (c.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.RenderSettings_Mode, out var value)) + { + if (value is IBinding binding) + value = c.EvalBinding(binding, isDataContext: false); + return ((RenderMode)value!) == RenderMode.Server; + } + } + return false; + } } /// @@ -44,7 +58,7 @@ public virtual bool RenderOnServer public DotvvmBindableObject? Parent { get; set; } // WORKAROUND: Roslyn is unable to cache the delegate itself - private static Func _dotvvmProperty_ResolveProperties = DotvvmProperty.ResolveProperties; + private static readonly Func _dotvvmProperty_ResolveProperties = DotvvmProperty.ResolveProperties; /// /// Gets all properties declared on this class or on any of its base classes. @@ -72,17 +86,20 @@ public bool IsPropertySet(DotvvmProperty property, bool inherit = true) [MarkupOptions(AllowHardCodedValue = false, AllowResourceBinding = true)] public object? DataContext { - get { + get + { for (var c = this; c != null; c = c.Parent) { - if (c.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var value)) + if (c.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, out var value)) { - return c.EvalPropertyValue(DotvvmBindableObject.DataContextProperty, value); + return value is IBinding binding + ? c.EvalBinding(binding, isDataContext: true) + : value; } } return null; } - set { this.properties.Set(DataContextProperty, value); } + set { this.properties.Set(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, value); } } DotvvmBindableObject IDotvvmObjectLike.Self => this; @@ -98,80 +115,58 @@ public T GetValue(DotvvmProperty property, bool inherit = true) } /// If the object is IBinding and the property is not of type IBinding, it is evaluated. +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] // let PGO specialize over value independently of call-site +#endif internal object? EvalPropertyValue(DotvvmProperty property, object? value) { - if (property.IsBindingProperty) 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) - 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}."); - } - } + if (!property.IsBindingProperty && value is IBinding binding) + return EvalBinding(binding, property == DataContextProperty); return value; } /// If the object is IBinding and the property is not of type IBinding, it is evaluated. +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] // let PGO specialize over value independently of call-site +#endif 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}."); - } - } + if (!property.IsBindingProperty && value is IBinding binding) + return EvalBinding(binding, isDataContext: false); return value; } /// If the object is IBinding and the property is not of type IBinding, it is evaluated. +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] // let PGO specialize over value independently of call-site +#endif internal object? EvalPropertyValue(DotvvmPropertyId property, object? value) { - if (DotvvmPropertyIdAssignment.IsBindingProperty(property)) return value; - if (value is IBinding) + if (value is IBinding binding && !DotvvmPropertyIdAssignment.IsBindingProperty(property)) + return EvalBinding(binding, property.Id == DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext); + return value; + } + + private object? EvalBinding(IBinding binding, bool isDataContext) + { + DotvvmBindableObject control = this; + // DataContext is always bound to it's parent + if (isDataContext) + control = control.Parent ?? throw new DotvvmControlException(this, "Cannot set DataContext binding on the root control"); + // handle binding + if (binding is IStaticValueBinding resourceBinding) { - 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 resourceBinding.Evaluate(control); } - return value; + else if (binding is ICommandBinding command) + { + return command.GetCommandDelegate(control); + } + else + { + throw new NotSupportedException($"Cannot evaluate binding {binding} of type {binding.GetType().Name}."); + } + } /// diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index 25c33bdba0..922ef29c5c 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -12,7 +12,7 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Utils; -using Impl = DotVVM.Framework.Controls.PropertyImmutableHashtable; +using Impl = DotVVM.Framework.Controls.PropertyDictionaryImpl; namespace DotVVM.Framework.Controls { @@ -130,11 +130,10 @@ private readonly void CheckInvariant() public void AssignBulk(DotvvmPropertyId[] keys, object?[] values, bool ownsKeys, bool ownsValues) { + if (keys is null || values is null || !(keys.Length is 8 or 16) || keys.Length != values.Length || values.GetType() != typeof(object[])) + throwArgumentError(keys, values); + CheckInvariant(); - // The our unsafe memory accesses are 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(DotvvmPropertyId[])); - Debug.Assert(keys.Length == values.Length); if (this.values == null || Object.ReferenceEquals(this.keys, keys)) { // empty -> fast assignment @@ -157,10 +156,27 @@ public void AssignBulk(DotvvmPropertyId[] keys, object?[] values, bool ownsKeys, } } CheckInvariant(); + + [DoesNotReturn, MethodImpl(NoInlining)] + void throwArgumentError(DotvvmPropertyId[]? keys, object?[]? values) + { + ThrowHelpers.ArgumentNull(nameof(keys)); + ThrowHelpers.ArgumentNull(nameof(values)); + if (keys.Length is not 8 and not 16) + throw new ArgumentException($"The length of keys array must be 8 or 16.", nameof(keys)); + if (keys.Length != values.Length) + throw new ArgumentException($"The length of values array must be the same.", nameof(keys)); + // The our unsafe memory accesses are quite likely to mess up with array covariance, just make sure we don't encounter that + if (values.GetType() != typeof(object[])) + throw new ArgumentException($"The values array must be of type {typeof(object[])}.", nameof(values)); + throw new Exception("Unknown error"); + } } public void AssignBulk(Dictionary values, bool owns) { + ThrowHelpers.ArgumentNull(values); + CheckInvariant(); if (this.values == null || object.ReferenceEquals(this.values, values)) { @@ -266,7 +282,7 @@ public readonly int CountPropertyGroup(ushort groupId) CheckInvariant(); if (state == TableState.Array8) { - return Impl.CountPropertyGroup8(this.keys, groupId); + return Impl.CountPropertyGroup8(this.keys!, groupId); } return CountPropertyGroupOutlined(groupId); } @@ -303,9 +319,14 @@ public readonly bool TryGet(DotvvmPropertyId p, out object? value) if (state == TableState.Array8) { var index = Impl.FindSlot8(this.keys!, p); + Debug.Assert(index < 8); if (index >= 0) { +#if NET6_0_OR_GREATER + value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArray), index); +#else value = valuesAsArray[index]; +#endif return true; } else @@ -366,6 +387,7 @@ public void Set(DotvvmPropertyId p, object? value) { var keys = this.keys!; var slot = Impl.FindSlotOrFree8(keys, p, out var exists); + Debug.Assert(slot < 8); if (slot >= 0) { if (!exists) @@ -373,10 +395,10 @@ public void Set(DotvvmPropertyId p, object? value) if (!ownsKeys) keys = CloneKeys(); // arrays are always size >= 8 - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(keys), slot) = p; + Unsafe.Add(ref Impl.UnsafeArrayReference(keys), slot) = p; } this.OwnValues(); - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(this.valuesAsArray), slot) = value; // avoid covariance check + Unsafe.Add(ref Impl.UnsafeArrayReference(this.valuesAsArray), slot) = value; // avoid covariance check CheckInvariant(); Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); return; @@ -396,6 +418,7 @@ private void SetOutlined(DotvvmPropertyId p, object? value) var slot = state == TableState.Array8 ? Impl.FindSlotOrFree8(keys, p, out var exists) : Impl.FindSlotOrFree16(keys, p, out exists); + Debug.Assert(slot < 16); if (slot >= 0) { Debug.Assert(slot < keys.Length && slot < valuesAsArray.Length, $"Slot {slot} is out of range for keys {keys.Length} and values {valuesAsArray.Length} (prop={p}, value={value})"); @@ -456,6 +479,7 @@ public bool TryAdd(DotvvmPropertyId p, object? value) Debug.Assert(values!.GetType() == typeof(object[])); Debug.Assert(keys is {}); var slot = Impl.FindSlotOrFree8(this.keys, p, out var exists); + Debug.Assert(slot < 8); if (slot >= 0) { if (exists) @@ -465,8 +489,8 @@ public bool TryAdd(DotvvmPropertyId p, object? value) OwnValues(); OwnKeys(); // arrays are always length >= 8 - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(keys), slot) = p; - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArray), slot) = value; // avoid covariance check + Unsafe.Add(ref Impl.UnsafeArrayReference(keys), slot) = p; + Unsafe.Add(ref Impl.UnsafeArrayReference(valuesAsArray), slot) = value; // avoid covariance check CheckInvariant(); Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); return true; @@ -485,7 +509,7 @@ private bool TryAddOulined(DotvvmPropertyId p, object? value) { Debug.Assert(this.values is object[]); Debug.Assert(keys is DotvvmPropertyId[]); - var slot = state == TableState.Array8 + var slot = this.state == TableState.Array8 ? Impl.FindSlotOrFree8(keys, p, out var exists) : Impl.FindSlotOrFree16(keys, p, out exists); if (slot >= 0) @@ -503,7 +527,7 @@ private bool TryAddOulined(DotvvmPropertyId p, object? value) keys[slot] = p; var valuesAsArray = this.valuesAsArray; Impl.Assert(valuesAsArray.Length > slot); - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArray), slot) = value; // avoid covariance check + Unsafe.Add(ref Impl.UnsafeArrayReference(valuesAsArray), slot) = value; // avoid covariance check CheckInvariant(); Debug.Assert(GetOrThrow(p) == value, $"{p} was not set to {value}."); return true; @@ -515,7 +539,7 @@ private bool TryAddOulined(DotvvmPropertyId p, object? value) goto TailRecursion; } } - if (values == null) + if (this.values == null) { SetEmptyToSingle(p, value); return true; @@ -927,7 +951,16 @@ public bool MoveNext() { var index = this.index + 1; var bitmap = this.bitmap >> index; +#if NET5_0_OR_GREATER this.index = index + BitOperations.TrailingZeroCount(bitmap); +#else + while ((bitmap & 1) == 0) + { + index++; + bitmap >>= 1; + } + this.index = index; +#endif return bitmap != 0; } else diff --git a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs index d19dfa34eb..7d02b0fc66 100644 --- a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs +++ b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs @@ -1,4 +1,3 @@ -#define Vectorize // for easier testing on without old Framework using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -8,15 +7,13 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using DotVVM.Framework.Binding; -using DotVVM.Framework.Hosting.ErrorPages; -using System.Security; using System.Diagnostics.CodeAnalysis; -#if NET6_0_OR_GREATER && Vectorize +#if Vectorize using System.Runtime.Intrinsics; #endif -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER using UnreachableException = System.Diagnostics.UnreachableException; #else using UnreachableException = System.Exception; @@ -24,7 +21,7 @@ namespace DotVVM.Framework.Controls { - internal static class PropertyImmutableHashtable + internal static class PropertyDictionaryImpl { /// 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; @@ -49,7 +46,7 @@ internal static class PropertyImmutableHashtable [MethodImpl(Inline)] private static bool ContainsKey8(ref DotvvmPropertyId keys, DotvvmPropertyId p) { -#if NET8_0_OR_GREATER && Vectorize +#if Vectorize if (Vector128.IsHardwareAccelerated) { Debug.Assert(Vector256.Count == AdhocTableSize); @@ -72,13 +69,13 @@ private static bool ContainsKey8(ref DotvvmPropertyId keys, DotvvmPropertyId p) public static bool ContainsKey8(DotvvmPropertyId[] keys, DotvvmPropertyId p) { Debug.Assert(keys.Length == AdhocTableSize); - return ContainsKey8(ref MemoryMarshal.GetArrayDataReference(keys), p); + return ContainsKey8(ref UnsafeArrayReference(keys), p); } [MethodImpl(Inline)] private static bool ContainsKey16(ref DotvvmPropertyId keys, DotvvmPropertyId p) { -#if NET8_0_OR_GREATER && Vectorize +#if Vectorize if (Vector128.IsHardwareAccelerated) { Debug.Assert(Vector256.Count == AdhocTableSize); @@ -94,13 +91,13 @@ private static bool ContainsKey16(ref DotvvmPropertyId keys, DotvvmPropertyId p) public static bool ContainsKey16(DotvvmPropertyId[] keys, DotvvmPropertyId p) { Debug.Assert(keys.Length == AdhocLargeTableSize); - return ContainsKey16(ref MemoryMarshal.GetArrayDataReference(keys), p); + return ContainsKey16(ref UnsafeArrayReference(keys), p); } [MethodImpl(Inline)] private static int FindSlot8(ref DotvvmPropertyId keys, DotvvmPropertyId p) { -#if NET8_0_OR_GREATER && Vectorize +#if Vectorize if (Vector128.IsHardwareAccelerated) { Debug.Assert(Vector256.Count == AdhocTableSize); @@ -132,13 +129,13 @@ private static int FindSlot8(ref DotvvmPropertyId keys, DotvvmPropertyId p) public static int FindSlot8(DotvvmPropertyId[] keys, DotvvmPropertyId p) { Debug.Assert(keys.Length == AdhocTableSize); - return FindSlot8(ref MemoryMarshal.GetArrayDataReference(keys), p); + return FindSlot8(ref UnsafeArrayReference(keys), p); } [MethodImpl(Inline)] private static int FindSlot16(ref DotvvmPropertyId keys, DotvvmPropertyId p) { -#if NET8_0_OR_GREATER && Vectorize +#if Vectorize if (Vector128.IsHardwareAccelerated) { var v1 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); @@ -169,7 +166,7 @@ private static int FindSlot16(ref DotvvmPropertyId keys, DotvvmPropertyId p) public static int FindSlot16(DotvvmPropertyId[] keys, DotvvmPropertyId p) { Debug.Assert(keys.Length == AdhocLargeTableSize); - return FindSlot16(ref MemoryMarshal.GetArrayDataReference(keys), p); + return FindSlot16(ref UnsafeArrayReference(keys), p); } public static int FindSlot(DotvvmPropertyId[] keys, DotvvmPropertyId p) @@ -191,7 +188,7 @@ public static int FindSlot(DotvvmPropertyId[] keys, DotvvmPropertyId p) [MethodImpl(Inline)] private static int FindSlotOrFree8(ref DotvvmPropertyId keys, DotvvmPropertyId p, out bool exists) { -#if NET8_0_OR_GREATER +#if Vectorize if (Vector128.IsHardwareAccelerated) { var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); @@ -235,7 +232,7 @@ private static int FindSlotOrFree8(ref DotvvmPropertyId keys, DotvvmPropertyId p [MethodImpl(Inline)] private static int FindSlotOrFree16(ref DotvvmPropertyId keys, DotvvmPropertyId p, out bool exists) { -#if NET8_0_OR_GREATER +#if Vectorize if (Vector128.IsHardwareAccelerated) { var v1 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); @@ -274,10 +271,10 @@ private static int FindSlotOrFree16(ref DotvvmPropertyId keys, DotvvmPropertyId [MethodImpl(Inline)] public static int FindSlotOrFree8(DotvvmPropertyId[] keys, DotvvmPropertyId p, out bool exists) => - FindSlotOrFree8(ref MemoryMarshal.GetArrayDataReference(keys), p, out exists); + FindSlotOrFree8(ref UnsafeArrayReference(keys), p, out exists); [MethodImpl(NoInlining)] public static int FindSlotOrFree16(DotvvmPropertyId[] keys, DotvvmPropertyId p, out bool exists) => - FindSlotOrFree16(ref MemoryMarshal.GetArrayDataReference(keys), p, out exists); + FindSlotOrFree16(ref UnsafeArrayReference(keys), p, out exists); public static ushort FindGroupBitmap(ref DotvvmPropertyId keys, int length, ushort groupId) { @@ -286,7 +283,7 @@ public static ushort FindGroupBitmap(ref DotvvmPropertyId keys, int length, usho ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; // groupId ^ 0x8000 ushort bitmap = 0; -#if NET8_0_OR_GREATER +#if Vectorize if (Vector128.IsHardwareAccelerated) { var v1 = Unsafe.ReadUnaligned>(in Unsafe.As(ref keys)); @@ -316,19 +313,19 @@ public static ushort FindGroupBitmap(ref DotvvmPropertyId keys, int length, usho public static ushort FindGroupBitmap(DotvvmPropertyId[] keys, ushort groupId) { - return FindGroupBitmap(ref MemoryMarshal.GetArrayDataReference(keys), keys.Length, groupId); + return FindGroupBitmap(ref UnsafeArrayReference(keys), keys.Length, groupId); } public static bool ContainsPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) { - Debug.Assert(keys.Length % Vector256.Count == 0); Debug.Assert(keys.Length >= AdhocTableSize); Debug.Assert(keys.Length % 8 == 0); ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; - ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); -#if NET8_0_OR_GREATER +#if Vectorize + Debug.Assert(keys.Length % Vector256.Count == 0); + ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); if (Vector128.IsHardwareAccelerated) { for (int i = 0; i < keys.Length; i += 8) @@ -354,13 +351,14 @@ public static bool ContainsPropertyGroup(DotvvmPropertyId[] keys, ushort groupId public static int Count(DotvvmPropertyId[] keys) { - Debug.Assert(keys.Length % Vector256.Count == 0); + Debug.Assert(keys.Length % 8 == 0); Debug.Assert(keys.Length >= AdhocTableSize); + +#if Vectorize ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); - Debug.Assert(keys.Length % 8 == 0); -#if NET8_0_OR_GREATER + Debug.Assert(keys.Length % Vector256.Count == 0); if (Vector128.IsHardwareAccelerated) { int zeroCount = 0; @@ -387,7 +385,7 @@ private static int CountPropertyGroup8(ref DotvvmPropertyId keys, ushort groupId ushort idPrefix = DotvvmPropertyId.CreatePropertyGroupId(groupId, 0).TypeId; ref var keysInts = ref Unsafe.As(ref keys); -#if NET8_0_OR_GREATER && Vectorize +#if Vectorize if (Vector128.IsHardwareAccelerated) { var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); @@ -410,14 +408,14 @@ private static int CountPropertyGroup8(ref DotvvmPropertyId keys, ushort groupId public static int CountPropertyGroup8(DotvvmPropertyId[] keys, ushort groupId) { Debug.Assert(keys.Length == 8); - ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + ref var keysRef = ref UnsafeArrayReference(keys); return CountPropertyGroup8(ref keysRef, groupId); } public static int CountPropertyGroup(DotvvmPropertyId[] keys, ushort groupId) { Debug.Assert(keys.Length % 8 == 0); - ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); + ref var keysRef = ref UnsafeArrayReference(keys); int count = 0; for (int i = 0; i < keys.Length; i += 8) @@ -435,6 +433,22 @@ private static byte BoolToInt(bool x) => Unsafe.As(ref x); #endif + [MethodImpl(Inline)] + public static ref DotvvmPropertyId UnsafeArrayReference(DotvvmPropertyId[] array) => +#if NET6_0_OR_GREATER + ref MemoryMarshal.GetArrayDataReference(array); +#else + ref array[0]; +#endif + + [MethodImpl(Inline)] + public static ref T UnsafeArrayReference(T[] array) => +#if NET6_0_OR_GREATER + ref MemoryMarshal.GetArrayDataReference(array); +#else + ref array[0]; +#endif + static ConcurrentDictionary tableCache = new(new EqCmp()); class EqCmp : IEqualityComparer @@ -468,11 +482,11 @@ public static void Assert([DoesNotReturnIf(false)] bool condition) [DoesNotReturn] [MethodImpl(NoInlining)] - public static void Fail() => throw new UnreachableException("Assertion failed in DotVVM property dictionary. This is a serious bug, please report it."); + public static void Fail() => Fail(); [DoesNotReturn] [MethodImpl(NoInlining)] - public static T Fail() => throw new UnreachableException("Assertion failed in DotVVM property dictionary. This is a serious bug, please report it."); + public static T Fail() => throw new UnreachableException("Assertion failed in DotVVM property dictionary. The collection state was probably corrupted by concurrent access, or by a serious DotVVM bug."); private static bool IsOrderedWithoutDuplicatesAndZero(DotvvmPropertyId[] keys) { diff --git a/src/Framework/Framework/Controls/RouteLink.cs b/src/Framework/Framework/Controls/RouteLink.cs index 7beabaf2ac..f4044a96c6 100644 --- a/src/Framework/Framework/Controls/RouteLink.cs +++ b/src/Framework/Framework/Controls/RouteLink.cs @@ -139,7 +139,6 @@ 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/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 031e7dcacc..9d2deefc36 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -70,7 +70,7 @@ - + @@ -98,6 +98,9 @@ $(DefineConstants);DotNetCore + + $(DefineConstants);Vectorize + $(DefineConstants);CSharp8Polyfill;NoSpan;INTERNAL_NULLABLE_ATTRIBUTES diff --git a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj index 29d8bdccca..cad20cfe37 100644 --- a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj +++ b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj @@ -38,6 +38,6 @@ - + diff --git a/src/Tests/Runtime/CapabilityPropertyTests.cs b/src/Tests/Runtime/CapabilityPropertyTests.cs index 2fc85ab269..2e884a9c87 100644 --- a/src/Tests/Runtime/CapabilityPropertyTests.cs +++ b/src/Tests/Runtime/CapabilityPropertyTests.cs @@ -8,6 +8,7 @@ using DotVVM.Framework.Controls; using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Testing; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -315,6 +316,43 @@ public void BitMoreComplexCapability_NullableValue(Type controlType) Assert.AreEqual(2, control1.GetCapability().Nullable); } + [DataTestMethod] + [DataRow(typeof(TestControl1), "Something", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl1), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl1), "Visible", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl1), "ID", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl2), "Something", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl2), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl2), "AnotherHtml:Visible", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl3), "Something", MappingMode.Attribute, true, false)] + [DataRow(typeof(TestControl3), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl4), "Something", MappingMode.Attribute, true, false)] + [DataRow(typeof(TestControl4), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl5), "SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl5), "AnotherTest:SomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl5), "ItemSomethingElse", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl6), "BindingOnly", MappingMode.Attribute, true, false)] + [DataRow(typeof(TestControl6), "ValueOrBinding", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl6), "ValueOrBindingNullable", MappingMode.Attribute, true, true)] + [DataRow(typeof(TestControl6), "Nullable", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl6), "NotNullable", MappingMode.Attribute, false, true)] + [DataRow(typeof(TestControl6), "Template", MappingMode.InnerElement, false, true)] + [DataRow(typeof(TestControl6), "NestedControl", MappingMode.InnerElement, false, true)] + [DataRow(typeof(TestControl6), "NestedControls", MappingMode.InnerElement, false, true)] + public void PropertyImplicitMarkupOptions(Type control, string property, MappingMode expectedMode, bool expectedAllowsBinding, bool expectedAllowsValue) + { + + var c = (DotvvmBindableObject)Activator.CreateInstance(control); + var allProps = DotvvmProperty.ResolveProperties(control).Select(p => p.Name); + var prop = DotvvmProperty.ResolveProperty(control, property); + Assert.IsNotNull(prop, $"{control.Name}.{property} is not defined. These properties exist: {string.Join(", ", allProps)}"); + + Assert.AreEqual(expectedMode, prop.MarkupOptions.MappingMode); + Assert.AreEqual(expectedAllowsBinding, prop.MarkupOptions.AllowBinding); + Assert.AreEqual(expectedAllowsValue, prop.MarkupOptions.AllowHardCodedValue); + } + + public class TestControl1: HtmlGenericControl, @@ -445,6 +483,7 @@ public sealed record TestCapability [DotvvmControlCapability] public sealed record TestNestedCapability { + [MarkupOptions(AllowBinding = true, AllowHardCodedValue = false)] public string Something { get; init; } = "abc"; public TestCapability Test { get; init; } public HtmlCapability Html { get; init; } @@ -470,6 +509,9 @@ public sealed record BitMoreComplexCapability public ValueOrBinding? ValueOrBindingNullable { get; init; } public int? Nullable { get; init; } public int NotNullable { get; init; } = 30; + public ITemplate Template { get; init; } + public HtmlGenericControl NestedControl { get; init; } + public List NestedControls { get; init; } } } diff --git a/src/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index 00fb46565e..086c892a62 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -10,12 +10,16 @@ using DotVVM.Framework.Controls; using DotVVM.Framework.Testing; using DotVVM.Framework.Utils; +using Microsoft.AspNetCore.Components.Web.Virtualization; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Net.Sockets; using System.Reflection; +using System.Threading; using System.Threading.Tasks; namespace DotVVM.Framework.Tests.Runtime @@ -364,7 +368,7 @@ 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 setter = PropertyDictionaryImpl.CreateBulkSetter(properties, Enumerable.Range(0, 1000).Select(i => (object?)i).ToArray()); var control1 = new HtmlGenericControl("div"); setter(control1); @@ -460,5 +464,85 @@ public void DotvvmProperty_VirtualDictionary_Append(int testClone) XAssert.Equal(Enumerable.Range(0, i+1).Cast(), control.Attributes.Values); } } + + [TestMethod, Ignore] + public void DotvvmProperty_ParallelAccess_DoesntCrashProcess() + { + var properties = new DotvvmProperty[] { + DotvvmBindableObject.DataContextProperty, + DotvvmControl.IncludeInPageProperty, + HtmlGenericControl.VisibleProperty, + TextBox.EnabledProperty, + HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-1"), + HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-2"), + HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-3"), + HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("data-4"), + Button.EnabledProperty, + FormControls.EnabledProperty + }; + var control = new PlaceHolder(); + + var exceptions = new ConcurrentBag(); + + ThreadPool.SetMinThreads(100, 100); + Parallel.For(0, 10_000_000_000, new ParallelOptions { MaxDegreeOfParallelism = 100 }, i => { + try + { + var value = control.GetValue(properties[(i / 2) % properties.Length]); + control.properties.TryGet(properties[i % properties.Length], out value); + if (i % 2 == 0) + { + control.SetValue(properties[i % properties.Length], BoxingUtils.Box(i % 2 == 1)); + } + else + { + control.properties.TryAdd(properties[i % properties.Length], BoxingUtils.Box(i % 2 == 1)); + } + + if (i % 16 == 0) + { + switch (Random.Shared.Next(0, 4)) + { + case 0: + control = new PlaceHolder(); + break; + case 1: + control = (PlaceHolder)control.CloneControl(); + break; + case 2: + foreach (var prop in control.Properties.Keys) + control.Properties.Remove(prop); + break; + case 3: + foreach (var prop in control.properties.PropertyGroup(HtmlGenericControl.AttributesGroupDescriptor.Id)) + control.properties.Remove(prop.Key); + break; + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + control = new PlaceHolder(); + } + // if (control.properties.Count() >= 16) + // throw new Exception("Too many properties"); + }); + + var exceptionGroups = exceptions + .GroupBy(e => e.GetType().Name + ": " + e.Message) + // .GroupBy(e => e.ToString()) + .Select(g => (g.Key, g.Count())) + .OrderByDescending(g => g.Item2) + .ToList(); + foreach (var (key, count) in exceptionGroups) + { + Console.WriteLine($"{key}: {count}"); + } + if (exceptions.Count > 0) + { + Assert.Fail($"There were {exceptions.Count} exceptions thrown during the test. See the output for details."); + } + } } } diff --git a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt index 1178fad778..8e328ec381 100644 --- a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt +++ b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt @@ -1,2 +1,2 @@ InvalidDataContextTypeException occurred: Could not find DataContext space of '{value: False}'. The DataContextType property of the binding does not correspond to DataContextType of the HtmlGenericControl nor any of its ancestors. Control's context is (type=string, par=[int]), binding's context is (type=string). Real data context types: . - at object DotVVM.Framework.Controls.DotvvmBindableObject.EvalPropertyValue(DotvvmProperty property, object value) + at object DotVVM.Framework.Controls.DotvvmBindableObject.EvalBinding(IBinding binding, bool isDataContext) From ddf5945faeee73ebd409930ca01407b9287d1a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 15 Aug 2025 17:04:50 +0200 Subject: [PATCH 08/11] Add script to generate pre-assigned property IDs * Optimize relevant property accessed using the new compile-time constants * Optimize PropertyGroup.Clear * Cover DotvvmPropertyDictionary with more tests (and fix some bugs) --- .../Framework/Binding/BindingHelper.cs | 17 +- ...DotvvmCapabilityProperty.CodeGeneration.cs | 11 +- .../Framework/Binding/DotvvmProperty.cs | 3 +- .../Framework/Binding/DotvvmPropertyId.cs | 2 + ...DotvvmPropertyIdAssignment.GroupMembers.cs | 421 ++++++++- ...vmPropertyIdAssignment.PropertyGroupIds.cs | 31 + .../DotvvmPropertyIdAssignment.PropertyIds.cs | 652 ++++++++++++-- .../DotvvmPropertyIdAssignment.TypeIds.cs | 171 +++- .../Binding/DotvvmPropertyIdAssignment.cs | 33 +- .../Binding/DotvvmPropertyWithFallback.cs | 2 +- .../Framework/Binding/ValueOrBinding.cs | 9 +- .../Binding/ValueOrBindingExtensions.cs | 3 - .../Binding/VirtualPropertyGroupDictionary.cs | 172 ++-- .../Compilation/Binding/TypeConversions.cs | 4 + .../Framework/Compilation/BindingCompiler.cs | 4 +- .../ControlTree/BindingExtensionParameter.cs | 18 +- .../Framework/Controls/DataItemContainer.cs | 8 +- .../Controls/DotvvmBindableObject.cs | 28 +- .../Controls/DotvvmBindableObjectHelper.cs | 10 +- .../Framework/Controls/DotvvmControl.cs | 36 +- .../Controls/DotvvmControlCollection.cs | 14 +- .../Controls/DotvvmControlProperties.cs | 96 +- .../Framework/Controls/HtmlGenericControl.cs | 14 +- src/Framework/Framework/Controls/Internal.cs | 6 +- src/Framework/Framework/Controls/Label.cs | 2 +- src/Framework/Framework/Controls/Literal.cs | 13 +- .../Controls/PropertyImmutableHashtable.cs | 31 +- .../Framework/DotVVM.Framework.csproj | 6 + .../Runtime/Commands/EventValidator.cs | 19 +- .../Runtime/DefaultOutputRenderer.cs | 2 +- .../scripts/generate-property-ids.mjs | 826 ++++++++++++++++++ src/Tests/Runtime/DotvvmPropertyTests.cs | 130 +++ 32 files changed, 2457 insertions(+), 337 deletions(-) create mode 100644 src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyGroupIds.cs create mode 100644 src/Framework/Framework/scripts/generate-property-ids.mjs diff --git a/src/Framework/Framework/Binding/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index fd2fb2264d..2cdbbca809 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -64,8 +64,8 @@ public static string FormatKnockoutScript(this ParametrizedCode code, DotvvmBind /// Gets Internal.PathFragmentProperty or DataContext.KnockoutExpression. Returns null if none of these is set. /// public static string? GetDataContextPathFragment(this DotvvmBindableObject currentControl) => - currentControl.properties.TryGet(Internal.PathFragmentProperty, out var pathFragment) && pathFragment is string pathFragmentStr ? pathFragmentStr : - currentControl.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var dataContext) && dataContext is IValueBinding binding ? + currentControl.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_PathFragment, out var pathFragment) && pathFragment is string pathFragmentStr ? pathFragmentStr : + currentControl.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, out var dataContext) && dataContext is IValueBinding binding ? binding.GetProperty() .Code.FormatKnockoutScript(currentControl, binding) : null; @@ -88,16 +88,16 @@ internal static (int stepsUp, DotvvmBindableObject target) FindDataContextTarget if (bindingContext == null || controlContext == null || controlContext.Equals(bindingContext)) return (0, control); var changes = 0; - foreach (var a in control.GetAllAncestors(includingThis: true)) + for (var ancestor = control; ancestor is {}; ancestor = ancestor.Parent) { - var ancestorContext = a.GetDataContextType(inherit: false); + var ancestorContext = ancestor.GetDataContextType(inherit: false); if (bindingContext.Equals(ancestorContext)) - return (changes, a); + return (changes, ancestor); // count only client-side data contexts (DataContext={resource:} is skipped in JS) - if (a.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var ancestorRuntimeContext)) + if (ancestor.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, out var ancestorRuntimeContext)) { - if (a.properties.TryGet(Internal.IsServerOnlyDataContextProperty, out var isServerOnly) && isServerOnly != null) + if (ancestor.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsServerOnlyDataContext) is {} isServerOnly) { if (isServerOnly is false) changes++; @@ -173,9 +173,8 @@ public static T ExecDelegate(this BindingDelegate func, DotvvmBindableObje // this has O(h^2) complexity because GetValue calls another GetDataContexts, // but this function is used rarely - for exceptions, manually created bindings, ... // Normal bindings have specialized code generated in BindingCompiler - if (c.IsPropertySet(DotvvmBindableObject.DataContextProperty, inherit: false)) + if (c.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext)) { - Debug.Assert(c.properties.Contains(DotvvmBindableObject.DataContextProperty), "Control claims that DataContextProperty is set, but it's not present in the properties dictionary."); yield return c.GetValue(DotvvmBindableObject.DataContextProperty); count--; } diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs index 6c3d6c737b..a1b7de7916 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs @@ -76,27 +76,28 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyG var valueParameter = Expression.Parameter(type, "value"); var ctor = typeof(VirtualPropertyGroupDictionary<>) .MakeGenericType(propType) - .GetConstructor(new [] { typeof(DotvvmBindableObject), typeof(DotvvmPropertyGroup) })!; + .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, [ typeof(DotvvmBindableObject), typeof(ushort), typeof(bool) ])!; var createMethod = typeof(VirtualPropertyGroupDictionary<>) .MakeGenericType(propType) .GetMethod( typeof(ValueOrBinding).IsAssignableFrom(elementType) ? nameof(VirtualPropertyGroupDictionary.CreatePropertyDictionary) : nameof(VirtualPropertyGroupDictionary.CreateValueDictionary), - BindingFlags.Public | BindingFlags.Static + BindingFlags.NonPublic | BindingFlags.Static, + [ typeof(DotvvmBindableObject), typeof(ushort) ] )!; var enumerableType = typeof(IEnumerable<>).MakeGenericType(typeof(KeyValuePair<,>).MakeGenericType(typeof(string), elementType)); var copyFromMethod = typeof(VirtualPropertyGroupDictionary<>) .MakeGenericType(propType) - .GetMethod("CopyFrom", new [] { enumerableType, typeof(bool) })!; + .GetMethod("CopyFrom", [ enumerableType, typeof(bool) ])!; return ( Lambda( - Convert(Call(createMethod, currentControlParameter, Constant(pgroup)), type), + Convert(Call(createMethod, currentControlParameter, Constant(pgroup.Id)), type), currentControlParameter ), Lambda( Call( - New(ctor, currentControlParameter, Constant(pgroup)), + New(ctor, currentControlParameter, Constant(pgroup.Id), Constant(pgroup.IsBindingProperty)), copyFromMethod, Convert(valueParameter, enumerableType), Constant(true) // clear diff --git a/src/Framework/Framework/Binding/DotvvmProperty.cs b/src/Framework/Framework/Binding/DotvvmProperty.cs index d4ced3b1d6..d394204fe0 100644 --- a/src/Framework/Framework/Binding/DotvvmProperty.cs +++ b/src/Framework/Framework/Binding/DotvvmProperty.cs @@ -209,9 +209,10 @@ public bool IsOwnedByCapability(DotvvmCapabilityProperty capability) => private object? GetInheritedValue(DotvvmBindableObject control) { + var id = this.Id; for (var p = control.Parent; p is not null; p = p.Parent) { - if (p.properties.TryGet(Id, out var v)) + if (p.properties.TryGet(id, out var v)) return v; } return DefaultValue; diff --git a/src/Framework/Framework/Binding/DotvvmPropertyId.cs b/src/Framework/Framework/Binding/DotvvmPropertyId.cs index 4fe42c5bf7..f7c765899a 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyId.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyId.cs @@ -129,6 +129,8 @@ public bool IsZero public override string ToString() { + if (IsZero) + return "[0000_0000]"; if (IsPropertyGroup) { var pg = PropertyGroupInstance; diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs index 60ba6b8a1a..1c4f4ac173 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.GroupMembers.cs @@ -1,38 +1,401 @@ +// Generated by scripts/generate-property-ids.mjs using System; using System.Collections.Immutable; -namespace DotVVM.Framework.Binding -{ +namespace DotVVM.Framework.Binding; - static partial class DotvvmPropertyIdAssignment +static partial class DotvvmPropertyIdAssignment +{ + public static class GroupMembers { - 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 data_bind = 5; - - public static readonly ImmutableArray<(string Name, ushort ID)> List = ImmutableArray.Create( - ("id", id), - ("class", @class), - ("style", style), - ("name", name), - ("data-bind", data_bind) - ); + public const ushort accept = 1; + public const ushort accesskey = 2; + public const ushort action = 3; + public const ushort align = 4; + public const ushort allow = 5; + public const ushort alt = 6; + public const ushort aria_checked = 7; + public const ushort aria_controls = 8; + public const ushort aria_describedby = 9; + public const ushort aria_expanded = 10; + public const ushort aria_hidden = 11; + public const ushort aria_label = 12; + public const ushort aria_selected = 13; + public const ushort @as = 14; + public const ushort async = 15; + public const ushort autocomplete = 16; + public const ushort autofocus = 17; + public const ushort border = 18; + public const ushort charset = 19; + public const ushort @checked = 20; + public const ushort @class = 21; + public const ushort cols = 22; + public const ushort colspan = 23; + public const ushort content = 24; + public const ushort contenteditable = 25; + public const ushort crossorigin = 26; + public const ushort data_bind = 27; + public const ushort data_dismiss = 28; + public const ushort data_dotvvm_id = 29; + public const ushort data_placement = 30; + public const ushort data_target = 31; + public const ushort data_toggle = 32; + public const ushort data_ui = 33; + public const ushort data_uitest_name = 34; + public const ushort dir = 35; + public const ushort disabled = 36; + public const ushort download = 37; + public const ushort draggable = 38; + public const ushort enctype = 39; + public const ushort @for = 40; + public const ushort form = 41; + public const ushort formaction = 42; + public const ushort formmethod = 43; + public const ushort formnovalidate = 44; + public const ushort formtarget = 45; + public const ushort height = 46; + public const ushort hidden = 47; + public const ushort href = 48; + public const ushort hreflang = 49; + public const ushort http_equiv = 50; + public const ushort id = 51; + public const ushort integrity = 52; + public const ushort itemprop = 53; + public const ushort lang = 54; + public const ushort list = 55; + public const ushort loading = 56; + public const ushort max = 57; + public const ushort maxlength = 58; + public const ushort media = 59; + public const ushort method = 60; + public const ushort min = 61; + public const ushort minlength = 62; + public const ushort multiple = 63; + public const ushort name = 64; + public const ushort novalidate = 65; + public const ushort pattern = 66; + public const ushort ping = 67; + public const ushort placeholder = 68; + public const ushort preload = 69; + public const ushort @readonly = 70; + public const ushort referrerpolicy = 71; + public const ushort rel = 72; + public const ushort required = 73; + public const ushort role = 74; + public const ushort rows = 75; + public const ushort sandbox = 76; + public const ushort scope = 77; + public const ushort selected = 78; + public const ushort size = 79; + public const ushort slot = 80; + public const ushort span = 81; + public const ushort spellcheck = 82; + public const ushort src = 83; + public const ushort step = 84; + public const ushort style = 85; + public const ushort tabindex = 86; + public const ushort target = 87; + public const ushort title = 88; + public const ushort translate = 89; + public const ushort type = 90; + public const ushort value = 91; + public const ushort width = 92; + public const ushort wrap = 93; + public const ushort background_color = 94; + public const ushort bottom = 95; + public const ushort color = 96; + public const ushort display = 97; + public const ushort font_size = 98; + public const ushort left = 99; + public const ushort line_height = 100; + public const ushort margin_bottom = 101; + public const ushort margin_right = 102; + public const ushort margin_top = 103; + public const ushort margin = 104; + public const ushort max_height = 105; + public const ushort max_width = 106; + public const ushort min_height = 107; + public const ushort min_width = 108; + public const ushort opacity = 109; + public const ushort padding_bottom = 110; + public const ushort padding_left = 111; + public const ushort padding_right = 112; + public const ushort padding_top = 113; + public const ushort padding = 114; + public const ushort position = 115; + public const ushort right = 116; + public const ushort top = 117; + public const ushort visibility = 118; + public const ushort z_index = 119; + public const ushort Id = 120; + public const ushort Name = 121; + public const ushort GroupId = 122; + public const ushort FileName = 123; + public const ushort UserId = 124; + public const ushort Slug = 125; + public const ushort slug = 126; + public const ushort Lang = 127; - public static ushort TryGetId(ReadOnlySpan attr) => - attr switch { - "id" => id, - "class" => @class, - "style" => style, - "name" => name, - "data-bind" => data_bind, - _ => 0, - }; + public static readonly ImmutableArray<(string Name, ushort ID)> List = ImmutableArray.Create( + ("accept", accept), + ("accesskey", accesskey), + ("action", action), + ("align", align), + ("allow", allow), + ("alt", alt), + ("aria-checked", aria_checked), + ("aria-controls", aria_controls), + ("aria-describedby", aria_describedby), + ("aria-expanded", aria_expanded), + ("aria-hidden", aria_hidden), + ("aria-label", aria_label), + ("aria-selected", aria_selected), + ("as", @as), + ("async", async), + ("autocomplete", autocomplete), + ("autofocus", autofocus), + ("border", border), + ("charset", charset), + ("checked", @checked), + ("class", @class), + ("cols", cols), + ("colspan", colspan), + ("content", content), + ("contenteditable", contenteditable), + ("crossorigin", crossorigin), + ("data-bind", data_bind), + ("data-dismiss", data_dismiss), + ("data-dotvvm-id", data_dotvvm_id), + ("data-placement", data_placement), + ("data-target", data_target), + ("data-toggle", data_toggle), + ("data-ui", data_ui), + ("data-uitest-name", data_uitest_name), + ("dir", dir), + ("disabled", disabled), + ("download", download), + ("draggable", draggable), + ("enctype", enctype), + ("for", @for), + ("form", form), + ("formaction", formaction), + ("formmethod", formmethod), + ("formnovalidate", formnovalidate), + ("formtarget", formtarget), + ("height", height), + ("hidden", hidden), + ("href", href), + ("hreflang", hreflang), + ("http-equiv", http_equiv), + ("id", id), + ("integrity", integrity), + ("itemprop", itemprop), + ("lang", lang), + ("list", list), + ("loading", loading), + ("max", max), + ("maxlength", maxlength), + ("media", media), + ("method", method), + ("min", min), + ("minlength", minlength), + ("multiple", multiple), + ("name", name), + ("novalidate", novalidate), + ("pattern", pattern), + ("ping", ping), + ("placeholder", placeholder), + ("preload", preload), + ("readonly", @readonly), + ("referrerpolicy", referrerpolicy), + ("rel", rel), + ("required", required), + ("role", role), + ("rows", rows), + ("sandbox", sandbox), + ("scope", scope), + ("selected", selected), + ("size", size), + ("slot", slot), + ("span", span), + ("spellcheck", spellcheck), + ("src", src), + ("step", step), + ("style", style), + ("tabindex", tabindex), + ("target", target), + ("title", title), + ("translate", translate), + ("type", type), + ("value", value), + ("width", width), + ("wrap", wrap), + ("background-color", background_color), + ("bottom", bottom), + ("color", color), + ("display", display), + ("font-size", font_size), + ("left", left), + ("line-height", line_height), + ("margin-bottom", margin_bottom), + ("margin-right", margin_right), + ("margin-top", margin_top), + ("margin", margin), + ("max-height", max_height), + ("max-width", max_width), + ("min-height", min_height), + ("min-width", min_width), + ("opacity", opacity), + ("padding-bottom", padding_bottom), + ("padding-left", padding_left), + ("padding-right", padding_right), + ("padding-top", padding_top), + ("padding", padding), + ("position", position), + ("right", right), + ("top", top), + ("visibility", visibility), + ("z-index", z_index), + ("Id", Id), + ("Name", Name), + ("GroupId", GroupId), + ("FileName", FileName), + ("UserId", UserId), + ("Slug", Slug), + ("slug", slug), + ("Lang", Lang) + ); - // TODO - } + public static ushort TryGetId(ReadOnlySpan attr) => + attr switch { + "accept" => accept, + "accesskey" => accesskey, + "action" => action, + "align" => align, + "allow" => allow, + "alt" => alt, + "aria-checked" => aria_checked, + "aria-controls" => aria_controls, + "aria-describedby" => aria_describedby, + "aria-expanded" => aria_expanded, + "aria-hidden" => aria_hidden, + "aria-label" => aria_label, + "aria-selected" => aria_selected, + "as" => @as, + "async" => async, + "autocomplete" => autocomplete, + "autofocus" => autofocus, + "border" => border, + "charset" => charset, + "checked" => @checked, + "class" => @class, + "cols" => cols, + "colspan" => colspan, + "content" => content, + "contenteditable" => contenteditable, + "crossorigin" => crossorigin, + "data-bind" => data_bind, + "data-dismiss" => data_dismiss, + "data-dotvvm-id" => data_dotvvm_id, + "data-placement" => data_placement, + "data-target" => data_target, + "data-toggle" => data_toggle, + "data-ui" => data_ui, + "data-uitest-name" => data_uitest_name, + "dir" => dir, + "disabled" => disabled, + "download" => download, + "draggable" => draggable, + "enctype" => enctype, + "for" => @for, + "form" => form, + "formaction" => formaction, + "formmethod" => formmethod, + "formnovalidate" => formnovalidate, + "formtarget" => formtarget, + "height" => height, + "hidden" => hidden, + "href" => href, + "hreflang" => hreflang, + "http-equiv" => http_equiv, + "id" => id, + "integrity" => integrity, + "itemprop" => itemprop, + "lang" => lang, + "list" => list, + "loading" => loading, + "max" => max, + "maxlength" => maxlength, + "media" => media, + "method" => method, + "min" => min, + "minlength" => minlength, + "multiple" => multiple, + "name" => name, + "novalidate" => novalidate, + "pattern" => pattern, + "ping" => ping, + "placeholder" => placeholder, + "preload" => preload, + "readonly" => @readonly, + "referrerpolicy" => referrerpolicy, + "rel" => rel, + "required" => required, + "role" => role, + "rows" => rows, + "sandbox" => sandbox, + "scope" => scope, + "selected" => selected, + "size" => size, + "slot" => slot, + "span" => span, + "spellcheck" => spellcheck, + "src" => src, + "step" => step, + "style" => style, + "tabindex" => tabindex, + "target" => target, + "title" => title, + "translate" => translate, + "type" => type, + "value" => value, + "width" => width, + "wrap" => wrap, + "background-color" => background_color, + "bottom" => bottom, + "color" => color, + "display" => display, + "font-size" => font_size, + "left" => left, + "line-height" => line_height, + "margin-bottom" => margin_bottom, + "margin-right" => margin_right, + "margin-top" => margin_top, + "margin" => margin, + "max-height" => max_height, + "max-width" => max_width, + "min-height" => min_height, + "min-width" => min_width, + "opacity" => opacity, + "padding-bottom" => padding_bottom, + "padding-left" => padding_left, + "padding-right" => padding_right, + "padding-top" => padding_top, + "padding" => padding, + "position" => position, + "right" => right, + "top" => top, + "visibility" => visibility, + "z-index" => z_index, + "Id" => Id, + "Name" => Name, + "GroupId" => GroupId, + "FileName" => FileName, + "UserId" => UserId, + "Slug" => Slug, + "slug" => slug, + "Lang" => Lang, + _ => 0, + }; } } diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyGroupIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyGroupIds.cs new file mode 100644 index 0000000000..d2f1f580e6 --- /dev/null +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyGroupIds.cs @@ -0,0 +1,31 @@ +// Generated by scripts/generate-property-ids.mjs +using DotVVM.Framework.Controls; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class PropertyGroupIds + { + /// + public const ushort HtmlGenericControl_Attributes = 1; + + /// + public const ushort HtmlGenericControl_CssClasses = 2; + + /// + public const ushort HtmlGenericControl_CssStyles = 3; + + /// + public const ushort RouteLink_Params = 4; + + /// + public const ushort RouteLink_QueryParameters = 5; + + /// + public const ushort JsComponent_Props = 6; + + /// + public const ushort JsComponent_Templates = 7; + } +} diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs index 0059499d72..2dde63c6f0 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.PropertyIds.cs @@ -1,76 +1,590 @@ +// Generated by scripts/generate-property-ids.mjs using DotVVM.Framework.Controls; using DotVVM.Framework.Controls.Infrastructure; -namespace DotVVM.Framework.Binding -{ +namespace DotVVM.Framework.Binding; - static partial class DotvvmPropertyIdAssignment +static partial class DotvvmPropertyIdAssignment +{ + public static class PropertyIds { - public static class PropertyIds - { - // fields are looked-up automatically for type registered in TypeIds - - /// - public const uint DotvvmBindableObject_DataContext = TypeIds.DotvvmBindableObject << 16 | 1; - - /// - public const uint DotvvmControl_ID = TypeIds.DotvvmControl << 16 | 2; - /// - public const uint DotvvmControl_ClientID = TypeIds.DotvvmControl << 16 | 4; - /// - public const uint DotvvmControl_IncludeInPage = TypeIds.DotvvmControl << 16 | 6; - - /// - public const uint DotvvmControl_ClientIDMode = TypeIds.DotvvmControl << 16 | 1; - - /// - public const uint HtmlGenericControl_Visible = TypeIds.HtmlGenericControl << 16 | 2; - /// - public const uint HtmlGenericControl_InnerText = TypeIds.HtmlGenericControl << 16 | 4; - /// - public const uint HtmlGenericControl_HtmlCapability = TypeIds.HtmlGenericControl << 16 | 1; - - /// - public const uint Literal_Text = TypeIds.Literal << 16 | 2; - /// - public const uint Literal_FormatString = TypeIds.Literal << 16 | 4; - /// - public const uint Literal_RenderSpanElement = TypeIds.Literal << 16 | 6; - /// - public const uint ButtonBase_Click = TypeIds.ButtonBase << 16 | 2; - /// - public const uint ButtonBase_ClickArguments = TypeIds.ButtonBase << 16 | 4; - /// - public const uint ButtonBase_Text = TypeIds.ButtonBase << 16 | 8; - /// - public const uint ButtonBase_Enabled = TypeIds.ButtonBase << 16 | 1; - /// - public const uint ButtonBase_TextOrContentCapability = TypeIds.ButtonBase << 16 | 3; - - /// - public const uint Button_ButtonTagName = TypeIds.Button << 16 | 2; - /// - public const uint Button_IsSubmitButton = TypeIds.Button << 16 | 4; - /// - public const uint TextBox_Text = TypeIds.TextBox << 16 | 2; - /// - public const uint TextBox_Changed = TypeIds.TextBox << 16 | 4; - /// - public const uint TextBox_Type = TypeIds.TextBox << 16 | 6; - /// - public const uint TextBox_TextInput = TypeIds.TextBox << 16 | 8; - /// - public const uint TextBox_FormatString = TypeIds.TextBox << 16 | 10; - /// - public const uint TextBox_SelectAllOnFocus = TypeIds.TextBox << 16 | 12; - /// - public const uint TextBox_Enabled = TypeIds.TextBox << 16 | 1; - /// - public const uint TextBox_UpdateTextOnInput = TypeIds.TextBox << 16 | 3; - /// - public const uint RenderSettings_Mode = TypeIds.RenderSettings << 16 | 1; - - // TODO - } + /// + public const uint DotvvmBindableObject_DataContext = TypeIds.DotvvmBindableObject << 16 | 1; + + /// + public const uint DotvvmControl_ID = TypeIds.DotvvmControl << 16 | 2; + + /// + public const uint DotvvmControl_ClientID = TypeIds.DotvvmControl << 16 | 4; + + /// + public const uint DotvvmControl_IncludeInPage = TypeIds.DotvvmControl << 16 | 6; + + /// + public const uint DotvvmControl_ClientIDMode = TypeIds.DotvvmControl << 16 | 1; + + /// + public const uint HtmlGenericControl_Visible = TypeIds.HtmlGenericControl << 16 | 2; + + /// + public const uint HtmlGenericControl_InnerText = TypeIds.HtmlGenericControl << 16 | 4; + + /// + public const uint HtmlGenericControl_HtmlCapability = TypeIds.HtmlGenericControl << 16 | 1; + + /// + public const uint Literal_Text = TypeIds.Literal << 16 | 2; + + /// + public const uint Literal_FormatString = TypeIds.Literal << 16 | 4; + + /// + public const uint Literal_RenderSpanElement = TypeIds.Literal << 16 | 6; + + /// + public const uint ButtonBase_Click = TypeIds.ButtonBase << 16 | 2; + + /// + public const uint ButtonBase_ClickArguments = TypeIds.ButtonBase << 16 | 4; + + /// + public const uint ButtonBase_Text = TypeIds.ButtonBase << 16 | 6; + + /// + public const uint ButtonBase_Enabled = TypeIds.ButtonBase << 16 | 1; + + /// + public const uint ButtonBase_TextOrContentCapability = TypeIds.ButtonBase << 16 | 3; + + /// + public const uint Button_ButtonTagName = TypeIds.Button << 16 | 2; + + /// + public const uint Button_IsSubmitButton = TypeIds.Button << 16 | 4; + + /// + public const uint TextBox_Text = TypeIds.TextBox << 16 | 2; + + /// + public const uint TextBox_Changed = TypeIds.TextBox << 16 | 4; + + /// + public const uint TextBox_Type = TypeIds.TextBox << 16 | 6; + + /// + public const uint TextBox_TextInput = TypeIds.TextBox << 16 | 8; + + /// + public const uint TextBox_FormatString = TypeIds.TextBox << 16 | 10; + + /// + public const uint TextBox_SelectAllOnFocus = TypeIds.TextBox << 16 | 12; + + /// + public const uint TextBox_Enabled = TypeIds.TextBox << 16 | 1; + + /// + public const uint TextBox_UpdateTextOnInput = TypeIds.TextBox << 16 | 3; + + /// + public const uint RouteLink_RouteName = TypeIds.RouteLink << 16 | 2; + + /// + public const uint RouteLink_Enabled = TypeIds.RouteLink << 16 | 4; + + /// + public const uint RouteLink_Text = TypeIds.RouteLink << 16 | 6; + + /// + public const uint RouteLink_UrlSuffix = TypeIds.RouteLink << 16 | 8; + + /// + public const uint RouteLink_Culture = TypeIds.RouteLink << 16 | 10; + + /// + public const uint CheckableControlBase_Text = TypeIds.CheckableControlBase << 16 | 2; + + /// + public const uint CheckableControlBase_CheckedValue = TypeIds.CheckableControlBase << 16 | 4; + + /// + public const uint CheckableControlBase_Changed = TypeIds.CheckableControlBase << 16 | 6; + + /// + public const uint CheckableControlBase_LabelCssClass = TypeIds.CheckableControlBase << 16 | 8; + + /// + public const uint CheckableControlBase_InputCssClass = TypeIds.CheckableControlBase << 16 | 10; + + /// + public const uint CheckableControlBase_ItemKeyBinding = TypeIds.CheckableControlBase << 16 | 12; + + /// + public const uint CheckBox_Checked = TypeIds.CheckBox << 16 | 2; + + /// + public const uint CheckBox_CheckedItems = TypeIds.CheckBox << 16 | 4; + + /// + public const uint CheckBox_DisableIndeterminate = TypeIds.CheckBox << 16 | 6; + + /// + public const uint RadioButton_Checked = TypeIds.RadioButton << 16 | 2; + + /// + public const uint RadioButton_CheckedItem = TypeIds.RadioButton << 16 | 4; + + /// + public const uint RadioButton_GroupName = TypeIds.RadioButton << 16 | 6; + + /// + public const uint Validator_HideWhenValid = TypeIds.Validator << 16 | 1; + + /// + public const uint Validator_InvalidCssClass = TypeIds.Validator << 16 | 3; + + /// + public const uint Validator_SetToolTipText = TypeIds.Validator << 16 | 5; + + /// + public const uint Validator_ShowErrorMessageText = TypeIds.Validator << 16 | 7; + + /// + public const uint Validation_Enabled = TypeIds.Validation << 16 | 1; + + /// + public const uint Validation_Target = TypeIds.Validation << 16 | 3; + + /// + public const uint ValidationSummary_IncludeErrorsFromChildren = TypeIds.ValidationSummary << 16 | 2; + + /// + public const uint ValidationSummary_HideWhenValid = TypeIds.ValidationSummary << 16 | 4; + + /// + public const uint ValidationSummary_IncludeErrorsFromTarget = TypeIds.ValidationSummary << 16 | 6; + + /// + public const uint ItemsControl_DataSource = TypeIds.ItemsControl << 16 | 2; + + /// + public const uint Repeater_EmptyDataTemplate = TypeIds.Repeater << 16 | 2; + + /// + public const uint Repeater_ItemTemplate = TypeIds.Repeater << 16 | 4; + + /// + public const uint Repeater_RenderWrapperTag = TypeIds.Repeater << 16 | 6; + + /// + public const uint Repeater_SeparatorTemplate = TypeIds.Repeater << 16 | 8; + + /// + public const uint Repeater_WrapperTagName = TypeIds.Repeater << 16 | 10; + + /// + public const uint Repeater_RenderAsNamedTemplate = TypeIds.Repeater << 16 | 12; + + /// + public const uint HierarchyRepeater_ItemChildrenBinding = TypeIds.HierarchyRepeater << 16 | 2; + + /// + public const uint HierarchyRepeater_ItemTemplate = TypeIds.HierarchyRepeater << 16 | 4; + + /// + public const uint HierarchyRepeater_EmptyDataTemplate = TypeIds.HierarchyRepeater << 16 | 6; + + /// + public const uint HierarchyRepeater_RenderWrapperTag = TypeIds.HierarchyRepeater << 16 | 8; + + /// + public const uint HierarchyRepeater_WrapperTagName = TypeIds.HierarchyRepeater << 16 | 10; + + /// + public const uint GridView_FilterPlacement = TypeIds.GridView << 16 | 2; + + /// + public const uint GridView_EmptyDataTemplate = TypeIds.GridView << 16 | 4; + + /// + public const uint GridView_Columns = TypeIds.GridView << 16 | 6; + + /// + public const uint GridView_RowDecorators = TypeIds.GridView << 16 | 8; + + /// + public const uint GridView_HeaderRowDecorators = TypeIds.GridView << 16 | 10; + + /// + public const uint GridView_EditRowDecorators = TypeIds.GridView << 16 | 12; + + /// + public const uint GridView_SortChanged = TypeIds.GridView << 16 | 14; + + /// + public const uint GridView_ShowHeaderWhenNoData = TypeIds.GridView << 16 | 16; + + /// + public const uint GridView_InlineEditing = TypeIds.GridView << 16 | 18; + + /// + public const uint GridView_LoadData = TypeIds.GridView << 16 | 20; + + /// + public const uint GridViewColumn_HeaderText = TypeIds.GridViewColumn << 16 | 2; + + /// + public const uint GridViewColumn_HeaderTemplate = TypeIds.GridViewColumn << 16 | 4; + + /// + public const uint GridViewColumn_FilterTemplate = TypeIds.GridViewColumn << 16 | 6; + + /// + public const uint GridViewColumn_SortExpression = TypeIds.GridViewColumn << 16 | 8; + + /// + public const uint GridViewColumn_SortAscendingHeaderCssClass = TypeIds.GridViewColumn << 16 | 10; + + /// + public const uint GridViewColumn_SortDescendingHeaderCssClass = TypeIds.GridViewColumn << 16 | 12; + + /// + public const uint GridViewColumn_AllowSorting = TypeIds.GridViewColumn << 16 | 14; + + /// + public const uint GridViewColumn_CssClass = TypeIds.GridViewColumn << 16 | 16; + + /// + public const uint GridViewColumn_IsEditable = TypeIds.GridViewColumn << 16 | 18; + + /// + public const uint GridViewColumn_HeaderCssClass = TypeIds.GridViewColumn << 16 | 20; + + /// + public const uint GridViewColumn_Width = TypeIds.GridViewColumn << 16 | 22; + + /// + public const uint GridViewColumn_Visible = TypeIds.GridViewColumn << 16 | 24; + + /// + public const uint GridViewColumn_CellDecorators = TypeIds.GridViewColumn << 16 | 26; + + /// + public const uint GridViewColumn_EditCellDecorators = TypeIds.GridViewColumn << 16 | 28; + + /// + public const uint GridViewColumn_EditTemplate = TypeIds.GridViewColumn << 16 | 30; + + /// + public const uint GridViewColumn_HeaderCellDecorators = TypeIds.GridViewColumn << 16 | 32; + + /// + public const uint GridViewTextColumn_FormatString = TypeIds.GridViewTextColumn << 16 | 2; + + /// + public const uint GridViewTextColumn_ChangedBinding = TypeIds.GridViewTextColumn << 16 | 4; + + /// + public const uint GridViewTextColumn_ValueBinding = TypeIds.GridViewTextColumn << 16 | 6; + + /// + public const uint GridViewTextColumn_ValidatorPlacement = TypeIds.GridViewTextColumn << 16 | 8; + + /// + public const uint GridViewCheckBoxColumn_ValueBinding = TypeIds.GridViewCheckBoxColumn << 16 | 2; + + /// + public const uint GridViewCheckBoxColumn_ValidatorPlacement = TypeIds.GridViewCheckBoxColumn << 16 | 4; + + /// + public const uint GridViewTemplateColumn_ContentTemplate = TypeIds.GridViewTemplateColumn << 16 | 2; + + /// + public const uint DataPager_DataSet = TypeIds.DataPager << 16 | 2; + + /// + public const uint DataPager_FirstPageTemplate = TypeIds.DataPager << 16 | 4; + + /// + public const uint DataPager_LastPageTemplate = TypeIds.DataPager << 16 | 6; + + /// + public const uint DataPager_PreviousPageTemplate = TypeIds.DataPager << 16 | 8; + + /// + public const uint DataPager_NextPageTemplate = TypeIds.DataPager << 16 | 10; + + /// + public const uint DataPager_RenderLinkForCurrentPage = TypeIds.DataPager << 16 | 12; + + /// + public const uint DataPager_HideWhenOnlyOnePage = TypeIds.DataPager << 16 | 14; + + /// + public const uint DataPager_LoadData = TypeIds.DataPager << 16 | 16; + + /// + public const uint AppendableDataPager_LoadTemplate = TypeIds.AppendableDataPager << 16 | 2; + + /// + public const uint AppendableDataPager_LoadingTemplate = TypeIds.AppendableDataPager << 16 | 4; + + /// + public const uint AppendableDataPager_EndTemplate = TypeIds.AppendableDataPager << 16 | 6; + + /// + public const uint AppendableDataPager_DataSet = TypeIds.AppendableDataPager << 16 | 8; + + /// + public const uint AppendableDataPager_LoadData = TypeIds.AppendableDataPager << 16 | 10; + + /// + public const uint SelectorBase_ItemTextBinding = TypeIds.SelectorBase << 16 | 2; + + /// + public const uint SelectorBase_ItemValueBinding = TypeIds.SelectorBase << 16 | 4; + + /// + public const uint SelectorBase_SelectionChanged = TypeIds.SelectorBase << 16 | 6; + + /// + public const uint SelectorBase_ItemTitleBinding = TypeIds.SelectorBase << 16 | 8; + + /// + public const uint Selector_SelectedValue = TypeIds.Selector << 16 | 2; + + /// + public const uint MultiSelector_SelectedValues = TypeIds.MultiSelector << 16 | 2; + + /// + public const uint ListBox_Size = TypeIds.ListBox << 16 | 2; + + /// + public const uint ComboBox_EmptyItemText = TypeIds.ComboBox << 16 | 2; + + /// + public const uint SelectorItem_Text = TypeIds.SelectorItem << 16 | 2; + + /// + public const uint SelectorItem_Value = TypeIds.SelectorItem << 16 | 4; + + /// + public const uint FileUpload_UploadedFiles = TypeIds.FileUpload << 16 | 2; + + /// + public const uint FileUpload_Capture = TypeIds.FileUpload << 16 | 4; + + /// + public const uint FileUpload_MaxFileSize = TypeIds.FileUpload << 16 | 6; + + /// + public const uint FileUpload_UploadCompleted = TypeIds.FileUpload << 16 | 8; + + /// + public const uint Timer_Command = TypeIds.Timer << 16 | 2; + + /// + public const uint Timer_Interval = TypeIds.Timer << 16 | 4; + + /// + public const uint Timer_Enabled = TypeIds.Timer << 16 | 6; + + /// + public const uint UpdateProgress_Delay = TypeIds.UpdateProgress << 16 | 2; + + /// + public const uint UpdateProgress_IncludedQueues = TypeIds.UpdateProgress << 16 | 4; + + /// + public const uint UpdateProgress_ExcludedQueues = TypeIds.UpdateProgress << 16 | 6; + + /// + public const uint Label_For = TypeIds.Label << 16 | 2; + + /// + public const uint EmptyData_WrapperTagName = TypeIds.EmptyData << 16 | 2; + + /// + public const uint EmptyData_RenderWrapperTag = TypeIds.EmptyData << 16 | 4; + + /// + public const uint Content_ContentPlaceHolderID = TypeIds.Content << 16 | 2; + + /// + public const uint TemplateHost_Template = TypeIds.TemplateHost << 16 | 2; + + /// + public const uint AddTemplateDecorator_AfterTemplate = TypeIds.AddTemplateDecorator << 16 | 2; + + /// + public const uint AddTemplateDecorator_BeforeTemplate = TypeIds.AddTemplateDecorator << 16 | 4; + + /// + public const uint SpaContentPlaceHolder_DefaultRouteName = TypeIds.SpaContentPlaceHolder << 16 | 2; + + /// + public const uint SpaContentPlaceHolder_PrefixRouteName = TypeIds.SpaContentPlaceHolder << 16 | 4; + + /// + public const uint SpaContentPlaceHolder_UseHistoryApi = TypeIds.SpaContentPlaceHolder << 16 | 6; + + /// + public const uint ModalDialog_Open = TypeIds.ModalDialog << 16 | 2; + + /// + public const uint ModalDialog_CloseOnBackdropClick = TypeIds.ModalDialog << 16 | 4; + + /// + public const uint ModalDialog_Close = TypeIds.ModalDialog << 16 | 6; + + /// + public const uint HtmlLiteral_Html = TypeIds.HtmlLiteral << 16 | 2; + + /// + public const uint RequiredResource_Name = TypeIds.RequiredResource << 16 | 2; + + /// + public const uint InlineScript_Dependencies = TypeIds.InlineScript << 16 | 2; + + /// + public const uint InlineScript_Script = TypeIds.InlineScript << 16 | 4; + + /// + public const uint RoleView_Roles = TypeIds.RoleView << 16 | 2; + + /// + public const uint RoleView_IsMemberTemplate = TypeIds.RoleView << 16 | 4; + + /// + public const uint RoleView_IsNotMemberTemplate = TypeIds.RoleView << 16 | 6; + + /// + public const uint RoleView_HideForAnonymousUsers = TypeIds.RoleView << 16 | 8; + + /// + public const uint ClaimView_Claim = TypeIds.ClaimView << 16 | 2; + + /// + public const uint ClaimView_Values = TypeIds.ClaimView << 16 | 4; + + /// + public const uint ClaimView_HasClaimTemplate = TypeIds.ClaimView << 16 | 6; + + /// + public const uint ClaimView_HideForAnonymousUsers = TypeIds.ClaimView << 16 | 8; + + /// + public const uint EnvironmentView_Environments = TypeIds.EnvironmentView << 16 | 2; + + /// + public const uint EnvironmentView_IsEnvironmentTemplate = TypeIds.EnvironmentView << 16 | 4; + + /// + public const uint EnvironmentView_IsNotEnvironmentTemplate = TypeIds.EnvironmentView << 16 | 6; + + /// + public const uint JsComponent_Global = TypeIds.JsComponent << 16 | 2; + + /// + public const uint JsComponent_Name = TypeIds.JsComponent << 16 | 4; + + /// + public const uint JsComponent_WrapperTagName = TypeIds.JsComponent << 16 | 6; + + /// + public const uint PostBackHandler_EventName = TypeIds.PostBackHandler << 16 | 2; + + /// + public const uint PostBackHandler_Enabled = TypeIds.PostBackHandler << 16 | 4; + + /// + public const uint SuppressPostBackHandler_Suppress = TypeIds.SuppressPostBackHandler << 16 | 2; + + /// + public const uint ConcurrencyQueueSetting_EventName = TypeIds.ConcurrencyQueueSetting << 16 | 2; + + /// + public const uint ConcurrencyQueueSetting_ConcurrencyQueue = TypeIds.ConcurrencyQueueSetting << 16 | 4; + + /// + public const uint NamedCommand_Name = TypeIds.NamedCommand << 16 | 2; + + /// + public const uint NamedCommand_Command = TypeIds.NamedCommand << 16 | 4; + + /// + public const uint PostBack_Update = TypeIds.PostBack << 16 | 2; + + /// + public const uint PostBack_Handlers = TypeIds.PostBack << 16 | 4; + + /// + public const uint PostBack_ConcurrencyQueueSettings = TypeIds.PostBack << 16 | 6; + + /// + public const uint PostBack_Concurrency = TypeIds.PostBack << 16 | 1; + + /// + public const uint PostBack_ConcurrencyQueue = TypeIds.PostBack << 16 | 3; + + /// + public const uint FormControls_Enabled = TypeIds.FormControls << 16 | 1; + + /// + public const uint UITests_GenerateStub = TypeIds.UITests << 16 | 1; + + /// + public const uint Internal_UniqueID = TypeIds.Internal << 16 | 2; + + /// + public const uint Internal_IsNamingContainer = TypeIds.Internal << 16 | 4; + + /// + public const uint Internal_IsControlBindingTarget = TypeIds.Internal << 16 | 6; + + /// + public const uint Internal_PathFragment = TypeIds.Internal << 16 | 8; + + /// + public const uint Internal_IsServerOnlyDataContext = TypeIds.Internal << 16 | 10; + + /// + public const uint Internal_MarkupLineNumber = TypeIds.Internal << 16 | 12; + + /// + public const uint Internal_ClientIDFragment = TypeIds.Internal << 16 | 14; + + /// + public const uint Internal_IsMasterPageCompositionFinished = TypeIds.Internal << 16 | 16; + + /// + public const uint Internal_CurrentIndexBinding = TypeIds.Internal << 16 | 18; + + /// + public const uint Internal_ReferencedViewModuleInfo = TypeIds.Internal << 16 | 20; + + /// + public const uint Internal_UsedPropertiesInfo = TypeIds.Internal << 16 | 22; + + /// + public const uint Internal_IsSpaPage = TypeIds.Internal << 16 | 1; + + /// + public const uint Internal_UseHistoryApiSpaNavigation = TypeIds.Internal << 16 | 3; + + /// + public const uint Internal_DataContextType = TypeIds.Internal << 16 | 5; + + /// + public const uint Internal_MarkupFileName = TypeIds.Internal << 16 | 7; + + /// + public const uint Internal_RequestContext = TypeIds.Internal << 16 | 9; + + /// + public const uint RenderSettings_Mode = TypeIds.RenderSettings << 16 | 1; + + /// + public const uint DotvvmView_Directives = TypeIds.DotvvmView << 16 | 1; } } diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs index 92df51bbfd..248064e93d 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.TypeIds.cs @@ -1,52 +1,139 @@ +// Generated by scripts/generate-property-ids.mjs using System; using System.Collections.Immutable; using DotVVM.Framework.Controls; using DotVVM.Framework.Controls.Infrastructure; -namespace DotVVM.Framework.Binding -{ +namespace DotVVM.Framework.Binding; - static partial class DotvvmPropertyIdAssignment +static partial class DotvvmPropertyIdAssignment +{ + public static class TypeIds { - 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 ushort Internal = 16; - public const ushort RenderSettings = 17; + 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 RadioButton = 13; + public const ushort Validator = 14; + public const ushort Validation = 15; + public const ushort ValidationSummary = 16; + public const ushort ItemsControl = 17; + public const ushort Repeater = 18; + public const ushort HierarchyRepeater = 19; + public const ushort GridView = 20; + public const ushort GridViewColumn = 21; + public const ushort GridViewTextColumn = 22; + public const ushort GridViewCheckBoxColumn = 23; + public const ushort GridViewTemplateColumn = 24; + public const ushort DataPager = 25; + public const ushort AppendableDataPager = 26; + public const ushort SelectorBase = 27; + public const ushort Selector = 28; + public const ushort MultiSelector = 29; + public const ushort ListBox = 30; + public const ushort ComboBox = 31; + public const ushort SelectorItem = 32; + public const ushort FileUpload = 33; + public const ushort Timer = 34; + public const ushort UpdateProgress = 35; + public const ushort Label = 36; + public const ushort EmptyData = 37; + public const ushort Content = 38; + public const ushort TemplateHost = 39; + public const ushort AddTemplateDecorator = 40; + public const ushort SpaContentPlaceHolder = 41; + public const ushort ModalDialog = 42; + public const ushort HtmlLiteral = 43; + public const ushort RequiredResource = 44; + public const ushort InlineScript = 45; + public const ushort RoleView = 46; + public const ushort ClaimView = 47; + public const ushort EnvironmentView = 48; + public const ushort JsComponent = 49; + public const ushort PostBackHandler = 50; + public const ushort SuppressPostBackHandler = 51; + public const ushort ConcurrencyQueueSetting = 52; + public const ushort NamedCommand = 53; + public const ushort PostBack = 54; + public const ushort FormControls = 55; + public const ushort UITests = 56; + public const ushort Events = 57; + public const ushort Styles = 58; + public const ushort Internal = 59; + public const ushort RenderSettings = 60; + public const ushort DotvvmView = 61; - public static readonly ImmutableArray<(Type type, ushort id)> List = ImmutableArray.Create( - (typeof(DotvvmBindableObject), DotvvmBindableObject), - (typeof(DotvvmControl), DotvvmControl), - (typeof(HtmlGenericControl), HtmlGenericControl), - (typeof(RawLiteral), RawLiteral), - (typeof(Literal), Literal), - (typeof(ButtonBase), ButtonBase), - (typeof(Button), Button), - (typeof(LinkButton), LinkButton), - (typeof(TextBox), TextBox), - (typeof(RouteLink), RouteLink), - (typeof(CheckableControlBase), CheckableControlBase), - (typeof(CheckBox), CheckBox), - (typeof(Validator), Validator), - (typeof(Validation), Validation), - (typeof(ValidationSummary), ValidationSummary), - (typeof(Internal), Internal), - (typeof(RenderSettings), RenderSettings) - ); - } + public static readonly ImmutableArray<(Type type, ushort id)> List = ImmutableArray.Create( + (typeof(DotvvmBindableObject), DotvvmBindableObject), + (typeof(DotvvmControl), DotvvmControl), + (typeof(HtmlGenericControl), HtmlGenericControl), + (typeof(RawLiteral), RawLiteral), + (typeof(Literal), Literal), + (typeof(ButtonBase), ButtonBase), + (typeof(Button), Button), + (typeof(LinkButton), LinkButton), + (typeof(TextBox), TextBox), + (typeof(RouteLink), RouteLink), + (typeof(CheckableControlBase), CheckableControlBase), + (typeof(CheckBox), CheckBox), + (typeof(RadioButton), RadioButton), + (typeof(Validator), Validator), + (typeof(Validation), Validation), + (typeof(ValidationSummary), ValidationSummary), + (typeof(ItemsControl), ItemsControl), + (typeof(Repeater), Repeater), + (typeof(HierarchyRepeater), HierarchyRepeater), + (typeof(GridView), GridView), + (typeof(GridViewColumn), GridViewColumn), + (typeof(GridViewTextColumn), GridViewTextColumn), + (typeof(GridViewCheckBoxColumn), GridViewCheckBoxColumn), + (typeof(GridViewTemplateColumn), GridViewTemplateColumn), + (typeof(DataPager), DataPager), + (typeof(AppendableDataPager), AppendableDataPager), + (typeof(SelectorBase), SelectorBase), + (typeof(Selector), Selector), + (typeof(MultiSelector), MultiSelector), + (typeof(ListBox), ListBox), + (typeof(ComboBox), ComboBox), + (typeof(SelectorItem), SelectorItem), + (typeof(FileUpload), FileUpload), + (typeof(Timer), Timer), + (typeof(UpdateProgress), UpdateProgress), + (typeof(Label), Label), + (typeof(EmptyData), EmptyData), + (typeof(Content), Content), + (typeof(TemplateHost), TemplateHost), + (typeof(AddTemplateDecorator), AddTemplateDecorator), + (typeof(SpaContentPlaceHolder), SpaContentPlaceHolder), + (typeof(ModalDialog), ModalDialog), + (typeof(HtmlLiteral), HtmlLiteral), + (typeof(RequiredResource), RequiredResource), + (typeof(InlineScript), InlineScript), + (typeof(RoleView), RoleView), + (typeof(ClaimView), ClaimView), + (typeof(EnvironmentView), EnvironmentView), + (typeof(JsComponent), JsComponent), + (typeof(PostBackHandler), PostBackHandler), + (typeof(SuppressPostBackHandler), SuppressPostBackHandler), + (typeof(ConcurrencyQueueSetting), ConcurrencyQueueSetting), + (typeof(NamedCommand), NamedCommand), + (typeof(PostBack), PostBack), + (typeof(FormControls), FormControls), + (typeof(UITests), UITests), + (typeof(Events), Events), + (typeof(Styles), Styles), + (typeof(Internal), Internal), + (typeof(RenderSettings), RenderSettings), + (typeof(DotvvmView), DotvvmView) + ); } } diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs index 0f6e694541..2e7d87f82f 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -146,7 +146,10 @@ public static bool IsActive(DotvvmPropertyId propertyId) if (obj.properties.TryGet(id, out var value)) return value; - return propertyGroups[id.GroupId]!.DefaultValue; + if (id.IsPropertyGroup) + return propertyGroups[id.GroupId]!.DefaultValue; + else + return controls[id.TypeId].properties[id.MemberId]!.DefaultValue; } else { @@ -255,8 +258,8 @@ public static void RegisterTypes(ReadOnlySpan types, Span ids) controls[id].activeBitmap = new ulong[(DEFAULT_PROPERTY_COUNT - 1) / 64 + 1]; if (id < RESERVED_CONTROL_TYPES) { - controls[id].counterStandard = DEFAULT_PROPERTY_COUNT; - controls[id].counterNonStandard = DEFAULT_PROPERTY_COUNT; + controls[id].counterStandard = RESERVED_PROPERTY_COUNT; + controls[id].counterNonStandard = RESERVED_PROPERTY_COUNT; } typeIds[type] = id; } @@ -286,7 +289,7 @@ public static DotvvmPropertyId RegisterProperty(DotvvmProperty property) throw new InvalidOperationException($"Predefined property ID of {property} cannot be 0."); if (id >> 16 != typeId) throw new InvalidOperationException($"Predefined property ID of {property} does not match the property declaring type ID."); - if ((id & 0xffff) > DEFAULT_PROPERTY_COUNT) + if ((id & 0xffff) > RESERVED_PROPERTY_COUNT) throw new InvalidOperationException($"Predefined property ID of {property} is too high (there is only {RESERVED_PROPERTY_COUNT} reserved slots)."); if (canUseDirectAccess != (id % 2 == 0)) throw new InvalidOperationException($"Predefined property ID of {property} does not match the property canUseDirectAccess={canUseDirectAccess}. The ID must be {(canUseDirectAccess ? "even" : "odd")} number."); @@ -370,9 +373,25 @@ public static ushort RegisterPropertyGroup(DotvvmPropertyGroup group) { lock (groupRegisterLock) { - var id = (ushort)groupCounter++; - if (id == 0) - throw new Exception("Too many property groups registered already."); + ushort id; + + // Check for predefined property group ID using reflection (similar to property registration) + var declaringTypeId = RegisterType(group.DeclaringType); + if (declaringTypeId < RESERVED_CONTROL_TYPES && + typeof(PropertyGroupIds).GetField(group.DeclaringType.Name + "_" + group.Name, BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is {} predefinedId) + { + id = (ushort)predefinedId; + if (id == 0) + throw new InvalidOperationException($"Predefined property group ID of {group} cannot be 0."); + if (id > RESERVED_CONTROL_TYPES) + throw new InvalidOperationException($"Predefined property group ID of {group} is too high (there is only {RESERVED_CONTROL_TYPES} reserved slots)."); + } + else + { + id = (ushort)groupCounter++; + if (id == 0) + throw new Exception("Too many property groups registered already."); + } if (id >= propertyGroups.Length) { diff --git a/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs b/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs index f41257d340..79263b228b 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyWithFallback.cs @@ -69,7 +69,7 @@ public static DotvvmPropertyWithFallback Register private bool TryGetValue(DotvvmBindableObject control, out object? value, bool inherit = true) { - if (control.properties.TryGet(this, out value)) + if (control.properties.TryGet(Id, out value)) { return true; } diff --git a/src/Framework/Framework/Binding/ValueOrBinding.cs b/src/Framework/Framework/Binding/ValueOrBinding.cs index 54f8b24f25..58c0f87f43 100644 --- a/src/Framework/Framework/Binding/ValueOrBinding.cs +++ b/src/Framework/Framework/Binding/ValueOrBinding.cs @@ -17,10 +17,13 @@ public interface ValueOrBinding { IBinding? BindingOrDefault { get; } object? BoxedValue { get; } + + /// Returns the value or the binding from the ValueOrBinding container. Equivalent to calling vob.BindingOrDefault ?? vob.BoxedValue + object? UnwrapToObject(); } /// Represents either a binding or a constant value. In TypeScript this would be | . Note that `default()` is the same as `new (default(T))` - public struct ValueOrBinding : ValueOrBinding + public readonly struct ValueOrBinding : ValueOrBinding { private readonly IBinding? binding; [AllowNull] @@ -93,6 +96,9 @@ public IBinding GetBinding() => public T GetValue() => HasValue ? value : throw new DotvvmControlException($"Value was expected but ValueOrBinding<{typeof(T).Name}> contains a binding: {binding}.") { RelatedBinding = binding }; + /// Returns the value or the binding from the ValueOrBinding container. Equivalent to calling vob.BindingOrDefault ?? vob.BoxedValue + public object? UnwrapToObject() => binding ?? BoxingUtils.BoxGeneric(value); + /// Returns a ValueOrBinding with new type T which is a base type of the old T2 public static ValueOrBinding DownCast(ValueOrBinding createFrom) where T2 : T => new ValueOrBinding(createFrom.binding, createFrom.value!); @@ -192,6 +198,5 @@ public TResult ProcessValueBinding(DotvvmBindableObject control, Func HasBinding ? binding.ToString() : value is null ? "null" : value.ToString(); - } } diff --git a/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs b/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs index a822e42cdf..0ada556d88 100644 --- a/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs +++ b/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs @@ -15,9 +15,6 @@ public static class ValueOrBindingExtensions { - /// Returns the value or the binding from the ValueOrBinding container. Equivalent to calling vob.BindingOrDefault ?? vob.BoxedValue - public static object? UnwrapToObject(this ValueOrBinding vob) => - vob.BindingOrDefault ?? vob.BoxedValue; /// If the obj is ValueOrBinding, returns the binding or the value from the container. Equivalent to obj is ValueOrBinding vob ? vob.UnwrapToObject() : obj public static object? UnwrapToObject(object? obj) => diff --git a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs index 8722f37415..213f5382ed 100644 --- a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs +++ b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs @@ -10,6 +10,8 @@ using DotVVM.Framework.Utils; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Diagnostics; + #if Vectorize using System.Runtime.Intrinsics; #endif @@ -20,25 +22,58 @@ namespace DotVVM.Framework.Binding public readonly struct VirtualPropertyGroupDictionary : IDictionary, IReadOnlyDictionary { private readonly DotvvmBindableObject control; - private readonly DotvvmPropertyGroup group; + private readonly ushort groupId; + private readonly bool isBindingProperty; public VirtualPropertyGroupDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) { this.control = control; - this.group = group; + this.groupId = group.Id; + this.isBindingProperty = group.IsBindingProperty; + } + + internal VirtualPropertyGroupDictionary(DotvvmBindableObject control, ushort groupId, bool isBindingProperty) + { + this.control = control; + this.groupId = groupId; + this.isBindingProperty = isBindingProperty; } + public DotvvmBindableObject Control => control; + public ushort GroupId => groupId; + public DotvvmPropertyGroup Group => DotvvmPropertyIdAssignment.GetPropertyGroup(groupId).NotNull(); + DotvvmPropertyId GetMemberId(string key, bool createNew = false) { var memberId = DotvvmPropertyIdAssignment.GetGroupMemberId(key, registerIfNotFound: createNew); - return DotvvmPropertyId.CreatePropertyGroupId(group.Id, memberId); + return DotvvmPropertyId.CreatePropertyGroupId(groupId, memberId); } + TValue EvalPropertyValue(object? value) + { + if (typeof(TValue).IsValueType) + { // => cheap type check + if (value is TValue val) + return val; + return EvalPropertyValueCore(value); + } + else + { + if (!isBindingProperty && value is IBinding binding) + { + value = control.EvalBinding(binding, false); + } + return (TValue)value!; + } + } + TValue EvalPropertyValueCore(object? value) => + (TValue)control.EvalPropertyValue(Group, value)!; + public IEnumerable Keys { get { - foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) + foreach (var (p, _) in control.properties.PropertyGroup(groupId)) { yield return DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; } @@ -50,9 +85,9 @@ public IEnumerable Values { get { - foreach (var (p, value) in control.properties.PropertyGroup(group.Id)) + foreach (var (p, value) in control.properties.PropertyGroup(groupId)) { - yield return (TValue)control.EvalPropertyValue(group, value)!; + yield return EvalPropertyValue(value); } } } @@ -61,17 +96,17 @@ public IEnumerable Properties { get { - foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) + var group = DotvvmPropertyIdAssignment.GetPropertyGroup(groupId).NotNull(); + foreach (var (p, _) in control.properties.PropertyGroup(groupId)) { - var prop = group.GetDotvvmProperty(p.MemberId); - yield return prop; + yield return group.GetDotvvmProperty(p.MemberId); } } } - public int Count => control.properties.CountPropertyGroup(group.Id); + public int Count => control.properties.CountPropertyGroup(groupId); - public bool Any() => control.properties.ContainsPropertyGroup(group.Id); + public bool Any() => control.properties.ContainsPropertyGroup(groupId); public bool IsReadOnly => false; @@ -86,14 +121,11 @@ public TValue this[string key] { var p = GetMemberId(key); if (control.properties.TryGet(p, out var value)) - return (TValue)control.EvalPropertyValue(group, value)!; + return EvalPropertyValue(value); else - return (TValue)group.DefaultValue!; - } - set - { - control.properties.Set(GetMemberId(key), value); + return (TValue)Group.DefaultValue!; } + set => control.properties.Set(GetMemberId(key, createNew: true), 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. @@ -115,7 +147,7 @@ public TValue this[string key] if (control.properties.TryGet(GetMemberId(key), out var value)) return value; else - return group.DefaultValue!; + return DotvvmPropertyIdAssignment.GetPropertyGroup(groupId)!.DefaultValue!; } /// Adds value or overwrites the property identified by . @@ -130,14 +162,18 @@ public void Set(string key, TValue value) => public void SetBinding(string key, IBinding binding) => control.properties.Set(GetMemberId(key, createNew: true), binding); - public bool ContainsKey(string key) + internal void SetInternal(ushort key, object? value) { - return control.properties.Contains(GetMemberId(key)); + Debug.Assert(DotvvmPropertyIdAssignment.GetGroupMemberName(key) is not null); + control.properties.Set(DotvvmPropertyId.CreatePropertyGroupId(groupId, key), value); } + public bool ContainsKey(string key) => + control.properties.Contains(GetMemberId(key)); + private void AddOnConflict(DotvvmPropertyId id, string key, object? value) { - var merger = this.group.ValueMerger; + var merger = this.Group.ValueMerger; if (merger is null) 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); @@ -146,7 +182,7 @@ private void AddOnConflict(DotvvmPropertyId id, string key, object? value) internal void AddInternal(ushort key, object? val) { - var prop = DotvvmPropertyId.CreatePropertyGroupId(group.Id, key); + var prop = DotvvmPropertyId.CreatePropertyGroupId(groupId, key); if (!control.properties.TryAdd(prop, val)) AddOnConflict(prop, prop.GroupMemberName.NotNull(), val); } @@ -154,21 +190,23 @@ internal void AddInternal(ushort key, object? val) public void Add(string key, ValueOrBinding value) { var prop = GetMemberId(key, createNew: true); - object? val = value.UnwrapToObject(); // TODO VOB boxing + object? val = value.UnwrapToObject(); if (!control.properties.TryAdd(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 - public void Add(string key, TValue value) => - this.Add(key, new ValueOrBinding(value)); - - /// Adds the property identified by . If the property is already set, it tries appending the value using the group's - public void AddBinding(string key, IBinding? binding) + public void Add(string key, TValue value) { - Add(key, new ValueOrBinding(binding!)); + var prop = GetMemberId(key, createNew: true); + var val = BoxingUtils.BoxGeneric(value); + if (!control.properties.TryAdd(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 + public void AddBinding(string key, IBinding? binding) => Add(key, new ValueOrBinding(binding!)); + public void CopyFrom(IEnumerable> values, bool clear = false) { if (clear) this.Clear(); @@ -186,14 +224,29 @@ public void CopyFrom(IEnumerable>> v Set(item.Key, item.Value); } } - public static IDictionary CreateValueDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) + + public void CopyFrom(VirtualPropertyGroupDictionary values, bool clear = false) + where TValue2 : TValue + { + if (clear) this.Clear(); + foreach (var (oldId, value) in values.control.properties.PropertyGroup(values.groupId)) + { + var newId = DotvvmPropertyId.CreatePropertyGroupId(groupId, oldId.MemberId); + control.properties.Set(newId, value); + } + } + + public static IDictionary CreateValueDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) => + CreateValueDictionary(control, group.Id); + + internal static IDictionary CreateValueDictionary(DotvvmBindableObject control, ushort groupId) { Dictionary result; #if Vectorize // don't bother counting without vector instructions - if (Vector256.IsHardwareAccelerated) + if (Vector128.IsHardwareAccelerated) { - var count = control.properties.CountPropertyGroup(group.Id); + var count = control.properties.CountPropertyGroup(groupId); result = new(count); if (count == 0) return result; @@ -205,10 +258,10 @@ public static IDictionary CreateValueDictionary(DotvvmBindableOb #endif foreach (var (p, valueRaw) in control.properties) { - if (p.IsInPropertyGroup(group.Id)) + if (p.IsInPropertyGroup(groupId)) { var name = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; - var valueObj = control.EvalPropertyValue(group, valueRaw); + var valueObj = control.EvalPropertyValue(DotvvmPropertyId.CreatePropertyGroupId(groupId, 1), valueRaw); if (valueObj is TValue value) result.Add(name, value); else if (valueObj is null) @@ -218,14 +271,17 @@ public static IDictionary CreateValueDictionary(DotvvmBindableOb return result; } - public static IDictionary> CreatePropertyDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) + public static IDictionary> CreatePropertyDictionary(DotvvmBindableObject control, DotvvmPropertyGroup group) => + CreatePropertyDictionary(control, group.Id); + + internal static IDictionary> CreatePropertyDictionary(DotvvmBindableObject control, ushort groupId) { Dictionary> result; #if Vectorize // don't bother counting without vector instructions - if (Vector256.IsHardwareAccelerated) + if (Vector128.IsHardwareAccelerated) { - var count = control.properties.CountPropertyGroup(group.Id); + var count = control.properties.CountPropertyGroup(groupId); result = new(count); if (count == 0) return result; @@ -237,7 +293,7 @@ public static IDictionary> CreatePropertyDictiona #endif foreach (var (p, valRaw) in control.properties) { - if (p.IsInPropertyGroup(group.Id)) + if (p.IsInPropertyGroup(groupId)) { var name = DotvvmPropertyIdAssignment.GetGroupMemberName(p.MemberId)!; result.Add(name, ValueOrBinding.FromBoxedValue(valRaw)); @@ -256,10 +312,10 @@ public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) #pragma warning restore CS8767 { var memberId = DotvvmPropertyIdAssignment.GetGroupMemberId(key, registerIfNotFound: false); - var p = DotvvmPropertyId.CreatePropertyGroupId(group.Id, memberId); + var p = DotvvmPropertyId.CreatePropertyGroupId(groupId, memberId); if (control.properties.TryGet(p, out var valueRaw)) { - value = (TValue)control.EvalPropertyValue(group, valueRaw)!; + value = EvalPropertyValue(valueRaw); return true; } else @@ -270,35 +326,9 @@ public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) } /// Adds the property-value pair to the dictionary. If the property is already set, it tries appending the value using the group's - public void Add(KeyValuePair item) - { - Add(item.Key, item.Value); - } - - public void Clear() - { - // we want to avoid allocating the list if there is only one property - DotvvmPropertyId toRemove = default; - List? toRemoveRest = null; + public void Add(KeyValuePair item) => Add(item.Key, item.Value); - foreach (var (p, _) in control.properties.PropertyGroup(group.Id)) - { - if (toRemove.Id == 0) - toRemove = p; - else - { - toRemoveRest ??= new List(); - toRemoveRest.Add(p); - } - } - - if (toRemove.Id != 0) - control.properties.Remove(toRemove); - - if (toRemoveRest is {}) - foreach (var p in toRemoveRest) - control.properties.Remove(p); - } + public void Clear() => control.properties.ClearPropertyGroup(groupId); public bool Contains(KeyValuePair item) { @@ -331,7 +361,7 @@ public bool Remove(KeyValuePair item) /// Enumerates all keys and values. If a property contains a binding, it will be automatically evaluated. public Enumerator GetEnumerator() => - new Enumerator(control, group, control.properties.EnumeratePropertyGroup(group.Id)); + new Enumerator(control, Group, control.properties.EnumeratePropertyGroup(groupId)); IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -358,7 +388,7 @@ public IEnumerable Values { get { - foreach (var (_, value) in self.control.properties.PropertyGroup(self.group.Id)) + foreach (var (_, value) in self.control.properties.PropertyGroup(self.groupId)) yield return value; } } @@ -367,7 +397,7 @@ public IEnumerable Values public bool ContainsKey(string key) => self.ContainsKey(key); - public RawValuesEnumerator GetEnumerator() => new RawValuesEnumerator(self.control.properties.EnumeratePropertyGroup(self.group.Id)); + public RawValuesEnumerator GetEnumerator() => new RawValuesEnumerator(self.control.properties.EnumeratePropertyGroup(self.groupId)); IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs index 1f04527a48..aafda71140 100644 --- a/src/Framework/Framework/Compilation/Binding/TypeConversions.cs +++ b/src/Framework/Framework/Compilation/Binding/TypeConversions.cs @@ -48,6 +48,10 @@ public class TypeConversion public static Expression BoxToObject(Expression src) { var type = src.Type; + if (src is ConstantExpression { Value: bool boolean }) + return Expression.Field(null, typeof(BoxingUtils), boolean ? nameof(BoxingUtils.True) : nameof(BoxingUtils.False)); + if (src is ConstantExpression { Value: (int)0 }) + return Expression.Field(null, typeof(BoxingUtils), nameof(BoxingUtils.Zero)); if (type == typeof(bool) || type == typeof(bool?) || type == typeof(int) || type == typeof(int?)) return Expression.Call(typeof(BoxingUtils), "Box", Type.EmptyTypes, src); if (src is ConstantExpression { Value: var constant }) diff --git a/src/Framework/Framework/Compilation/BindingCompiler.cs b/src/Framework/Framework/Compilation/BindingCompiler.cs index 7c03ae06e3..19f0ab5cae 100644 --- a/src/Framework/Framework/Compilation/BindingCompiler.cs +++ b/src/Framework/Framework/Compilation/BindingCompiler.cs @@ -297,7 +297,7 @@ public static DotvvmBindableObject GetContextControl(int skip, DotvvmBindableObj { while (control != null) { - if (control.properties.Contains(DotvvmBindableObject.DataContextProperty)) + if (control.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext)) { if (skip == 0) return control; @@ -315,7 +315,7 @@ public static (DotvvmBindableObject control, T context, bool isNull) GetContextA { while (control != null) { - if (control.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var contextRaw)) + if (control.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, out var contextRaw)) { if (skip == 0) { diff --git a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs index 995d796447..1f9960df64 100644 --- a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs +++ b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs @@ -92,12 +92,18 @@ public class CurrentCollectionIndexExtensionParameter : BindingExtensionParamete } - internal static int GetIndex(DotvvmBindableObject c) => - (c.NotNull("control is null, is the binding executed in the right data context?") - .GetAllAncestors(true, false) - .OfType() - .FirstOrDefault() ?? throw new DotvvmControlException(c, "Could not find ancestor DataItemContainer that stores the current collection index.")) - .DataItemIndex ?? throw new DotvvmControlException(c, "Nearest DataItemContainer does have the collection index specified."); + internal static int GetIndex(DotvvmBindableObject c) + { + c.NotNull("control is null, is the binding executed in the right data context?"); + for (var ancestor = c; ancestor != null; ancestor = ancestor.Parent) + { + if (ancestor is DataItemContainer container) + { + return container.DataItemIndex ?? throw new DotvvmControlException(c, "Nearest DataItemContainer does have the collection index specified."); + } + } + throw new DotvvmControlException(c, "Could not find ancestor DataItemContainer that stores the current collection index."); + } public override Expression GetServerEquivalent(Expression controlParameter) { diff --git a/src/Framework/Framework/Controls/DataItemContainer.cs b/src/Framework/Framework/Controls/DataItemContainer.cs index e56ba90f6e..2c61543ada 100644 --- a/src/Framework/Framework/Controls/DataItemContainer.cs +++ b/src/Framework/Framework/Controls/DataItemContainer.cs @@ -50,10 +50,14 @@ public int? DataItemIndex { if (this.index is int) return this.index; - var value = GetValue(Internal.UniqueIDProperty); + var value = properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID); return value is string id && int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) ? index : (int?)null; } - set { this.index = value; SetValue(Internal.UniqueIDProperty, value?.ToString()); } + set + { + this.index = value; + properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID, value?.ToString()); + } } public bool RenderItemBinding { get; set; } = true; diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index 355f8768cf..e664f6bcd4 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -99,7 +99,7 @@ public object? DataContext } return null; } - set { this.properties.Set(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, value); } + set => properties.Set(DotvvmPropertyIdAssignment.PropertyIds.DotvvmBindableObject_DataContext, value); } DotvvmBindableObject IDotvvmObjectLike.Self => this; @@ -147,7 +147,7 @@ public T GetValue(DotvvmProperty property, bool inherit = true) return value; } - private object? EvalBinding(IBinding binding, bool isDataContext) + internal object? EvalBinding(IBinding binding, bool isDataContext) { DotvvmBindableObject control = this; // DataContext is always bound to it's parent @@ -201,9 +201,7 @@ public void SetValue(DotvvmProperty property, ValueOrBinding valueOrBindin public ValueOrBinding GetValueOrBinding(DotvvmProperty property, bool inherit = true) { var value = this.GetValueRaw(property, inherit); - if (value is IBinding binding) - return new ValueOrBinding(binding); - else return new ValueOrBinding((T)value!); + return ValueOrBinding.FromBoxedValue(value); } /// @@ -248,6 +246,17 @@ public void SetValueRaw(DotvvmProperty property, object? value) property.SetValue(this, value); } + /// + /// Sets the value or a binding to the specified property. + /// + public void SetValueRaw(DotvvmPropertyId property, object? value) + { + if (property.CanUseFastAccessors) + properties.Set(property, value); + else + property.PropertyInstance.SetValue(this, value); + } + /// /// Gets the binding set to a specified property. Returns null if the property is not set or if the value is not a binding. /// @@ -339,7 +348,7 @@ internal IEnumerable GetDataContextHierarchy() var c = this; while (c != null) { - if (c.properties.TryGet(Internal.IsControlBindingTargetProperty, out var x) && (bool)x!) + if (c.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsControlBindingTarget, out var x) && (bool)x!) { return c; } @@ -354,7 +363,7 @@ internal IEnumerable GetDataContextHierarchy() public DotvvmBindableObject? GetClosestControlBindingTarget(out int numberOfDataContextChanges) => (Parent ?? this).GetClosestWithPropertyValue( out numberOfDataContextChanges, - (control, _) => control.properties.TryGet(Internal.IsControlBindingTargetProperty, out var x) && (bool)x!); + (control, _) => control.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsControlBindingTarget, out var x) && (bool)x!); /// /// Gets the closest control binding target and returns number of DataContext changes since the target. Returns null if the control is not found. @@ -419,8 +428,9 @@ public bool HasBinding(DotvvmProperty property) /// public IEnumerable> GetAllBindings() { - return Properties.Where(p => p.Value is IBinding) - .Select(p => new KeyValuePair(p.Key, (IBinding)p.Value!)); + foreach (var (propId, value) in properties) + if (value is IBinding binding) + yield return new KeyValuePair(propId.PropertyInstance, binding); } /// diff --git a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs index 72088d67a7..a961e1aecb 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs @@ -193,13 +193,16 @@ public static TControl AddAttribute(this TControl control, string attr public static TControl AddAttribute(this TControl control, string attribute, ValueOrBinding? value) where TControl : IControlWithHtmlAttributes { - return AddAttribute(control, attribute, value?.UnwrapToObject()); + if (value is { } v) + control.Attributes.Add(attribute, v.UnwrapToObject()); + return control; } /// Appends a value into the specified html attribute. If the attribute already exists, the old and new values are merged. Returns for fluent API usage. public static TControl AddAttribute(this TControl control, string attribute, ValueOrBinding value) where TControl : IControlWithHtmlAttributes { - return AddAttribute(control, attribute, value.UnwrapToObject()); + control.Attributes.Add(attribute, value.UnwrapToObject()); + return control; } /// Appends a list of css attributes to the control. If the attributes already exist, the old and new values are merged. Returns for fluent API usage. @@ -215,8 +218,7 @@ public static TControl AddAttributes(this TControl control, IE public static TControl AddAttributes(this TControl control, VirtualPropertyGroupDictionary attributes) where TControl : IControlWithHtmlAttributes { - foreach (var a in attributes.RawValues) - AddAttribute(control, a.Key, a.Value); + control.Attributes.CopyFrom(attributes); return control; } diff --git a/src/Framework/Framework/Controls/DotvvmControl.cs b/src/Framework/Framework/Controls/DotvvmControl.cs index dd575af9d2..7a1715626b 100644 --- a/src/Framework/Framework/Controls/DotvvmControl.cs +++ b/src/Framework/Framework/Controls/DotvvmControl.cs @@ -80,13 +80,13 @@ public static readonly DotvvmProperty ClientIDProperty ValueOrBinding? EnsureClientId() { - if (!IsPropertySet(IDProperty)) + if (!properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ID)) { return null; } - if (IsPropertySet(ClientIDProperty)) + if (properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ClientID, out var rawClientId)) { - return GetValueOrBinding(ClientIDProperty); + return ValueOrBinding.FromBoxedValue(rawClientId); } var id = CreateClientId(); @@ -189,7 +189,7 @@ public virtual void Render(IHtmlWriter writer, IDotvvmRequestContext context) writer.SetErrorContext(this); - if (properties.Contains(PostBack.UpdateProperty)) + if (properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.PostBack_Update)) { AddDotvvmUniqueIdAttribute(); } @@ -219,7 +219,7 @@ protected void AddDotvvmUniqueIdAttribute() { throw new DotvvmControlException(this, "Postback.Update cannot be set on property which don't render html attributes."); } - htmlAttributes.Attributes.Set("data-dotvvm-id", GetDotvvmUniqueId().UnwrapToObject()); + htmlAttributes.Attributes.SetInternal(DotvvmPropertyIdAssignment.GroupMembers.data_dotvvm_id, GetDotvvmUniqueId().UnwrapToObject()); } protected struct RenderState @@ -464,7 +464,7 @@ public DotvvmControl GetNamingContainer() /// public static bool IsNamingContainer(DotvvmBindableObject control) { - return (bool)Internal.IsNamingContainerProperty.GetValue(control)!; + return control.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsNamingContainer, out var value) && (bool)value!; } /// @@ -509,7 +509,7 @@ public ValueOrBinding GetDotvvmUniqueId(ValueOrBinding prefix = // build the client ID JoinValuesOrBindings(GetUniqueIdFragments(), prefix, suffix); - private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragments, ValueOrBinding prefix, ValueOrBinding suffix) + private ValueOrBinding JoinValuesOrBindings(List fragments, ValueOrBinding prefix, ValueOrBinding suffix) { if (fragments.Count == 1 && prefix.ValueIsNullOrEmpty() && suffix.ValueIsNullOrEmpty()) { @@ -576,14 +576,15 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm { var fragments = new List { - Internal.UniqueIDProperty.GetValue(this) + properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID) }; - foreach (var ancestor in GetAllAncestors()) + for (var ancestor = Parent; ancestor is {}; ancestor = ancestor.Parent) { if (IsNamingContainer(ancestor)) { fragments.Add( - Internal.ClientIDFragmentProperty.GetValue(ancestor) ?? Internal.UniqueIDProperty.GetValue(ancestor) + ancestor.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_ClientIDFragment) ?? + ancestor.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID) ); } } @@ -593,10 +594,9 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm private List? GetClientIdFragments() { - var rawId = IDProperty.GetValue(this); - + var rawId = properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ID); // can't generate ID from nothing - if (rawId == null) return null; + if (rawId is null) return null; var fragments = new List { rawId }; if (ClientIDMode == ClientIDMode.Static) @@ -607,7 +607,7 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm DotvvmControl? childContainer = null; var searchingForIdElement = false; - foreach (var ancestor in GetAllAncestors()) + for (var ancestor = Parent; ancestor is {}; ancestor = ancestor.Parent) { if (ancestor is not DotvvmControl ancestorControl) { @@ -622,11 +622,11 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm } searchingForIdElement = false; - if (Internal.ClientIDFragmentProperty.GetValue(ancestorControl) is {} clientIdExpression) + if (ancestorControl.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_ClientIDFragment) is {} clientIdExpression) { fragments.Add(clientIdExpression); } - else if (ancestorControl.GetValueRaw(IDProperty) is var ancestorId && ancestorId is not null or "") + else if (ancestorControl.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ID) is var ancestorId && ancestorId is not null or "") { // add the ID fragment fragments.Add(ancestorId); @@ -638,9 +638,9 @@ private ValueOrBinding JoinValuesOrBindings(IReadOnlyList fragm } } - if (searchingForIdElement && ancestorControl.IsPropertySet(ClientIDProperty)) + if (searchingForIdElement && ancestorControl.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ClientID, out var clientId)) { - fragments.Add(ancestorControl.GetValueRaw(ClientIDProperty)); + fragments.Add(clientId); searchingForIdElement = false; } diff --git a/src/Framework/Framework/Controls/DotvvmControlCollection.cs b/src/Framework/Framework/Controls/DotvvmControlCollection.cs index 3fc4f9c28b..06d5098cbd 100644 --- a/src/Framework/Framework/Controls/DotvvmControlCollection.cs +++ b/src/Framework/Framework/Controls/DotvvmControlCollection.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using DotVVM.Framework.Binding; using DotVVM.Framework.Controls.Infrastructure; using DotVVM.Framework.Hosting; using DotVVM.Framework.Runtime; @@ -219,9 +220,8 @@ private void SetParent(DotvvmControl item) item.Parent = parent; - if (!item.properties.Contains(Internal.UniqueIDProperty) && - parent.properties.TryGet(Internal.UniqueIDProperty, out var parentId) && - parentId is not null) + if (!item.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID) && + parent.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID) is {} parentId) { AssignUniqueIds(item, parentId); } @@ -276,17 +276,17 @@ void AssignUniqueIds(DotvvmControl item, object parentId) // if the parent is a naming container, we don't need to duplicate it's unique id into the control // when we don't do this Repeater generates different ids in server-side and client-side mode, because the // UniqueIDProperty of the parent DataItemContainer is different, but the ClientIdFragment is the same - if (item.Parent!.properties.Contains(Internal.IsNamingContainerProperty)) + if (item.Parent!.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.Internal_IsNamingContainer)) { parentId = ""; } var id = parentId.ToString() + "a" + uniqueIdCounter.ToString(); uniqueIdCounter++; - item.properties.Set(Internal.UniqueIDProperty, id); + item.properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID, id); foreach (var c in item.Children.controls) { - if (!c.properties.Contains(Internal.UniqueIDProperty)) + if (!c.properties.Contains(DotvvmPropertyIdAssignment.PropertyIds.Internal_UniqueID)) item.Children.AssignUniqueIds(c, id); } } @@ -304,7 +304,7 @@ internal void ValidateParentsLifecycleEvents() DotvvmBindableObject? c = parent; while (c != null) { - if (c.properties.TryGet(Internal.RequestContextProperty, out var context)) + if (c.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_RequestContext, out var context)) return (IDotvvmRequestContext?)context; c = c.Parent; } diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index 922ef29c5c..8c0c44aac8 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -99,7 +99,7 @@ private readonly void CheckInvariant() case TableState.Array16: Debug.Assert(keys is {}); Debug.Assert(values is object[]); - Debug.Assert(keys.Length == valuesAsArray.Length); + Debug.Assert(keys.Length == valuesAsArray.Length, $"keys.Length={keys.Length} != values.Length={valuesAsArray.Length}"); Debug.Assert(keys.Length == (state == TableState.Array8 ? 8 : 16)); for (int i = keys.Length - 1; i >= 0 ; i--) { @@ -310,6 +310,79 @@ private readonly int CountPropertyGroupOutlined(ushort groupId) } } + public void ClearPropertyGroup(ushort groupId) + { + switch (state) + { + case TableState.Empty: + return; + case TableState.Array8: + case TableState.Array16: + { + var bitmap = Impl.FindGroupBitmap(this.keys!, groupId); + int index = 0; + if (bitmap == 0) + return; + OwnKeys(); + OwnValues(); + do + { + int bit = BitOperations.TrailingZeroCount(bitmap); + bitmap >>= (bit + 1); + index += bit; + Debug.Assert(keys![index].IsInPropertyGroup(groupId), $"Key {keys[index]} at index {index} is not in property group {groupId}"); + keys![index] = default; + valuesAsArray[index] = null; + index++; + } while (bitmap != 0); + CheckInvariant(); + return; + } + case TableState.Dictinary: + { + var dict = this.valuesAsDictionary; + // we want to avoid allocating the list if there is only one property + DotvvmPropertyId toRemove = default; + List? toRemoveRest = null; + + foreach (var (p, _) in dict) + { + if (p.IsInPropertyGroup(groupId)) + { + if (toRemove.Id == 0) + toRemove = p; + else + { + toRemoveRest ??= new List(); + toRemoveRest.Add(p); + } + } + } + + if (toRemove.Id != 0) + { + if (!ownsValues) + { + CloneValues(); + dict = this.valuesAsDictionary; + } + + dict.Remove(toRemove); + } + + if (toRemoveRest is {}) + { + Debug.Assert(ownsValues); + foreach (var p in toRemoveRest) + dict.Remove(p); + } + CheckInvariant(); + return; + } + } + Impl.Fail(); + } + [MethodImpl(Inline)] public readonly bool TryGet(DotvvmProperty p, out object? value) => TryGet(p.Id, out value); [MethodImpl(Inline)] @@ -375,6 +448,15 @@ private readonly bool TryGetOutlined(DotvvmPropertyId p, out object? value) [MethodImpl(NoInlining), DoesNotReturn] private readonly object? ThrowKeyNotFound(DotvvmPropertyId p) => throw new KeyNotFoundException($"Property {p} was not found."); + [MethodImpl(Inline)] + public readonly object? GetOrNull(DotvvmProperty p) => GetOrNull(p.Id); + [MethodImpl(Inline)] + public readonly object? GetOrNull(DotvvmPropertyId p) + { + TryGet(p, out var x); + return x; + } + [MethodImpl(Inline)] public void Set(DotvvmProperty p, object? value) => Set(p.Id, value); // not necessarily great for inlining @@ -409,7 +491,7 @@ public void Set(DotvvmPropertyId p, object? value) } private void SetOutlined(DotvvmPropertyId p, object? value) { - TailRecursion: + TailCall: var keys = this.keys; if (keys is {}) @@ -443,7 +525,7 @@ private void SetOutlined(DotvvmPropertyId p, object? value) else { IncreaseSize(); - goto TailRecursion; + goto TailCall; } } else if (values == null) @@ -501,8 +583,7 @@ public bool TryAdd(DotvvmPropertyId p, object? value) private bool TryAddOulined(DotvvmPropertyId p, object? value) { - - TailRecursion: + TailCall: var keys = this.keys; if (keys != null) @@ -536,7 +617,7 @@ private bool TryAddOulined(DotvvmPropertyId p, object? value) else { IncreaseSize(); - goto TailRecursion; + goto TailCall; } } if (this.values == null) @@ -547,7 +628,6 @@ private bool TryAddOulined(DotvvmPropertyId p, object? value) else { // System.Dictionary backend - var valuesAsDictionary = this.valuesAsDictionary; #if CSharp8Polyfill if (valuesAsDictionary.TryGetValue(p, out var existingValue)) return Object.ReferenceEquals(existingValue, value); @@ -863,7 +943,7 @@ void SwitchToSimdDict() } var properties = new DotvvmPropertyId[valuesAsDictionary.Count >= 8 ? 16 : 8]; - var values = new object?[properties.Length >= 8 ? 16 : 8]; + var values = new object?[properties.Length]; int j = 0; foreach (var x in valuesAsDictionary) { diff --git a/src/Framework/Framework/Controls/HtmlGenericControl.cs b/src/Framework/Framework/Controls/HtmlGenericControl.cs index d5c819a162..0018dbd2cc 100644 --- a/src/Framework/Framework/Controls/HtmlGenericControl.cs +++ b/src/Framework/Framework/Controls/HtmlGenericControl.cs @@ -90,7 +90,7 @@ public HtmlGenericControl(string? tagName, TextOrContentCapability? content, Htm /// A dictionary of html attributes that are rendered on this control's html tag. [PropertyGroup(new[] { "", "html:" })] - public VirtualPropertyGroupDictionary Attributes => new(this, AttributesGroupDescriptor); + public VirtualPropertyGroupDictionary Attributes => new(this, DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_Attributes, false); /// A dictionary of html attributes that are rendered on this control's html tag. [MarkupOptions(MappingMode = MappingMode.Attribute, AllowBinding = true, AllowHardCodedValue = true, AllowValueMerging = true, AttributeValueMerger = typeof(HtmlAttributeValueMerger), AllowAttributeWithoutValue = true)] @@ -99,7 +99,7 @@ public HtmlGenericControl(string? tagName, TextOrContentCapability? content, Htm /// A dictionary of css classes. All classes whose value is `true` will be placed in the `class` attribute. [PropertyGroup("Class-", ValueType = typeof(bool))] - public VirtualPropertyGroupDictionary CssClasses => new(this, CssClassesGroupDescriptor); + public VirtualPropertyGroupDictionary CssClasses => new(this, DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssClasses, false); /// A dictionary of css classes. All classes whose value is `true` will be placed in the `class` attribute. public static DotvvmPropertyGroup CssClassesGroupDescriptor = @@ -107,7 +107,7 @@ public HtmlGenericControl(string? tagName, TextOrContentCapability? content, Htm /// A dictionary of css styles which will be placed in the `style` attribute. [PropertyGroup("Style-")] - public VirtualPropertyGroupDictionary CssStyles => new(this, CssStylesGroupDescriptor); + public VirtualPropertyGroupDictionary CssStyles => new(this, DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssStyles, false); /// A dictionary of css styles which will be placed in the `style` attribute. public static DotvvmPropertyGroup CssStylesGroupDescriptor = @@ -191,15 +191,15 @@ protected bool TouchProperty(DotvvmPropertyId prop, object? value, ref RenderSta r.HasId = true; else if (prop == DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_InnerText) r.InnerText = value; - else if (prop == PostBack.UpdateProperty.Id) + else if (prop == DotvvmPropertyIdAssignment.PropertyIds.PostBack_Update) r.HasPostbackUpdate = (bool)this.EvalPropertyValue(prop, value)!; else if (prop.IsPropertyGroup) { - if (prop.IsInPropertyGroup(CssClassesGroupDescriptor.Id)) + if (prop.IsInPropertyGroup(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssClasses)) r.HasClass = true; - else if (prop.IsInPropertyGroup(CssStylesGroupDescriptor.Id)) + else if (prop.IsInPropertyGroup(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssStyles)) r.HasStyle = true; - else if (prop.IsInPropertyGroup(AttributesGroupDescriptor.Id)) + else if (prop.IsInPropertyGroup(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_Attributes)) r.HasAttributes = true; else return false; } diff --git a/src/Framework/Framework/Controls/Internal.cs b/src/Framework/Framework/Controls/Internal.cs index 4d9a5c4547..28b274b5fb 100644 --- a/src/Framework/Framework/Controls/Internal.cs +++ b/src/Framework/Framework/Controls/Internal.cs @@ -82,7 +82,7 @@ public static class InternalPropertyExtensions { for (; obj != null; obj = obj.Parent) { - if (obj.properties.TryGet(Internal.DataContextTypeProperty, out var v)) + if (obj.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_DataContextType, out var v)) return (DataContextStack?)v; } return null; @@ -91,7 +91,7 @@ public static class InternalPropertyExtensions { if (inherit) return obj.GetDataContextType(); - else if (obj.properties.TryGet(Internal.DataContextTypeProperty, out var v)) + else if (obj.properties.TryGet(DotvvmPropertyIdAssignment.PropertyIds.Internal_DataContextType, out var v)) return (DataContextStack?)v; else return null; @@ -101,7 +101,7 @@ public static class InternalPropertyExtensions public static TControl SetDataContextType(this TControl control, DataContextStack? stack) where TControl : DotvvmBindableObject { - control.properties.Set(Internal.DataContextTypeProperty, stack); + control.properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Internal_DataContextType, stack); return control; } } diff --git a/src/Framework/Framework/Controls/Label.cs b/src/Framework/Framework/Controls/Label.cs index 94972d2b2c..a5815301d6 100644 --- a/src/Framework/Framework/Controls/Label.cs +++ b/src/Framework/Framework/Controls/Label.cs @@ -48,7 +48,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest else if (id is {}) { // let the html generic control evaluate the binding - this.Attributes.Add("for", id.UnwrapToObject()); + this.Attributes.Add("for", id.Value.BindingOrDefault); } base.AddAttributesToRender(writer, context); diff --git a/src/Framework/Framework/Controls/Literal.cs b/src/Framework/Framework/Controls/Literal.cs index 293dd46d68..d9376a69c8 100644 --- a/src/Framework/Framework/Controls/Literal.cs +++ b/src/Framework/Framework/Controls/Literal.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Net; using System.Runtime.CompilerServices; @@ -23,7 +24,7 @@ public sealed class Literal : HtmlGenericControl public string Text { get { return GetValue(TextProperty)?.ToString() ?? ""; } - set { SetValue(TextProperty, value); } + set { properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, value); } } public static readonly DotvvmProperty TextProperty = @@ -36,7 +37,7 @@ public string Text public string? FormatString { get { return (string?)GetValue(FormatStringProperty); } - set { SetValue(FormatStringProperty, value); } + set { properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_FormatString, value); } } public static readonly DotvvmProperty FormatStringProperty = @@ -49,7 +50,7 @@ public string? FormatString public bool RenderSpanElement { get { return (bool)GetValue(RenderSpanElementProperty)!; } - set { SetValue(RenderSpanElementProperty, value); } + set { properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_RenderSpanElement, value); } } public static readonly DotvvmProperty RenderSpanElementProperty = @@ -75,19 +76,19 @@ public Literal(string text, bool renderSpan = false) : base("span", false) public Literal(ValueOrBinding text, bool renderSpan = false): this() { - SetValue(TextProperty, text); + properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, text.UnwrapToObject()); RenderSpanElement = renderSpan; } public Literal(ValueOrBinding text, bool renderSpan = false): this() { - SetValue(TextProperty, text); + properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, text.UnwrapToObject()); RenderSpanElement = renderSpan; } public Literal(IStaticValueBinding text, bool renderSpan = false): this() { - SetBinding(TextProperty, text); + properties.Set(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, text); RenderSpanElement = renderSpan; } diff --git a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs index 7d02b0fc66..bd3bb5f9ad 100644 --- a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs +++ b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs @@ -102,10 +102,10 @@ private static int FindSlot8(ref DotvvmPropertyId keys, DotvvmPropertyId p) { Debug.Assert(Vector256.Count == AdhocTableSize); var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); - var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); - if (eq != 0) + var eq = Vector256.Equals(v, Vector256.Create(p.Id)); + if (eq != Vector256.Zero) { - return BitOperations.TrailingZeroCount(eq); + return BitOperations.TrailingZeroCount(eq.ExtractMostSignificantBits()); } else { @@ -192,16 +192,17 @@ private static int FindSlotOrFree8(ref DotvvmPropertyId keys, DotvvmPropertyId p if (Vector128.IsHardwareAccelerated) { var v = Unsafe.ReadUnaligned>(ref Unsafe.As(ref keys)); - var eq = Vector256.Equals(v, Vector256.Create(p.Id)).ExtractMostSignificantBits(); - exists = eq != 0; - if (eq != 0) + var eq = Vector256.Equals(v, Vector256.Create(p.Id)); + if (eq != Vector256.Zero) { - return BitOperations.TrailingZeroCount(eq); + exists = true; + return BitOperations.TrailingZeroCount(eq.ExtractMostSignificantBits()); } - var empty = Vector256.Equals(v, Vector256.Zero).ExtractMostSignificantBits(); - if (empty != 0) + exists = false; + var empty = Vector256.Equals(v, Vector256.Zero); + if (empty != Vector256.Zero) { - return BitOperations.TrailingZeroCount(empty); + return BitOperations.TrailingZeroCount(empty.ExtractMostSignificantBits()); } return -1; @@ -354,24 +355,22 @@ public static int Count(DotvvmPropertyId[] keys) Debug.Assert(keys.Length % 8 == 0); Debug.Assert(keys.Length >= AdhocTableSize); - #if Vectorize + int count = 0; ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); Debug.Assert(keys.Length % Vector256.Count == 0); if (Vector128.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); + var notZero = Vector256.GreaterThan(v, Vector256.Create(0)).ExtractMostSignificantBits(); + count += BitOperations.PopCount(notZero); } - return keys.Length - zeroCount; + return count; } #endif - int count = 0; for (int i = 0; i < keys.Length; i++) { count += BoolToInt(keys[i].Id == 0); diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 9d2deefc36..3aeda6ff2b 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -132,8 +132,14 @@ + + + + + + diff --git a/src/Framework/Framework/Runtime/Commands/EventValidator.cs b/src/Framework/Framework/Runtime/Commands/EventValidator.cs index 3a6954201e..83dd083d55 100644 --- a/src/Framework/Framework/Runtime/Commands/EventValidator.cs +++ b/src/Framework/Framework/Runtime/Commands/EventValidator.cs @@ -68,11 +68,14 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, if (resultBinding == null) { // find bindings of current control - var bindings = control.GetAllBindings() - .Where(b => b.Value is CommandBindingExpression); - foreach (var binding in bindings) + foreach (var (propertyId, rawValue) in control.properties) { + if (rawValue is not CommandBindingExpression binding) + continue; + var property = propertyId.PropertyInstance; + + infoMessage.Clear(); var bindingMatch = new FindBindingResult.BindingMatchChecklist(); @@ -88,7 +91,7 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, } //checking binding id - if (((CommandBindingExpression) binding.Value).BindingId == commandId) + if (binding.BindingId == commandId) { bindingMatch.BindingIdMatch = true; } @@ -124,23 +127,23 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, if(bindingMatch.AllMatches) { //correct binding found - resultBinding = (CommandBindingExpression)binding.Value; + resultBinding = binding; resultControl = control; - resultProperty = binding.Key; + resultProperty = property; } else { // only add information about ID mismatch if no other mismatch was found to avoid information clutter if (!bindingMatch.BindingIdMatch && infoMessage.Length == 0) { - infoMessage.Append($"Expected internal binding id: '{commandId}' Command binding id: '{((CommandBindingExpression)binding.Value).BindingId}'"); + infoMessage.Append($"Expected internal binding id: '{commandId}' Command binding id: '{binding.BindingId}'"); } if (!candidateBindings.ContainsKey(bindingMatch)) { candidateBindings.Add(bindingMatch, new CandidateBindings()); } candidateBindings[bindingMatch] - .AddBinding(new(infoMessage.ToString(), binding.Value)); + .AddBinding(new(infoMessage.ToString(), binding)); } } } diff --git a/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs b/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs index 7ba698e2bf..9cecee8dad 100644 --- a/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs +++ b/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs @@ -55,7 +55,7 @@ private void CheckRenderedResources(IDotvvmRequestContext context) { var control = stack.Pop(); - if (control.properties.TryGet(PostBack.UpdateProperty, out var val) && true.Equals(val)) + if (control.properties.GetOrNull(DotvvmPropertyIdAssignment.PropertyIds.PostBack_Update) is true) { using (var w = new StringWriter()) { diff --git a/src/Framework/Framework/scripts/generate-property-ids.mjs b/src/Framework/Framework/scripts/generate-property-ids.mjs new file mode 100644 index 0000000000..00f550868c --- /dev/null +++ b/src/Framework/Framework/scripts/generate-property-ids.mjs @@ -0,0 +1,826 @@ +#!/usr/bin/env node + +/** + * Generates the DotvvmPropertyIdAssignment files: + * - GroupMembers.cs - for property group member names + * - TypeIds.cs - for control type IDs + * - PropertyIds.cs - for individual property IDs + * - PropertyGroupIds.cs - for property group IDs + */ + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const predefinedNames = [ + // HTML attributes + 'accept', + 'accesskey', + 'action', + 'align', + 'allow', + 'alt', + 'aria-checked', + 'aria-controls', + 'aria-describedby', + 'aria-expanded', + 'aria-hidden', + 'aria-label', + 'aria-selected', + 'as', + 'async', + 'autocomplete', + 'autofocus', + 'border', + 'charset', + 'checked', + 'class', + 'cols', + 'colspan', + 'content', + 'contenteditable', + 'crossorigin', + 'data-bind', + 'data-dismiss', + 'data-dotvvm-id', + 'data-placement', + 'data-target', + 'data-toggle', + 'data-ui', + 'data-uitest-name', + 'dir', + 'disabled', + 'download', + 'draggable', + 'enctype', + 'for', + 'form', + 'formaction', + 'formmethod', + 'formnovalidate', + 'formtarget', + 'height', + 'hidden', + 'href', + 'hreflang', + 'http-equiv', + 'id', + 'integrity', + 'itemprop', + 'lang', + 'list', + 'loading', + 'max', + 'maxlength', + 'media', + 'method', + 'min', + 'minlength', + 'multiple', + 'name', + 'novalidate', + 'pattern', + 'ping', + 'placeholder', + 'preload', + 'readonly', + 'referrerpolicy', + 'rel', + 'required', + 'role', + 'rows', + 'sandbox', + 'scope', + 'selected', + 'size', + 'slot', + 'span', + 'spellcheck', + 'src', + 'step', + 'style', + 'tabindex', + 'target', + 'title', + 'translate', + 'type', + 'value', + 'width', + 'wrap', + + // Common CSS properties + 'background-color', + 'bottom', + 'color', + 'display', + 'font-size', + 'left', + 'line-height', + 'margin-bottom', + 'margin-right', + 'margin-top', + 'margin', + 'max-height', + 'max-width', + 'min-height', + 'min-width', + 'opacity', + 'padding-bottom', + 'padding-left', + 'padding-right', + 'padding-top', + 'padding', + 'position', + 'right', + 'top', + 'visibility', + 'z-index', + + // Common route parameter names + 'Id', + 'Name', + 'GroupId', + 'FileName', + 'UserId', + 'Slug', + 'slug', + 'Lang' +] + +// Control types with their properties +// The script will automatically assign sequential IDs +const controls = [ + { + name: 'DotvvmBindableObject', + id: 1, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['DataContext'] // inherited + }, + { + name: 'DotvvmControl', + id: 2, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ID', 'ClientID', 'IncludeInPage'], + slowProps: ['ClientIDMode'] // inherited + }, + { + name: 'HtmlGenericControl', + id: 3, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Visible', 'InnerText'], + slowProps: ['HtmlCapability'] // capability property + }, + { + name: 'RawLiteral', + id: 4, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: [] + }, + { + name: 'Literal', + id: 5, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Text', 'FormatString', 'RenderSpanElement'], + slowProps: [] + }, + { + name: 'ButtonBase', + id: 6, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Click', 'ClickArguments', 'Text'], + slowProps: ['Enabled', 'TextOrContentCapability'] // with fallback, capability + }, + { + name: 'Button', + id: 7, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ButtonTagName', 'IsSubmitButton'], + slowProps: [] + }, + { + name: 'LinkButton', + id: 8, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: [] + }, + { + name: 'TextBox', + id: 9, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Text', 'Changed', 'Type', 'TextInput', 'FormatString', 'SelectAllOnFocus'], + slowProps: ['Enabled', 'UpdateTextOnInput'] // inherited + }, + { + name: 'RouteLink', + id: 10, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['RouteName', 'Enabled', 'Text', 'UrlSuffix', 'Culture'], + slowProps: [] + }, + { + name: 'CheckableControlBase', + id: 11, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Text', 'CheckedValue', 'Changed', 'LabelCssClass', 'InputCssClass', 'ItemKeyBinding'], + slowProps: [] + }, + { + name: 'CheckBox', + id: 12, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Checked', 'CheckedItems', 'DisableIndeterminate'], + slowProps: [] + }, + { + name: 'RadioButton', + id: 13, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Checked', 'CheckedItem', 'GroupName'], + slowProps: [] + }, + { + name: 'Validator', + id: 14, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['HideWhenValid', 'InvalidCssClass', 'SetToolTipText', 'ShowErrorMessageText'] // inherited + }, + { + name: 'Validation', + id: 15, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['Enabled', 'Target'] // inherited + }, + { + name: 'ValidationSummary', + id: 16, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['IncludeErrorsFromChildren', 'HideWhenValid', 'IncludeErrorsFromTarget'], + slowProps: [] + }, + { + name: 'ItemsControl', + id: 17, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['DataSource'], + slowProps: [] + }, + { + name: 'Repeater', + id: 18, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['EmptyDataTemplate', 'ItemTemplate', 'RenderWrapperTag', 'SeparatorTemplate', 'WrapperTagName', 'RenderAsNamedTemplate'], + slowProps: [] + }, + { + name: 'HierarchyRepeater', + id: 19, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ItemChildrenBinding', 'ItemTemplate', 'EmptyDataTemplate', 'RenderWrapperTag', 'WrapperTagName'], + slowProps: [] + }, + { + name: 'GridView', + id: 20, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['FilterPlacement', 'EmptyDataTemplate', 'Columns', 'RowDecorators', 'HeaderRowDecorators', 'EditRowDecorators', 'SortChanged', 'ShowHeaderWhenNoData', 'InlineEditing', 'LoadData'], + slowProps: [] + }, + { + name: 'GridViewColumn', + id: 21, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['HeaderText', 'HeaderTemplate', 'FilterTemplate', 'SortExpression', 'SortAscendingHeaderCssClass', 'SortDescendingHeaderCssClass', 'AllowSorting', 'CssClass', 'IsEditable', 'HeaderCssClass', 'Width', 'Visible', 'CellDecorators', 'EditCellDecorators', 'EditTemplate', 'HeaderCellDecorators'], + slowProps: [] + }, + { + name: 'GridViewTextColumn', + id: 22, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['FormatString', 'ChangedBinding', 'ValueBinding', 'ValidatorPlacement'], + slowProps: [] + }, + { + name: 'GridViewCheckBoxColumn', + id: 23, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ValueBinding', 'ValidatorPlacement'], + slowProps: [] + }, + { + name: 'GridViewTemplateColumn', + id: 24, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ContentTemplate'], + slowProps: [] + }, + { + name: 'DataPager', + id: 25, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['DataSet', 'FirstPageTemplate', 'LastPageTemplate', 'PreviousPageTemplate', 'NextPageTemplate', 'RenderLinkForCurrentPage', 'HideWhenOnlyOnePage', 'LoadData'], + slowProps: [] + }, + { + name: 'AppendableDataPager', + id: 26, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['LoadTemplate', 'LoadingTemplate', 'EndTemplate', 'DataSet', 'LoadData'], + slowProps: [] + }, + { + name: 'SelectorBase', + id: 27, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ItemTextBinding', 'ItemValueBinding', 'SelectionChanged', 'ItemTitleBinding'], + slowProps: [] + }, + { + name: 'Selector', + id: 28, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['SelectedValue'], + slowProps: [] + }, + { + name: 'MultiSelector', + id: 29, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['SelectedValues'], + slowProps: [] + }, + { + name: 'ListBox', + id: 30, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Size'], + slowProps: [] + }, + { + name: 'ComboBox', + id: 31, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['EmptyItemText'], + slowProps: [] + }, + { + name: 'SelectorItem', + id: 32, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Text', 'Value'], + slowProps: [] + }, + { + name: 'FileUpload', + id: 33, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['UploadedFiles', 'Capture', 'MaxFileSize', 'UploadCompleted'], + slowProps: [] + }, + { + name: 'Timer', + id: 34, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Command', 'Interval', 'Enabled'], + slowProps: [] + }, + { + name: 'UpdateProgress', + id: 35, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Delay', 'IncludedQueues', 'ExcludedQueues'], + slowProps: [] + }, + { + name: 'Label', + id: 36, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['For'], + slowProps: [] + }, + { + name: 'EmptyData', + id: 37, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['WrapperTagName', 'RenderWrapperTag'], + slowProps: [] + }, + { + name: 'Content', + id: 38, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['ContentPlaceHolderID'], + slowProps: [] + }, + { + name: 'TemplateHost', + id: 39, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Template'], + slowProps: [] + }, + { + name: 'AddTemplateDecorator', + id: 40, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['AfterTemplate', 'BeforeTemplate'], + slowProps: [] + }, + { + name: 'SpaContentPlaceHolder', + id: 41, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['DefaultRouteName', 'PrefixRouteName', 'UseHistoryApi'], + slowProps: [] + }, + { + name: 'ModalDialog', + id: 42, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Open', 'CloseOnBackdropClick', 'Close'], + slowProps: [] + }, + { + name: 'HtmlLiteral', + id: 43, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Html'], + slowProps: [] + }, + { + name: 'RequiredResource', + id: 44, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Name'], + slowProps: [] + }, + { + name: 'InlineScript', + id: 45, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Dependencies', 'Script'], + slowProps: [] + }, + { + name: 'RoleView', + id: 46, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Roles', 'IsMemberTemplate', 'IsNotMemberTemplate', 'HideForAnonymousUsers'], + slowProps: [] + }, + { + name: 'ClaimView', + id: 47, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Claim', 'Values', 'HasClaimTemplate', 'HideForAnonymousUsers'], + slowProps: [] + }, + { + name: 'EnvironmentView', + id: 48, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Environments', 'IsEnvironmentTemplate', 'IsNotEnvironmentTemplate'], + slowProps: [] + }, + { + name: 'JsComponent', + id: 49, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Global', 'Name', 'WrapperTagName'], + slowProps: [] + }, + { + name: 'PostBackHandler', + id: 50, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['EventName', 'Enabled'], + slowProps: [] + }, + { + name: 'SuppressPostBackHandler', + id: 51, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Suppress'], + slowProps: [] + }, + { + name: 'ConcurrencyQueueSetting', + id: 52, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['EventName', 'ConcurrencyQueue'], + slowProps: [] + }, + { + name: 'NamedCommand', + id: 53, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Name', 'Command'], + slowProps: [] + }, + { + name: 'PostBack', + id: 54, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['Update', 'Handlers', 'ConcurrencyQueueSettings'], + slowProps: ['Concurrency', 'ConcurrencyQueue'] // inherited + }, + { + name: 'FormControls', + id: 55, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['Enabled'] // inherited + }, + { + name: 'UITests', + id: 56, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['GenerateStub'] // inherited + }, + { + name: 'Events', + id: 57, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], // Uses ActiveDotvvmProperty.RegisterCommandToAttribute + slowProps: [] + }, + { + name: 'Styles', + id: 58, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], // Uses CompileTimeOnlyDotvvmProperty + slowProps: [] + }, + { + name: 'Internal', + id: 59, + namespace: 'DotVVM.Framework.Controls', + fastProps: ['UniqueID', 'IsNamingContainer', 'IsControlBindingTarget', 'PathFragment', 'IsServerOnlyDataContext', 'MarkupLineNumber', 'ClientIDFragment', 'IsMasterPageCompositionFinished', 'CurrentIndexBinding', 'ReferencedViewModuleInfo', 'UsedPropertiesInfo'], + slowProps: ['IsSpaPage', 'UseHistoryApiSpaNavigation', 'DataContextType', 'MarkupFileName', 'RequestContext'] // inherited + }, + { + name: 'RenderSettings', + id: 60, + namespace: 'DotVVM.Framework.Controls', + fastProps: [], + slowProps: ['Mode'] // inherited + }, + { + name: 'DotvvmView', + id: 61, + namespace: 'DotVVM.Framework.Controls.Infrastructure', + fastProps: [], + slowProps: ['Directives'] // inherited + } +] + +const propertyGroups = [ + { type: 'HtmlGenericControl', name: 'Attributes' }, + { type: 'HtmlGenericControl', name: 'CssClasses' }, + { type: 'HtmlGenericControl', name: 'CssStyles' }, + { type: 'RouteLink', name: 'Params' }, + { type: 'RouteLink', name: 'QueryParameters' }, + { type: 'JsComponent', name: 'Props' }, + { type: 'JsComponent', name: 'Templates' } +] + +// ------------------------------------------------------------------------------------------------ + +const csharpKeywords = new Set([ + 'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch', 'char', 'checked', + 'class', 'const', 'continue', 'decimal', 'default', 'delegate', 'do', 'double', 'else', + 'enum', 'event', 'explicit', 'extern', 'false', 'finally', 'fixed', 'float', 'for', + 'foreach', 'goto', 'if', 'implicit', 'in', 'int', 'interface', 'internal', 'is', + 'lock', 'long', 'namespace', 'new', 'null', 'object', 'operator', 'out', 'override', + 'params', 'private', 'protected', 'public', 'readonly', 'ref', 'return', 'sbyte', + 'sealed', 'short', 'sizeof', 'stackalloc', 'static', 'string', 'struct', 'switch', + 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong', 'unchecked', 'unsafe', + 'ushort', 'using', 'virtual', 'void', 'volatile', 'while' +]) + +function csIdentifier(name) { + let identifier = name.replace(/-/g, '_') + return csharpKeywords.has(identifier) ? '@' + identifier : identifier +} + +function validateAndGeneratePropertyDefinitions() { + const propertyDefinitions = [] + const errors = [] + + for (const control of controls) { + if (control.fastProps.length > 16) { + errors.push(`Control ${control.name} has ${control.fastProps.length} fast properties, maximum is 16`) + } + if (control.slowProps.length > 16) { + errors.push(`Control ${control.name} has ${control.slowProps.length} slow properties, maximum is 16`) + } + + // properties with CanUseFastAccessors=true get even IDs (last bit zero) + control.fastProps.forEach((propName, index) => { + propertyDefinitions.push({ + typeName: control.name, + propertyName: propName, + sequentialId: (index + 1) * 2, + canUseFastAccessors: true + }) + }) + + // properties with CanUseFastAccessors=false get odd IDs (last bit one) + control.slowProps.forEach((propName, index) => { + propertyDefinitions.push({ + typeName: control.name, + propertyName: propName, + sequentialId: (index * 2) + 1, + canUseFastAccessors: false + }) + }) + } + + if (errors.length > 0) { + console.error('❌ Configuration validation failed:') + errors.forEach(error => console.error(` ${error}`)) + throw new Error('Configuration validation failed') + } + + return propertyDefinitions +} + +function generateControlTypes() { + return controls.map(control => ({ + name: control.name, + id: control.id, + namespace: control.namespace + })) +} + +function generateGroupMembersClass() { + const set = new Set() + const names = predefinedNames.filter(n => !set.has(n) && set.add(n)) + + const constants = names.map((name, index) => { + const id = index + 1 // Start from 1 + return ` public const ushort ${csIdentifier(name)} = ${id};` + }).join('\n') + + const listItems = names.map(name => { + return ` ("${name}", ${csIdentifier(name)})` + }).join(',\n') + + const switchCases = names.map(name => { + const identifier = csIdentifier(name) + return ` "${name}" => ${identifier}` + }).join(',\n') + + return `// Generated by scripts/generate-property-ids.mjs +using System; +using System.Collections.Immutable; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class GroupMembers + { +${constants} + + public static readonly ImmutableArray<(string Name, ushort ID)> List = ImmutableArray.Create( +${listItems} + ); + + public static ushort TryGetId(ReadOnlySpan attr) => + attr switch { +${switchCases}, + _ => 0, + }; + } +} +` +} + +function generateTypeIdsClass() { + const controlTypes = generateControlTypes() + + const constants = controlTypes.map(type => { + return `public const ushort ${type.name} = ${type.id};` + }).join('\n ') + + const listItems = controlTypes.map(type => { + return `(typeof(${type.name}), ${type.name})` + }).join(',\n ') + + const usingStatements = [...new Set(controlTypes.map(t => t.namespace))].map(ns => + `using ${ns};` + ).join('\n') + + return `// Generated by scripts/generate-property-ids.mjs +using System; +using System.Collections.Immutable; +${usingStatements} + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class TypeIds + { + ${constants} + + public static readonly ImmutableArray<(Type type, ushort id)> List = ImmutableArray.Create( + ${listItems} + ); + } +} +` +} + +function generatePropertyIdsClass() { + const propertyDefinitions = validateAndGeneratePropertyDefinitions() + const controlTypes = generateControlTypes() + + const constants = propertyDefinitions.map(prop => { + const typeInfo = controlTypes.find(t => t.name === prop.typeName) + if (!typeInfo) { + throw new Error(`Type ${prop.typeName} not found in control types`) + } + + return `/// + public const uint ${prop.typeName}_${prop.propertyName} = TypeIds.${prop.typeName} << 16 | ${prop.sequentialId};` + }).join('\n\n ') + + return `// Generated by scripts/generate-property-ids.mjs +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class PropertyIds + { + ${constants} + } +} +` +} + +function generatePropertyGroupIdsClass() { + const constants = propertyGroups.map((group, index) => { + const id = index + 1; + const constantName = `${group.type}_${group.name}`; + return `/// + public const ushort ${constantName} = ${id};` + }).join('\n\n ') + + return `// Generated by scripts/generate-property-ids.mjs +using DotVVM.Framework.Controls; + +namespace DotVVM.Framework.Binding; + +static partial class DotvvmPropertyIdAssignment +{ + public static class PropertyGroupIds + { + ${constants} + } +} +` +} + + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +function main() { + const baseDir = path.join(__dirname, '..') + + const files = [ + { name: 'GroupMembers', generator: generateGroupMembersClass }, + { name: 'TypeIds', generator: generateTypeIdsClass }, + { name: 'PropertyIds', generator: generatePropertyIdsClass }, + { name: 'PropertyGroupIds', generator: generatePropertyGroupIdsClass } + ] + + let hasErrors = false + + for (const file of files) { + try { + const filepath = path.join(baseDir, 'Binding', `DotvvmPropertyIdAssignment.${file.name}.cs`) + const content = file.generator().replace('\r\n', '\n') + fs.writeFileSync(filepath, content, 'utf8') + } catch (error) { + console.error(`❌ Error generating ${file.name}:`, error.message) + hasErrors = true + } + } + + if (hasErrors) { + process.exit(1) + } +} + +main() diff --git a/src/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index 086c892a62..b30e7797f9 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -304,12 +304,22 @@ public void DotvvmProperty_CorrectGetAndSet() if (p.PropertyInfo.PropertyType != p.PropertyType) Console.WriteLine(p); + instance.properties.Remove(p.Id); + Assert.AreEqual(p.DefaultValue, instance.GetValue(p), $"GetValue default value {p}"); + Assert.AreEqual(p.DefaultValue, instance.GetValueRaw(p.Id), $"GetValue(id) default value {p}"); + Assert.AreEqual(p.DefaultValue, p.PropertyInfo.GetValue(instance), $"Getter default value {p}"); + foreach (var example in GetExampleValues(p.PropertyType)) { instance.SetValue(p, example); Assert.AreEqual(example, instance.GetValue(p), $"GetValue is behaving weird {p}"); Assert.AreEqual(example, instance.GetValueRaw(p.Id), $"GetValue(id) is behaving weird {p}"); Assert.AreEqual(example, p.PropertyInfo.GetValue(instance), $"Getter is broken in {p}"); + + if (p.Id.CanUseFastAccessors) + { + Assert.AreEqual(example, instance.properties.GetOrThrow(p.Id), "$instance.properties.GetOrThrow {p}"); + } } if (p.PropertyInfo.SetMethod == null) @@ -544,5 +554,125 @@ public void DotvvmProperty_ParallelAccess_DoesntCrashProcess() Assert.Fail($"There were {exceptions.Count} exceptions thrown during the test. See the output for details."); } } + + [DataTestMethod] + [DataRow(0, 0, 0)] + [DataRow(1, 1, 1)] + [DataRow(30, 1, 1)] + [DataRow(1, 30, 1)] + [DataRow(1, 1, 30)] + [DataRow(1, 1, 8)] + [DataRow(1, 8, 1)] + [DataRow(8, 1, 1)] + [DataRow(5, 5, 8)] + [DataRow(5, 8, 5)] + [DataRow(8, 5, 5)] + [DataRow(5, 5, 5)] + [DataRow(8, 8, 8)] + public void PropertyGroup_Clear(int attributeCount, int classCount, int styleCount) + { + var control = new HtmlGenericControl(); + control.InnerText = "test-inner-text"; + var attributes = control.Attributes; + + (int, int, int) counts() => (control.Attributes.Count, control.CssClasses.Count, control.CssStyles.Count); + Assert.AreEqual(0, control.Attributes.Count); + Assert.AreEqual(0, control.CssClasses.Count); + Assert.AreEqual(0, control.CssStyles.Count); + + for (int i = 0; i < attributeCount; i++) + attributes.Set($"test-attribute-{i}", $"value{i}"); + + Assert.AreEqual((attributeCount, 0, 0), counts()); + + for (int i = 0; i < classCount; i++) + control.CssClasses.Add($"test-class-{i}", true); + + Assert.AreEqual((attributeCount, classCount, 0), counts()); + + + for (int i = 0; i < styleCount; i++) + control.CssStyles.Add($"test-style-{i}", $"value{i}"); + + Assert.AreEqual((attributeCount, classCount, styleCount), counts()); + + control.CssClasses.Clear(); + var checkpoint1 = control.CloneControl(); + Assert.AreEqual((attributeCount, 0, styleCount), counts()); + control.CssClasses.Add("another-class", true); + Assert.AreEqual((attributeCount, 1, styleCount), counts()); + + for (int i = 0; i < attributeCount; i++) + Assert.AreEqual(attributes[$"test-attribute-{i}"], $"value{i}"); + + for (int i = 0; i < classCount; i++) + Assert.IsFalse(control.CssClasses.ContainsKey($"test-class-{i}")); + + for (int i = 0; i < styleCount; i++) + Assert.AreEqual($"value{i}", control.CssStyles[$"test-style-{i}"]); + + control.Attributes.Clear(); + + Assert.AreEqual((0, 1, styleCount), counts()); + var checkpoint2 = control.CloneControl(); + + for (int i = 0; i < attributeCount; i++) + Assert.IsFalse(attributes.ContainsKey($"test-attribute-{i}")); + for (int i = 0; i < classCount; i++) + Assert.IsFalse(control.CssClasses.ContainsKey($"test-class-{i}")); + for (int i = 0; i < styleCount; i++) + Assert.AreEqual($"value{i}", control.CssStyles[$"test-style-{i}"]); + + Assert.IsTrue(control.CssClasses["another-class"]); + + control.CssStyles.Clear(); + + for (int i = 0; i < styleCount; i++) + Assert.IsFalse(control.CssStyles.ContainsKey($"test-style-{i}"), $"Style 'test-style-{i}' should not exist."); + + Assert.IsTrue(control.CssClasses["another-class"]); + + Assert.AreEqual("test-inner-text", control.InnerText); + + control = (HtmlGenericControl)checkpoint1; + Assert.AreEqual((attributeCount, 0, styleCount), counts()); + control = (HtmlGenericControl)checkpoint2; + Assert.AreEqual((0, 1, styleCount), counts()); + } + + [TestMethod] + public void PropertyIds_MatchGeneratedConstants() + { + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_ClientID, (uint)DotvvmControl.ClientIDProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.DotvvmControl_IncludeInPage, (uint)DotvvmControl.IncludeInPageProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_Visible, (uint)HtmlGenericControl.VisibleProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.HtmlGenericControl_InnerText, (uint)HtmlGenericControl.InnerTextProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.Literal_Text, (uint)Literal.TextProperty.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyIds.ButtonBase_Click, (uint)Button.ClickProperty.Id); + } + + [TestMethod] + public void PropertyGroupIds_MatchGeneratedConstants() + { + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_Attributes, HtmlGenericControl.AttributesGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssClasses, HtmlGenericControl.CssClassesGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.HtmlGenericControl_CssStyles, HtmlGenericControl.CssStylesGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.RouteLink_Params, RouteLink.ParamsGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.RouteLink_QueryParameters, RouteLink.QueryParametersGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.JsComponent_Props, JsComponent.PropsGroupDescriptor.Id); + Assert.AreEqual(DotvvmPropertyIdAssignment.PropertyGroupIds.JsComponent_Templates, JsComponent.TemplatesGroupDescriptor.Id); + } + + [TestMethod] + public void PropertyIds_AreUnique() + { + XAssert.Distinct(DotvvmProperty.AllProperties.Select(p => p.Id)); + } + + [TestMethod] + public void PropertyGroupIds_AreUnique() + { + XAssert.Distinct(DotvvmPropertyGroup.AllGroups.Select(g => g.Id)); + } } } From 60809c85b7d85a00e53dd16ba68c233b6a38e614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 15 Aug 2025 23:37:47 +0200 Subject: [PATCH 09/11] Optimize property group Clear --- .../Controls/DotvvmControlProperties.cs | 135 +++++++++--------- .../Controls/PropertyImmutableHashtable.cs | 2 + src/Framework/Framework/Utils/BoxingUtils.cs | 1 + 3 files changed, 74 insertions(+), 64 deletions(-) diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index 8c0c44aac8..c9af991eb7 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -312,75 +312,77 @@ private readonly int CountPropertyGroupOutlined(ushort groupId) public void ClearPropertyGroup(ushort groupId) { - switch (state) + if (state == TableState.Empty) + return; + else if (state == TableState.Dictinary) { - case TableState.Empty: - return; - case TableState.Array8: - case TableState.Array16: - { - var bitmap = Impl.FindGroupBitmap(this.keys!, groupId); - int index = 0; - if (bitmap == 0) - return; - OwnKeys(); - OwnValues(); - do - { - int bit = BitOperations.TrailingZeroCount(bitmap); - bitmap >>= (bit + 1); - index += bit; - Debug.Assert(keys![index].IsInPropertyGroup(groupId), $"Key {keys[index]} at index {index} is not in property group {groupId}"); - keys![index] = default; - valuesAsArray[index] = null; - index++; - } while (bitmap != 0); - CheckInvariant(); - return; - } - case TableState.Dictinary: - { - var dict = this.valuesAsDictionary; - // we want to avoid allocating the list if there is only one property - DotvvmPropertyId toRemove = default; - List? toRemoveRest = null; + ClearPropertyGroupOutlined(groupId); + return; + } + + Debug.Assert(state is TableState.Array16 or TableState.Array8); + var bitmap = Impl.FindGroupBitmap(this.keys!, groupId); + if (bitmap == 0) + return; - foreach (var (p, _) in dict) - { - if (p.IsInPropertyGroup(groupId)) - { - if (toRemove.Id == 0) - toRemove = p; - else - { - toRemoveRest ??= new List(); - toRemoveRest.Add(p); - } - } - } + OwnKeys(); + OwnValues(); - if (toRemove.Id != 0) - { - if (!ownsValues) - { - CloneValues(); - dict = this.valuesAsDictionary; - } + int index = 0; + var values = Impl.UnsafeArrayReference(this.valuesAsArray); + var keys = Impl.UnsafeArrayReference(this.keys!); + do + { + int bit = BitOperations.TrailingZeroCount(bitmap); + bitmap >>= (bit + 1); + index += bit; + Debug.Assert(this.keys![index].IsInPropertyGroup(groupId), $"Key {this.keys[index]} at index {index} is not in property group {groupId}"); + Unsafe.Add(ref keys, index) = default; + Unsafe.Add(ref values, index) = null; + index++; + } while (bitmap != 0); + CheckInvariant(); + } - dict.Remove(toRemove); - } + private void ClearPropertyGroupOutlined(ushort groupId) + { + var dict = this.valuesAsDictionary; + // we want to avoid allocating the list if there is only one property + DotvvmPropertyId toRemove = default; + List? toRemoveRest = null; - if (toRemoveRest is {}) + foreach (var (p, _) in dict) + { + if (p.IsInPropertyGroup(groupId)) + { + if (toRemove.Id == 0) + toRemove = p; + else { - Debug.Assert(ownsValues); - foreach (var p in toRemoveRest) - dict.Remove(p); + toRemoveRest ??= new List(); + toRemoveRest.Add(p); } - CheckInvariant(); - return; } } - Impl.Fail(); + + if (toRemove.Id != 0) + { + if (!ownsValues) + { + CloneValues(); + dict = this.valuesAsDictionary; + } + + dict.Remove(toRemove); + } + + if (toRemoveRest is {}) + { + Debug.Assert(ownsValues); + foreach (var p in toRemoveRest) + dict.Remove(p); + } + CheckInvariant(); } [MethodImpl(Inline)] @@ -396,9 +398,9 @@ public readonly bool TryGet(DotvvmPropertyId p, out object? value) if (index >= 0) { #if NET6_0_OR_GREATER - value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArray), index); + value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArrayUnsafe), index); #else - value = valuesAsArray[index]; + value = valuesAsArrayUnsafe![index]; #endif return true; } @@ -422,7 +424,12 @@ private readonly bool TryGetOutlined(DotvvmPropertyId p, out object? value) var index = Impl.FindSlot16(this.keys!, p); if (index >= 0) { - value = valuesAsArray[index]; + +#if NET6_0_OR_GREATER + value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArrayUnsafe), index); +#else + value = valuesAsArrayUnsafe![index]; +#endif return true; } else @@ -463,7 +470,7 @@ private readonly bool TryGetOutlined(DotvvmPropertyId p, out object? value) public void Set(DotvvmPropertyId p, object? value) { CheckInvariant(); - if (p.MemberId == 0) ThrowZeroPropertyId(); + if (p.MemberId == 0) { ThrowZeroPropertyId(); return; } if (state == TableState.Array8) { diff --git a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs index bd3bb5f9ad..2c6417fbc0 100644 --- a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs +++ b/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs @@ -277,6 +277,7 @@ public static int FindSlotOrFree8(DotvvmPropertyId[] keys, DotvvmPropertyId p, o public static int FindSlotOrFree16(DotvvmPropertyId[] keys, DotvvmPropertyId p, out bool exists) => FindSlotOrFree16(ref UnsafeArrayReference(keys), p, out exists); + [MethodImpl(Inline)] public static ushort FindGroupBitmap(ref DotvvmPropertyId keys, int length, ushort groupId) { Debug.Assert(length is AdhocTableSize or AdhocLargeTableSize); @@ -312,6 +313,7 @@ public static ushort FindGroupBitmap(ref DotvvmPropertyId keys, int length, usho return bitmap; } + [MethodImpl(Inline)] public static ushort FindGroupBitmap(DotvvmPropertyId[] keys, ushort groupId) { return FindGroupBitmap(ref UnsafeArrayReference(keys), keys.Length, groupId); diff --git a/src/Framework/Framework/Utils/BoxingUtils.cs b/src/Framework/Framework/Utils/BoxingUtils.cs index 567fa582d6..50965808c0 100644 --- a/src/Framework/Framework/Utils/BoxingUtils.cs +++ b/src/Framework/Framework/Utils/BoxingUtils.cs @@ -26,6 +26,7 @@ public static object Box(int v) } public static object? Box(int? v) => v.HasValue ? Box(v.GetValueOrDefault()) : null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static object? BoxGeneric(T v) { if (typeof(T).IsValueType) From 065a2199850e656a6aa9092b95733bd1f97ea5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 18 Aug 2025 21:18:16 +0200 Subject: [PATCH 10/11] Fix compilation on netstandard --- ...DotvvmCapabilityProperty.CodeGeneration.cs | 6 +- .../Binding/DotvvmPropertyIdAssignment.cs | 5 +- .../ControlTree/DefaultControlResolver.cs | 6 ++ .../Controls/DotvvmControlProperties.cs | 65 ++++++++++++------- ...Hashtable.cs => PropertyDictionaryImpl.cs} | 14 +++- .../Runtime/Commands/EventValidator.cs | 1 + .../Framework/Utils/FunctionalExtensions.cs | 2 + src/Tests/Runtime/CapabilityPropertyTests.cs | 3 - src/Tests/Runtime/DotvvmPropertyTests.cs | 4 +- 9 files changed, 71 insertions(+), 35 deletions(-) rename src/Framework/Framework/Controls/{PropertyImmutableHashtable.cs => PropertyDictionaryImpl.cs} (98%) diff --git a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs index a1b7de7916..3c8fad160d 100644 --- a/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs +++ b/src/Framework/Framework/Binding/DotvvmCapabilityProperty.CodeGeneration.cs @@ -76,14 +76,16 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyG var valueParameter = Expression.Parameter(type, "value"); var ctor = typeof(VirtualPropertyGroupDictionary<>) .MakeGenericType(propType) - .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, [ typeof(DotvvmBindableObject), typeof(ushort), typeof(bool) ])!; + .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [ typeof(DotvvmBindableObject), typeof(ushort), typeof(bool) ], null)!; var createMethod = typeof(VirtualPropertyGroupDictionary<>) .MakeGenericType(propType) .GetMethod( typeof(ValueOrBinding).IsAssignableFrom(elementType) ? nameof(VirtualPropertyGroupDictionary.CreatePropertyDictionary) : nameof(VirtualPropertyGroupDictionary.CreateValueDictionary), BindingFlags.NonPublic | BindingFlags.Static, - [ typeof(DotvvmBindableObject), typeof(ushort) ] + binder: null, + [ typeof(DotvvmBindableObject), typeof(ushort) ], + modifiers: null )!; var enumerableType = typeof(IEnumerable<>).MakeGenericType(typeof(KeyValuePair<,>).MakeGenericType(typeof(string), elementType)); var copyFromMethod = diff --git a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs index 2e7d87f82f..b4bd6033cc 100644 --- a/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs +++ b/src/Framework/Framework/Binding/DotvvmPropertyIdAssignment.cs @@ -217,9 +217,8 @@ public static ushort RegisterType(Type type) [MethodImpl(MethodImplOptions.NoInlining)] static ushort unlikely(Type type) { - var types = MemoryMarshal.CreateReadOnlySpan(ref type, 1); Span ids = stackalloc ushort[1]; - RegisterTypes(types, ids); + RegisterTypes([type], ids); return ids[0]; } } @@ -420,7 +419,7 @@ private static void VolatileResize(ref T[] array, int newSize) #region Group members public static ushort GetGroupMemberId(string name, bool registerIfNotFound) { - var id = GroupMembers.TryGetId(name); + var id = GroupMembers.TryGetId(name.AsSpan()); if (id != 0) return id; if (propertyGroupMemberIds.TryGetValue(name, out id)) diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs index 7b3fdf11e6..32626c8817 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs @@ -108,8 +108,14 @@ private List OrderAndFilterAssemblies(IEnumerable assemblies { if (name.Name is null) namelessAssemblies.Add(a); +#if DotNetCore else if (renumbering.TryAdd(name.Name, assemblyList.Count)) { +#else + else if (!renumbering.ContainsKey(name.Name)) + { + renumbering.Add(name.Name, assemblyList.Count); +#endif assemblyList.Add(a); references.Add(r); } diff --git a/src/Framework/Framework/Controls/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index c9af991eb7..b91da89647 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -20,7 +20,7 @@ internal struct DotvvmControlProperties : IEnumerable keys == values == null - // 2. Dictinary -> keys == null & values is Dictionary --> it falls back to traditional mutable property dictionary + // 2. Dictionary -> keys == null & values is Dictionary --> it falls back to traditional mutable property dictionary // 3. Array8 or Array16 -> keys is DotvvmPropertyId[] & values is object[] -- small linear search array // Note about unsafe code: @@ -118,7 +118,7 @@ private readonly void CheckInvariant() } } break; - case TableState.Dictinary: + case TableState.Dictionary: Debug.Assert(keys is null); Debug.Assert(values is Dictionary); break; @@ -160,8 +160,8 @@ public void AssignBulk(DotvvmPropertyId[] keys, object?[] values, bool ownsKeys, [DoesNotReturn, MethodImpl(NoInlining)] void throwArgumentError(DotvvmPropertyId[]? keys, object?[]? values) { - ThrowHelpers.ArgumentNull(nameof(keys)); - ThrowHelpers.ArgumentNull(nameof(values)); + ThrowHelpers.ArgumentNull(keys); + ThrowHelpers.ArgumentNull(values); if (keys.Length is not 8 and not 16) throw new ArgumentException($"The length of keys array must be 8 or 16.", nameof(keys)); if (keys.Length != values.Length) @@ -183,15 +183,20 @@ public void AssignBulk(Dictionary values, bool owns) this.keys = null; this.valuesAsDictionary = values; this.ownsValues = owns; - this.state = TableState.Dictinary; + this.state = TableState.Dictionary; } else { - if (owns) + if (owns && values.Count >= 8) { foreach (var (k, v) in this) { +#if DotNetCore values.TryAdd(k, v); +#else + if (!values.ContainsKey(k)) + values[k] = v; +#endif } this.values = values; this.keys = null; @@ -236,7 +241,7 @@ private readonly bool ContainsOutlined(DotvvmPropertyId p) // doesn't need to be { return Impl.ContainsKey16(this.keys!, p); } - if (state == TableState.Dictinary) + if (state == TableState.Dictionary) { return valuesAsDictionary!.ContainsKey(p); } @@ -295,7 +300,7 @@ private readonly int CountPropertyGroupOutlined(ushort groupId) return 0; case TableState.Array16: return Impl.CountPropertyGroup(this.keys!, groupId); - case TableState.Dictinary: + case TableState.Dictionary: { int count = 0; foreach (var key in valuesAsDictionary.Keys) @@ -314,23 +319,25 @@ public void ClearPropertyGroup(ushort groupId) { if (state == TableState.Empty) return; - else if (state == TableState.Dictinary) + else if (state == TableState.Dictionary) { ClearPropertyGroupOutlined(groupId); return; } - + Debug.Assert(state is TableState.Array16 or TableState.Array8); +#if Vectorize var bitmap = Impl.FindGroupBitmap(this.keys!, groupId); if (bitmap == 0) return; OwnKeys(); OwnValues(); + Impl.Assert(this.keys!.Length == this.valuesAsArray.Length); int index = 0; - var values = Impl.UnsafeArrayReference(this.valuesAsArray); - var keys = Impl.UnsafeArrayReference(this.keys!); + ref var values = ref Impl.UnsafeArrayReference(this.valuesAsArray); + ref var keys = ref Impl.UnsafeArrayReference(this.keys); do { int bit = BitOperations.TrailingZeroCount(bitmap); @@ -341,6 +348,21 @@ public void ClearPropertyGroup(ushort groupId) Unsafe.Add(ref values, index) = null; index++; } while (bitmap != 0); +#else + var keys = this.keys!; + for (var i = 0; i < keys.Length; i++) + { + if (keys[i].IsInPropertyGroup(groupId)) + { + if (!ownsKeys) + keys = CloneKeys(); + if (!ownsValues) + CloneValues(); + keys[i] = default; + this.valuesAsArray[i] = null; + } + } +#endif CheckInvariant(); } @@ -398,7 +420,7 @@ public readonly bool TryGet(DotvvmPropertyId p, out object? value) if (index >= 0) { #if NET6_0_OR_GREATER - value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArrayUnsafe), index); + value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArrayUnsafe!), index); #else value = valuesAsArrayUnsafe![index]; #endif @@ -426,7 +448,7 @@ private readonly bool TryGetOutlined(DotvvmPropertyId p, out object? value) { #if NET6_0_OR_GREATER - value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArrayUnsafe), index); + value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(valuesAsArrayUnsafe!), index); #else value = valuesAsArrayUnsafe![index]; #endif @@ -438,7 +460,7 @@ private readonly bool TryGetOutlined(DotvvmPropertyId p, out object? value) return false; } } - case TableState.Dictinary: + case TableState.Dictionary: return valuesAsDictionary.TryGetValue(p, out value); default: value = null; @@ -740,9 +762,6 @@ public readonly int Count() return Impl.Count(this.keys); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static byte BoolToInt(bool x) => Unsafe.As(ref x); - internal void CloneInto(ref DotvvmControlProperties newDict, DotvvmBindableObject newParent) { CheckInvariant(); @@ -886,7 +905,7 @@ void IncreaseSize() case TableState.Array16: SwitchToDictionary(); break; - case TableState.Dictinary: + case TableState.Dictionary: break; default: Impl.Fail(); @@ -912,17 +931,17 @@ void SwitchToDictionary() if (keysTmp[i].Id != 0) d[keysTmp[i]] = valuesTmp[i]; } - this.state = TableState.Dictinary; + this.state = TableState.Dictionary; this.valuesAsDictionary = d; this.keys = null; this.ownsValues = true; break; case TableState.Empty: - this.state = TableState.Dictinary; + this.state = TableState.Dictionary; this.valuesAsDictionary = new Dictionary(); this.ownsValues = true; break; - case TableState.Dictinary: + case TableState.Dictionary: break; default: Impl.Fail(); @@ -988,7 +1007,7 @@ public PropertyGroupEnumerable(in DotvvmControlProperties properties, ushort gro public enum TableState : byte { Empty = 0, - Dictinary = 1, + Dictionary = 1, Array8 = 2, Array16 = 3, } diff --git a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs b/src/Framework/Framework/Controls/PropertyDictionaryImpl.cs similarity index 98% rename from src/Framework/Framework/Controls/PropertyImmutableHashtable.cs rename to src/Framework/Framework/Controls/PropertyDictionaryImpl.cs index 2c6417fbc0..d3e787f1bb 100644 --- a/src/Framework/Framework/Controls/PropertyImmutableHashtable.cs +++ b/src/Framework/Framework/Controls/PropertyDictionaryImpl.cs @@ -357,8 +357,8 @@ public static int Count(DotvvmPropertyId[] keys) Debug.Assert(keys.Length % 8 == 0); Debug.Assert(keys.Length >= AdhocTableSize); -#if Vectorize int count = 0; +#if Vectorize ref var keysRef = ref MemoryMarshal.GetArrayDataReference(keys); Debug.Assert(keys.Length % Vector256.Count == 0); @@ -375,7 +375,7 @@ public static int Count(DotvvmPropertyId[] keys) #endif for (int i = 0; i < keys.Length; i++) { - count += BoolToInt(keys[i].Id == 0); + count += BoolToInt(keys[i].Id != 0); } return count; } @@ -481,6 +481,16 @@ public static void Assert([DoesNotReturnIf(false)] bool condition) Fail(); } +// #if !NETSTANDARD1_5_OR_GREATER +// [MethodImpl(Inline)] +// public static int TrailingZeroCount(uint v) +// { +// // https://graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightFloatCast +// float f = (float)(v & -v); // cast the least significant bit in v to a float +// return (int)(Unsafe.As(ref f) >> 23) - 0x7f; +// } +// #endif + [DoesNotReturn] [MethodImpl(NoInlining)] public static void Fail() => Fail(); diff --git a/src/Framework/Framework/Runtime/Commands/EventValidator.cs b/src/Framework/Framework/Runtime/Commands/EventValidator.cs index 83dd083d55..fdb234c854 100644 --- a/src/Framework/Framework/Runtime/Commands/EventValidator.cs +++ b/src/Framework/Framework/Runtime/Commands/EventValidator.cs @@ -6,6 +6,7 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Runtime.Commands { diff --git a/src/Framework/Framework/Utils/FunctionalExtensions.cs b/src/Framework/Framework/Utils/FunctionalExtensions.cs index d9cf6f1113..e3357f5f9a 100644 --- a/src/Framework/Framework/Utils/FunctionalExtensions.cs +++ b/src/Framework/Framework/Utils/FunctionalExtensions.cs @@ -82,11 +82,13 @@ public static IEnumerable SelectRecursively(this IEnumerable enumerable public static string StringJoin(this IEnumerable enumerable, string separator) => string.Join(separator, enumerable); +#if !DotNetCore public static void Deconstruct(this KeyValuePair pair, out K key, out V value) { key = pair.Key; value = pair.Value; } +#endif public static IEnumerable<(int, T)> Indexed(this IEnumerable enumerable) => enumerable.Select((a, b) => (b, a)); diff --git a/src/Tests/Runtime/CapabilityPropertyTests.cs b/src/Tests/Runtime/CapabilityPropertyTests.cs index 2e884a9c87..dadbc56d64 100644 --- a/src/Tests/Runtime/CapabilityPropertyTests.cs +++ b/src/Tests/Runtime/CapabilityPropertyTests.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Controls; -using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Testing; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index b30e7797f9..8e8ff9dacf 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -10,14 +10,13 @@ using DotVVM.Framework.Controls; using DotVVM.Framework.Testing; using DotVVM.Framework.Utils; -using Microsoft.AspNetCore.Components.Web.Virtualization; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using System.Net.Sockets; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -476,6 +475,7 @@ public void DotvvmProperty_VirtualDictionary_Append(int testClone) } [TestMethod, Ignore] + [Conditional("NET6_0_OR_GREATER")] public void DotvvmProperty_ParallelAccess_DoesntCrashProcess() { var properties = new DotvvmProperty[] { From 53c4d1e11e9825c2465453fdca9cc89292e372db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 20 Aug 2025 12:30:28 +0200 Subject: [PATCH 11/11] Fix DotvvmPropertyTests build on Windows --- src/Tests/Runtime/DotvvmPropertyTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tests/Runtime/DotvvmPropertyTests.cs b/src/Tests/Runtime/DotvvmPropertyTests.cs index 8e8ff9dacf..d14f4b1cc1 100644 --- a/src/Tests/Runtime/DotvvmPropertyTests.cs +++ b/src/Tests/Runtime/DotvvmPropertyTests.cs @@ -474,8 +474,8 @@ public void DotvvmProperty_VirtualDictionary_Append(int testClone) } } +#if NET6_0_OR_GREATER [TestMethod, Ignore] - [Conditional("NET6_0_OR_GREATER")] public void DotvvmProperty_ParallelAccess_DoesntCrashProcess() { var properties = new DotvvmProperty[] { @@ -554,6 +554,7 @@ public void DotvvmProperty_ParallelAccess_DoesntCrashProcess() Assert.Fail($"There were {exceptions.Count} exceptions thrown during the test. See the output for details."); } } +#endif [DataTestMethod] [DataRow(0, 0, 0)]