Skip to content

Commit

Permalink
Refactoring of DotvvmProperty value storage
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
exyi committed Nov 3, 2024
1 parent 2bbf954 commit 4745066
Show file tree
Hide file tree
Showing 33 changed files with 2,123 additions and 517 deletions.
4 changes: 2 additions & 2 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
<NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>
<Deterministic>true</Deterministic>
<PreserveCompilationContext>true</PreserveCompilationContext>
<DefaultTargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">netstandard2.1;net6.0;net472</DefaultTargetFrameworks>
<DefaultTargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netstandard2.1;net6.0</DefaultTargetFrameworks>
<DefaultTargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">net8.0;net6.0;netstandard2.1;net472</DefaultTargetFrameworks>
<DefaultTargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">net8.0;net6.0;netstandard2.1</DefaultTargetFrameworks>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages>
</PropertyGroup>
Expand Down
4 changes: 4 additions & 0 deletions src/Framework/Framework/Binding/ActiveDotvvmProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ namespace DotVVM.Framework.Binding
/// <summary> An abstract DotvvmProperty which contains code to be executed when the assigned control is being rendered. </summary>
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);


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace DotVVM.Framework.Binding
/// </summary>
public class CompileTimeOnlyDotvvmProperty : DotvvmProperty
{
public CompileTimeOnlyDotvvmProperty()
private CompileTimeOnlyDotvvmProperty(string name, Type declaringType) : base(name, declaringType, isValueInherited: false)
{
}

Expand All @@ -37,7 +37,7 @@ public override bool IsSet(DotvvmBindableObject control, bool inherit = true)
/// </summary>
public static CompileTimeOnlyDotvvmProperty Register<TPropertyType, TDeclaringType>(string propertyName)
{
var property = new CompileTimeOnlyDotvvmProperty();
var property = new CompileTimeOnlyDotvvmProperty(propertyName, typeof(TDeclaringType));
return (CompileTimeOnlyDotvvmProperty)Register<TPropertyType, TDeclaringType>(propertyName, property: property);
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/Framework/Framework/Binding/DelegateActionProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ namespace DotVVM.Framework.Binding
/// <summary> DotvvmProperty which calls the function passed in the Register method, when the assigned control is being rendered. </summary>
public sealed class DelegateActionProperty<TValue>: ActiveDotvvmProperty
{
private Action<IHtmlWriter, IDotvvmRequestContext, DotvvmProperty, DotvvmControl> func;
private readonly Action<IHtmlWriter, IDotvvmRequestContext, DotvvmProperty, DotvvmControl> func;

public DelegateActionProperty(Action<IHtmlWriter, IDotvvmRequestContext, DotvvmProperty, DotvvmControl> func)
public DelegateActionProperty(Action<IHtmlWriter, IDotvvmRequestContext, DotvvmProperty, DotvvmControl> func, string name, Type declaringType, bool isValueInherited) : base(name, declaringType, isValueInherited)
{
this.func = func;
}
Expand All @@ -27,7 +27,8 @@ public override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestCon

public static DelegateActionProperty<TValue> Register<TDeclaringType>(string name, Action<IHtmlWriter, IDotvvmRequestContext, DotvvmProperty, DotvvmControl> func, [AllowNull] TValue defaultValue = default(TValue))
{
return (DelegateActionProperty<TValue>)DotvvmProperty.Register<TValue, TDeclaringType>(name, defaultValue, false, new DelegateActionProperty<TValue>(func));
var property = new DelegateActionProperty<TValue>(func, name, typeof(TDeclaringType), isValueInherited: false);
return (DelegateActionProperty<TValue>)DotvvmProperty.Register<TValue, TDeclaringType>(name, defaultValue, false, property);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
Expand Down Expand Up @@ -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
)
);
Expand All @@ -191,18 +207,18 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA
Expression getValue;
if (canUseDirectAccess && unwrappedType.IsValueType)
{
getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), new Type[] { unwrappedType }, currentControlParameter, Constant(property));
if (type.IsNullable())
getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), new Type[] { unwrappedType }, currentControlParameter, propertyIdExpr, Constant(property.DefaultValue, type.MakeNullableType()));
if (!type.IsNullable())
getValue = Expression.Property(getValue, "Value");
}
else
{
getValue = Call(currentControlParameter, getValueMethod, Constant(property), Constant(property.IsValueInherited));
getValue = Call(currentControlParameter, getValueMethod, propertyExpr, Constant(property.IsValueInherited));
}
return (
Expression.Lambda(
Expression.Convert(
Expression.Call(currentControlParameter, getValueMethod, Expression.Constant(property), Expression.Constant(false)),
Expression.Call(currentControlParameter, getValueMethod, propertyExpr, Expression.Constant(false)),
type
),
currentControlParameter
Expand Down
40 changes: 20 additions & 20 deletions src/Framework/Framework/Binding/DotvvmCapabilityProperty.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,60 @@ public partial class DotvvmCapabilityProperty
{
internal static class Helpers
{
public static ValueOrBinding<T>? GetOptionalValueOrBinding<T>(DotvvmBindableObject c, DotvvmProperty p)
public static ValueOrBinding<T>? GetOptionalValueOrBinding<T>(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue)
{
if (c.properties.TryGet(p, out var x))
return ValueOrBinding<T>.FromBoxedValue(x);
else return null;
}
public static ValueOrBinding<T> GetValueOrBinding<T>(DotvvmBindableObject c, DotvvmProperty p)
public static ValueOrBinding<T> GetValueOrBinding<T>(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue)
{
if (!c.properties.TryGet(p, out var x))
x = p.DefaultValue;
x = defaultValue;
return ValueOrBinding<T>.FromBoxedValue(x);
}
public static ValueOrBinding<T>? GetOptionalValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p)
public static ValueOrBinding<T>? GetOptionalValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue)
{
if (c.IsPropertySet(p))
return ValueOrBinding<T>.FromBoxedValue(c.GetValue(p));
else return null;
}
public static ValueOrBinding<T> GetValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p)
public static ValueOrBinding<T> GetValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue)
{
return ValueOrBinding<T>.FromBoxedValue(c.GetValue(p));
}
public static void SetOptionalValueOrBinding<T>(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding<T>? val)
public static void SetOptionalValueOrBinding<T>(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue, ValueOrBinding<T>? val)
{
if (val.HasValue)
{
SetValueOrBinding<T>(c, p, val.GetValueOrDefault());
SetValueOrBinding<T>(c, p, defaultValue, val.GetValueOrDefault());
}
else
{
c.properties.Remove(p);
}
}
public static void SetValueOrBinding<T>(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding<T> val)
public static void SetValueOrBinding<T>(DotvvmBindableObject c, DotvvmPropertyId p, object? defaultValue, ValueOrBinding<T> val)
{
var boxedVal = val.UnwrapToObject();
SetValueDirect(c, p, boxedVal);
SetValueDirect(c, p, defaultValue, boxedVal);
}
public static void SetOptionalValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding<T>? val)
public static void SetOptionalValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue, ValueOrBinding<T>? val)
{
if (val.HasValue)
{
SetValueOrBindingSlow<T>(c, p, val.GetValueOrDefault());
SetValueOrBindingSlow<T>(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<T>(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding<T> val)
public static void SetValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p, object? defaultValue, ValueOrBinding<T> 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
}
Expand All @@ -75,15 +75,15 @@ public static void SetValueOrBindingSlow<T>(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<T>(DotvvmBindableObject c, DotvvmProperty p)
public static T? GetStructValueDirect<T>(DotvvmBindableObject c, DotvvmPropertyId p, T? defaultValue)
where T: struct
{
if (c.properties.TryGet(p, out var x))
Expand All @@ -94,11 +94,11 @@ public static void SetValueOrBindingSlow<T>(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
}
Expand Down
20 changes: 5 additions & 15 deletions src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 4745066

Please sign in to comment.