Browse Source

Improve AvaloniaObject.GetValue performance (#15342)

* Improve AvaloniaProperty.GetValue performance

* Remove unneeded MethodImpl on Optional.GetValueOrDefault

* Use ReferenceEqualityComparer instead of TypeEqualityComparer

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/15485/head
Julien Lebosquain 2 years ago
committed by GitHub
parent
commit
edceb96f34
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 56
      src/Avalonia.Base/AvaloniaObject.cs
  2. 92
      src/Avalonia.Base/AvaloniaProperty.cs
  3. 10
      src/Avalonia.Base/AvaloniaPropertyMetadata.cs
  4. 22
      src/Avalonia.Base/Compatibility/ReferenceEqualityComparer.cs
  5. 2
      src/Avalonia.Base/Data/BindingOperations.cs
  6. 4
      src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
  7. 3
      src/Avalonia.Base/Data/Optional.cs
  8. 10
      src/Avalonia.Base/DirectProperty.cs
  9. 37
      src/Avalonia.Base/DirectPropertyBase.cs
  10. 5
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  11. 7
      src/Avalonia.Base/IDirectPropertyAccessor.cs
  12. 9
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  13. 6
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  14. 2
      src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
  15. 2
      src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
  16. 12
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  17. 3
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  18. 4
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs
  19. 2
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  20. 2
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  21. 63
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  22. 74
      src/Avalonia.Base/StyledProperty.cs
  23. 12
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  24. 208
      src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
  25. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  26. 2
      src/Markup/Avalonia.Markup/Data/Binding.cs
  27. 2
      src/Markup/Avalonia.Markup/Data/BindingBase.cs
  28. 2
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  29. 2
      src/Shared/StringCompatibilityExtensions.cs
  30. 9
      tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs
  31. 2
      tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs

56
src/Avalonia.Base/AvaloniaObject.cs

@ -9,8 +9,8 @@ using Avalonia.Data.Core;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
@ -126,7 +126,7 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void ClearValue(AvaloniaProperty property) public void ClearValue(AvaloniaProperty property)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
_values.ClearValue(property); _values.ClearValue(property);
} }
@ -137,7 +137,7 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void ClearValue<T>(AvaloniaProperty<T> property) public void ClearValue<T>(AvaloniaProperty<T> property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
switch (property) switch (property)
@ -159,7 +159,7 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void ClearValue<T>(StyledProperty<T> property) public void ClearValue<T>(StyledProperty<T> property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
_values.ClearValue(property); _values.ClearValue(property);
@ -171,11 +171,11 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void ClearValue<T>(DirectPropertyBase<T> property) public void ClearValue<T>(DirectPropertyBase<T> property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
p.InvokeSetter(this, p.GetUnsetValue(GetType())); p.InvokeSetter(this, p.GetUnsetValue(this));
} }
/// <summary> /// <summary>
@ -216,7 +216,7 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
public object? GetValue(AvaloniaProperty property) public object? GetValue(AvaloniaProperty property)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
if (property.IsDirect) if (property.IsDirect)
return property.RouteGetValue(this); return property.RouteGetValue(this);
@ -232,7 +232,7 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
public T GetValue<T>(StyledProperty<T> property) public T GetValue<T>(StyledProperty<T> property)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
return _values.GetValue(property); return _values.GetValue(property);
} }
@ -245,7 +245,7 @@ namespace Avalonia
/// <returns>The value.</returns> /// <returns>The value.</returns>
public T GetValue<T>(DirectPropertyBase<T> property) public T GetValue<T>(DirectPropertyBase<T> property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -262,7 +262,7 @@ namespace Avalonia
/// </remarks> /// </remarks>
public Optional<T> GetBaseValue<T>(StyledProperty<T> property) public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
return _values.GetBaseValue(property); return _values.GetBaseValue(property);
} }
@ -274,7 +274,7 @@ namespace Avalonia
/// <returns>True if the property is animating, otherwise false.</returns> /// <returns>True if the property is animating, otherwise false.</returns>
public bool IsAnimating(AvaloniaProperty property) public bool IsAnimating(AvaloniaProperty property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
@ -292,7 +292,7 @@ namespace Avalonia
/// </remarks> /// </remarks>
public bool IsSet(AvaloniaProperty property) public bool IsSet(AvaloniaProperty property)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
@ -310,7 +310,7 @@ namespace Avalonia
object? value, object? value,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
return property.RouteSetValue(this, value, priority); return property.RouteSetValue(this, value, priority);
} }
@ -330,7 +330,7 @@ namespace Avalonia
T value, T value,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
ValidatePriority(priority); ValidatePriority(priority);
@ -357,7 +357,7 @@ namespace Avalonia
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
public void SetValue<T>(DirectPropertyBase<T> property, T value) public void SetValue<T>(DirectPropertyBase<T> property, T value)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -401,7 +401,7 @@ namespace Avalonia
/// </remarks> /// </remarks>
public void SetCurrentValue<T>(StyledProperty<T> property, T value) public void SetCurrentValue<T>(StyledProperty<T> property, T value)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
LogPropertySet(property, value, BindingPriority.LocalValue); LogPropertySet(property, value, BindingPriority.LocalValue);
@ -458,8 +458,8 @@ namespace Avalonia
IObservable<object?> source, IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source)); ThrowHelper.ThrowIfNull(source, nameof(source));
VerifyAccess(); VerifyAccess();
ValidatePriority(priority); ValidatePriority(priority);
@ -495,8 +495,8 @@ namespace Avalonia
IObservable<T> source, IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source)); ThrowHelper.ThrowIfNull(source, nameof(source));
VerifyAccess(); VerifyAccess();
ValidatePriority(priority); ValidatePriority(priority);
@ -518,8 +518,8 @@ namespace Avalonia
IObservable<BindingValue<T>> source, IObservable<BindingValue<T>> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source)); ThrowHelper.ThrowIfNull(source, nameof(source));
VerifyAccess(); VerifyAccess();
ValidatePriority(priority); ValidatePriority(priority);
@ -539,7 +539,7 @@ namespace Avalonia
DirectPropertyBase<T> property, DirectPropertyBase<T> property,
IObservable<object?> source) IObservable<object?> source)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -574,7 +574,7 @@ namespace Avalonia
DirectPropertyBase<T> property, DirectPropertyBase<T> property,
IObservable<T> source) IObservable<T> source)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -600,7 +600,7 @@ namespace Avalonia
DirectPropertyBase<T> property, DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source) IObservable<BindingValue<T>> source)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess(); VerifyAccess();
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
@ -796,7 +796,7 @@ namespace Avalonia
{ {
if (value is UnsetValueType) if (value is UnsetValueType)
{ {
property.InvokeSetter(this, property.GetUnsetValue(GetType())); property.InvokeSetter(this, property.GetUnsetValue(this));
} }
else if (!(value is DoNothingType)) else if (!(value is DoNothingType))
{ {
@ -815,7 +815,7 @@ namespace Avalonia
{ {
case BindingValueType.UnsetValue: case BindingValueType.UnsetValue:
case BindingValueType.BindingError: case BindingValueType.BindingError:
var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType())); var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(this));
property.InvokeSetter(this, fallback); property.InvokeSetter(this, fallback);
break; break;
case BindingValueType.DataValidationError: case BindingValueType.DataValidationError:
@ -828,7 +828,7 @@ namespace Avalonia
break; break;
} }
var metadata = property.GetMetadata(GetType()); var metadata = property.GetMetadata(this);
if (metadata.EnableDataValidation == true) if (metadata.EnableDataValidation == true)
{ {

92
src/Avalonia.Base/AvaloniaProperty.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
@ -28,10 +29,11 @@ namespace Avalonia
/// <summary> /// <summary>
/// Provides a fast path when the property has no metadata overrides. /// Provides a fast path when the property has no metadata overrides.
/// </summary> /// </summary>
private KeyValuePair<Type, AvaloniaPropertyMetadata>? _singleMetadata; private Type? _singleHostType;
private AvaloniaPropertyMetadata? _singleMetadata;
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadata; private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadata = new(ReferenceEqualityComparer.Instance);
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadataCache = new Dictionary<Type, AvaloniaPropertyMetadata>(); private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadataCache = new(ReferenceEqualityComparer.Instance);
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class. /// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
@ -60,8 +62,6 @@ namespace Avalonia
throw new ArgumentException("'name' may not contain periods."); throw new ArgumentException("'name' may not contain periods.");
} }
_metadata = new Dictionary<Type, AvaloniaPropertyMetadata>();
Name = name; Name = name;
PropertyType = valueType; PropertyType = valueType;
OwnerType = ownerType; OwnerType = ownerType;
@ -71,7 +71,8 @@ namespace Avalonia
metadata.Freeze(); metadata.Freeze();
_metadata.Add(hostType, metadata); _metadata.Add(hostType, metadata);
_defaultMetadata = metadata.GenerateTypeSafeMetadata(); _defaultMetadata = metadata.GenerateTypeSafeMetadata();
_singleMetadata = new(hostType, metadata); _singleHostType = hostType;
_singleMetadata = metadata;
} }
/// <summary> /// <summary>
@ -85,8 +86,6 @@ namespace Avalonia
Type ownerType, Type ownerType,
AvaloniaPropertyMetadata? metadata) AvaloniaPropertyMetadata? metadata)
{ {
_metadata = new Dictionary<Type, AvaloniaPropertyMetadata>();
Name = source?.Name ?? throw new ArgumentNullException(nameof(source)); Name = source?.Name ?? throw new ArgumentNullException(nameof(source));
PropertyType = source.PropertyType; PropertyType = source.PropertyType;
OwnerType = ownerType ?? throw new ArgumentNullException(nameof(ownerType)); OwnerType = ownerType ?? throw new ArgumentNullException(nameof(ownerType));
@ -474,11 +473,39 @@ namespace Avalonia
/// Gets the <see cref="AvaloniaPropertyMetadata"/> which applies to this property when it is used with the specified type. /// Gets the <see cref="AvaloniaPropertyMetadata"/> which applies to this property when it is used with the specified type.
/// </summary> /// </summary>
/// <typeparam name="T">The type for which to retrieve metadata.</typeparam> /// <typeparam name="T">The type for which to retrieve metadata.</typeparam>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AvaloniaPropertyMetadata GetMetadata<T>() where T : AvaloniaObject => GetMetadata(typeof(T)); public AvaloniaPropertyMetadata GetMetadata<T>() where T : AvaloniaObject => GetMetadata(typeof(T));
/// <inheritdoc cref="GetMetadata{T}"/> /// <inheritdoc cref="GetMetadata{T}"/>
/// <param name="type">The type for which to retrieve metadata.</param> /// <param name="type">The type for which to retrieve metadata.</param>
public AvaloniaPropertyMetadata GetMetadata(Type type) => GetMetadataWithOverrides(type); /// <remarks>
/// For performance, prefer the <see cref="GetMetadata(Avalonia.AvaloniaObject)"/> overload when possible.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AvaloniaPropertyMetadata GetMetadata(Type type)
{
if (_singleMetadata == _defaultMetadata)
{
return _defaultMetadata;
}
return GetMetadataFromCache(type);
}
/// <summary>
/// Gets the <see cref="AvaloniaPropertyMetadata"/> which applies to this property when it is used with the specified object.
/// </summary>
/// <param name="owner">The object for which to retrieve metadata.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AvaloniaPropertyMetadata GetMetadata(AvaloniaObject owner)
{
if (_singleMetadata == _defaultMetadata)
{
return _defaultMetadata;
}
return GetMetadataFromCache(owner);
}
/// <summary> /// <summary>
/// Checks whether the <paramref name="value"/> is valid for the property. /// Checks whether the <paramref name="value"/> is valid for the property.
@ -593,42 +620,65 @@ namespace Avalonia
_metadataCache.Clear(); _metadataCache.Clear();
_singleMetadata = null; _singleMetadata = null;
_singleHostType = null;
} }
private protected abstract IObservable<AvaloniaPropertyChangedEventArgs> GetChanged(); private protected abstract IObservable<AvaloniaPropertyChangedEventArgs> GetChanged();
private AvaloniaPropertyMetadata GetMetadataWithOverrides(Type type) [MethodImpl(MethodImplOptions.NoInlining)]
private AvaloniaPropertyMetadata GetMetadataFromCache(AvaloniaObject obj)
{
// Don't cache if we have _singleMetadata: IsInstanceOfType is faster than a dictionary lookup.
if (_singleMetadata is not null)
{
return _singleHostType!.IsInstanceOfType(obj) ? _singleMetadata : _defaultMetadata;
}
var type = obj.GetType();
if (!_metadataCache.TryGetValue(type, out var result))
{
_metadataCache[type] = result = GetMetadataUncached(type);
}
return result;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private AvaloniaPropertyMetadata GetMetadataFromCache(Type type)
{ {
if (type is null) if (!_metadataCache.TryGetValue(type, out var result))
{ {
throw new ArgumentNullException(nameof(type)); _metadataCache[type] = result = GetMetadataUncached(type);
} }
if (_metadataCache.TryGetValue(type, out var result)) return result;
}
private AvaloniaPropertyMetadata GetMetadataUncached(Type type)
{
if (_singleMetadata == _defaultMetadata)
{ {
return result; return _defaultMetadata;
} }
if (_singleMetadata is { } singleMetadata) if (_singleMetadata is not null)
{ {
return _metadataCache[type] = singleMetadata.Key.IsAssignableFrom(type) ? singleMetadata.Value : _defaultMetadata; return _singleHostType!.IsAssignableFrom(type) ? _singleMetadata : _defaultMetadata;
} }
var currentType = type; var currentType = type;
while (currentType != null) while (currentType is not null)
{ {
if (_metadata.TryGetValue(currentType, out result)) if (_metadata.TryGetValue(currentType, out var result))
{ {
_metadataCache[type] = result;
return result; return result;
} }
currentType = currentType.BaseType; currentType = currentType.BaseType;
} }
return _metadataCache[type] = _defaultMetadata; return _defaultMetadata;
} }
bool IPropertyInfo.CanGet => true; bool IPropertyInfo.CanGet => true;

10
src/Avalonia.Base/AvaloniaPropertyMetadata.cs

@ -8,7 +8,6 @@ namespace Avalonia
/// </summary> /// </summary>
public abstract class AvaloniaPropertyMetadata public abstract class AvaloniaPropertyMetadata
{ {
private bool _isReadOnly;
private BindingMode _defaultBindingMode; private BindingMode _defaultBindingMode;
/// <summary> /// <summary>
@ -47,6 +46,11 @@ namespace Avalonia
/// </remarks> /// </remarks>
public bool? EnableDataValidation { get; private set; } public bool? EnableDataValidation { get; private set; }
/// <summary>
/// Gets whether this instance is read-only and can't be modified.
/// </summary>
public bool IsReadOnly { get; private set; }
/// <summary> /// <summary>
/// Merges the metadata with the base metadata. /// Merges the metadata with the base metadata.
/// </summary> /// </summary>
@ -56,7 +60,7 @@ namespace Avalonia
AvaloniaPropertyMetadata baseMetadata, AvaloniaPropertyMetadata baseMetadata,
AvaloniaProperty property) AvaloniaProperty property)
{ {
if (_isReadOnly) if (IsReadOnly)
{ {
throw new InvalidOperationException("The metadata is read-only."); throw new InvalidOperationException("The metadata is read-only.");
} }
@ -74,7 +78,7 @@ namespace Avalonia
/// No further modifications are allowed after this call. /// No further modifications are allowed after this call.
/// </summary> /// </summary>
public void Freeze() public void Freeze()
=> _isReadOnly = true; => IsReadOnly = true;
/// <summary> /// <summary>
/// Gets a copy of this object configured for use with any owner type. /// Gets a copy of this object configured for use with any owner type.

22
src/Avalonia.Base/Compatibility/ReferenceEqualityComparer.cs

@ -0,0 +1,22 @@
#if NETSTANDARD2_0
using System.Runtime.CompilerServices;
namespace System.Collections.Generic;
internal sealed class ReferenceEqualityComparer : IEqualityComparer<object?>, IEqualityComparer
{
public static ReferenceEqualityComparer Instance { get; } = new();
private ReferenceEqualityComparer()
{
}
public new bool Equals(object? x, object? y)
=> ReferenceEquals(x, y);
public int GetHashCode(object? obj)
=> RuntimeHelpers.GetHashCode(obj);
}
#endif

2
src/Avalonia.Base/Data/BindingOperations.cs

@ -34,7 +34,7 @@ namespace Avalonia.Data
if (mode == BindingMode.Default) if (mode == BindingMode.Default)
{ {
mode = property.GetMetadata(target.GetType()).DefaultBindingMode; mode = property.GetMetadata(target).DefaultBindingMode;
} }
switch (mode) switch (mode)

4
src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs

@ -532,9 +532,9 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
if (TargetProperty is not null && _target?.TryGetTarget(out var target) == true) if (TargetProperty is not null && _target?.TryGetTarget(out var target) == true)
{ {
if (TargetProperty.IsDirect) if (TargetProperty.IsDirect)
_defaultValue = ((IDirectPropertyAccessor)TargetProperty).GetUnsetValue(target.GetType()); _defaultValue = ((IDirectPropertyAccessor)TargetProperty).GetUnsetValue(target);
else else
_defaultValue = ((IStyledPropertyAccessor)TargetProperty).GetDefaultValue(target.GetType()); _defaultValue = ((IStyledPropertyAccessor)TargetProperty).GetDefaultValue(target);
_isDefaultValueInitialized = true; _isDefaultValueInitialized = true;
return _defaultValue; return _defaultValue;

3
src/Avalonia.Base/Data/Optional.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Avalonia.Data namespace Avalonia.Data
{ {
@ -67,7 +68,7 @@ namespace Avalonia.Data
/// Gets the value if present, otherwise the default value. /// Gets the value if present, otherwise the default value.
/// </summary> /// </summary>
/// <returns>The value.</returns> /// <returns>The value.</returns>
public T? GetValueOrDefault() => HasValue ? _value : default; public T? GetValueOrDefault() => _value;
/// <summary> /// <summary>
/// Gets the value if present, otherwise a default value. /// Gets the value if present, otherwise a default value.

10
src/Avalonia.Base/DirectProperty.cs

@ -97,7 +97,7 @@ namespace Avalonia
metadata.Freeze(); metadata.Freeze();
var result = new DirectProperty<TNewOwner, TValue>( var result = new DirectProperty<TNewOwner, TValue>(
(DirectPropertyBase<TValue>)this, this,
getter, getter,
setter, setter,
metadata); metadata);
@ -144,9 +144,9 @@ namespace Avalonia
} }
object? IDirectPropertyAccessor.GetUnsetValue(Type type) object? IDirectPropertyAccessor.GetUnsetValue(Type type)
{ => GetMetadata(type).UnsetValue;
var metadata = GetMetadata(type);
return metadata.UnsetValue; object? IDirectPropertyAccessor.GetUnsetValue(AvaloniaObject owner)
} => GetMetadata(owner).UnsetValue;
} }
} }

37
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Runtime.CompilerServices;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
@ -70,21 +71,37 @@ namespace Avalonia
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <returns>The unset value.</returns> /// <returns>The unset value.</returns>
public TValue GetUnsetValue(Type type) public TValue GetUnsetValue(Type type)
{ => GetMetadata(type).UnsetValue;
type = type ?? throw new ArgumentNullException(nameof(type));
return GetMetadata(type).UnsetValue;
}
/// <summary> /// <summary>
/// Gets the property metadata for the specified type. /// Gets the unset value for the property on the specified object.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="owner">The object.</param>
/// <returns> /// <returns>The unset value.</returns>
/// The property metadata. public TValue GetUnsetValue(AvaloniaObject owner)
/// </returns> => GetMetadata(owner).UnsetValue;
/// <inheritdoc cref="AvaloniaProperty.GetMetadata(System.Type)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new DirectPropertyMetadata<TValue> GetMetadata(Type type) public new DirectPropertyMetadata<TValue> GetMetadata(Type type)
=> CastMetadata(base.GetMetadata(type));
/// <inheritdoc cref="AvaloniaProperty.GetMetadata(Avalonia.AvaloniaObject)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new DirectPropertyMetadata<TValue> GetMetadata(AvaloniaObject owner)
=> CastMetadata(base.GetMetadata(owner));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static DirectPropertyMetadata<TValue> CastMetadata(AvaloniaPropertyMetadata metadata)
{ {
return (DirectPropertyMetadata<TValue>)base.GetMetadata(type); #if DEBUG
return (DirectPropertyMetadata<TValue>)metadata;
#else
// Avoid casts in release mode for performance.
// We control every path:
// it shouldn't be possible a metadata type other than a DirectPropertyMetadata<T> stored for a DirectPropertyBase<T>.
return Unsafe.As<DirectPropertyMetadata<TValue>>(metadata);
#endif
} }
/// <summary> /// <summary>

5
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@ -49,6 +49,11 @@ namespace Avalonia
/// <inheritdoc /> /// <inheritdoc />
public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata()
{ {
if (IsReadOnly)
{
return this;
}
var copy = new DirectPropertyMetadata<TValue>(UnsetValue, DefaultBindingMode, EnableDataValidation); var copy = new DirectPropertyMetadata<TValue>(UnsetValue, DefaultBindingMode, EnableDataValidation);
copy.Freeze(); copy.Freeze();
return copy; return copy;

7
src/Avalonia.Base/IDirectPropertyAccessor.cs

@ -1,5 +1,4 @@
using System; using System;
using Avalonia.Metadata;
namespace Avalonia namespace Avalonia
{ {
@ -38,5 +37,11 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
object? GetUnsetValue(Type type); object? GetUnsetValue(Type type);
/// <summary>
/// Gets the unset value of the property for the specified object.
/// </summary>
/// <param name="owner">The object.</param>
object? GetUnsetValue(AvaloniaObject owner);
} }
} }

9
src/Avalonia.Base/IStyledPropertyAccessor.cs

@ -16,6 +16,15 @@ namespace Avalonia
/// </returns> /// </returns>
object? GetDefaultValue(Type type); object? GetDefaultValue(Type type);
/// <summary>
/// Gets the default value for the property for the specified object.
/// </summary>
/// <param name="owner">The object.</param>
/// <returns>
/// The default value.
/// </returns>
object? GetDefaultValue(AvaloniaObject owner);
/// <summary> /// <summary>
/// Validates the specified property value. /// Validates the specified property value.
/// </summary> /// </summary>

6
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -45,7 +45,7 @@ namespace Avalonia.PropertyStore
Frame = frame; Frame = frame;
Property = property; Property = property;
Source = source; Source = source;
if (property.GetMetadata(target.GetType()).EnableDataValidation == true) if (property.GetMetadata(target).EnableDataValidation == true)
_uncommon = new() { _hasDataValidation = true }; _uncommon = new() { _hasDataValidation = true };
} }
@ -112,7 +112,7 @@ namespace Avalonia.PropertyStore
protected abstract BindingValue<TValue> ConvertAndValidate(TSource value); protected abstract BindingValue<TValue> ConvertAndValidate(TSource value);
protected abstract BindingValue<TValue> ConvertAndValidate(BindingValue<TSource> value); protected abstract BindingValue<TValue> ConvertAndValidate(BindingValue<TSource> value);
protected abstract TValue GetDefaultValue(Type ownerType); protected abstract TValue GetDefaultValue(AvaloniaObject owner);
protected virtual void Start(bool produceValue) protected virtual void Start(bool produceValue)
{ {
@ -186,7 +186,7 @@ namespace Avalonia.PropertyStore
if (_uncommon?._isDefaultValueInitialized != true) if (_uncommon?._isDefaultValueInitialized != true)
{ {
_uncommon ??= new(); _uncommon ??= new();
_uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); _uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner);
_uncommon._isDefaultValueInitialized = true; _uncommon._isDefaultValueInitialized = true;
} }

2
src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs

@ -15,7 +15,7 @@ namespace Avalonia.PropertyStore
public DirectBindingObserver(ValueStore owner, DirectPropertyBase<T> property) public DirectBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{ {
_owner = owner; _owner = owner;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false; _hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false;
Property = property; Property = property;
} }

2
src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs

@ -16,7 +16,7 @@ namespace Avalonia.PropertyStore
public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase<T> property) public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{ {
_owner = owner; _owner = owner;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false; _hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false;
Property = property; Property = property;
} }

12
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@ -11,6 +11,11 @@ namespace Avalonia.PropertyStore
/// </remarks> /// </remarks>
internal abstract class EffectiveValue internal abstract class EffectiveValue
{ {
/// <summary>
/// Gets the property targeted by this value.
/// </summary>
public AvaloniaProperty Property { get; protected init; }
/// <summary> /// <summary>
/// Gets the current effective value as a boxed value. /// Gets the current effective value as a boxed value.
/// </summary> /// </summary>
@ -53,6 +58,13 @@ namespace Avalonia.PropertyStore
/// </summary> /// </summary>
public bool IsCoercedDefaultValue { get; set; } public bool IsCoercedDefaultValue { get; set; }
/// <summary>
/// Initializes a new instance of <see cref="EffectiveValue"/>.
/// </summary>
/// <param name="property">The property targeted by this value.</param>
protected EffectiveValue(AvaloniaProperty property)
=> Property = property;
/// <summary> /// <summary>
/// Begins a reevaluation pass on the effective value. /// Begins a reevaluation pass on the effective value.
/// </summary> /// </summary>

3
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -23,10 +23,11 @@ namespace Avalonia.PropertyStore
AvaloniaObject owner, AvaloniaObject owner,
StyledProperty<T> property, StyledProperty<T> property,
EffectiveValue<T>? inherited) EffectiveValue<T>? inherited)
: base(property)
{ {
Priority = BindingPriority.Unset; Priority = BindingPriority.Unset;
BasePriority = BindingPriority.Unset; BasePriority = BindingPriority.Unset;
_metadata = property.GetMetadata(owner.GetType()); _metadata = property.GetMetadata(owner);
var value = inherited is null ? _metadata.DefaultValue : inherited.Value; var value = inherited is null ? _metadata.DefaultValue : inherited.Value;

4
src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs

@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
{ {
_owner = owner; _owner = owner;
Property = property; Property = property;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false; _hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false;
} }
public StyledProperty<T> Property { get;} public StyledProperty<T> Property { get;}
@ -121,7 +121,7 @@ namespace Avalonia.PropertyStore
{ {
if (!_isDefaultValueInitialized) if (!_isDefaultValueInitialized)
{ {
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); _defaultValue = Property.GetDefaultValue(_owner.Owner);
_isDefaultValueInitialized = true; _isDefaultValueInitialized = true;
} }

2
src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs

@ -33,6 +33,6 @@ namespace Avalonia.PropertyStore
throw new NotSupportedException(); throw new NotSupportedException();
} }
protected override TTarget GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); protected override TTarget GetDefaultValue(AvaloniaObject owner) => Property.GetDefaultValue(owner);
} }
} }

2
src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs

@ -51,6 +51,6 @@ namespace Avalonia.PropertyStore
return value; return value;
} }
protected override T GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); protected override T GetDefaultValue(AvaloniaObject owner) => Property.GetDefaultValue(owner);
} }
} }

63
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
@ -283,11 +284,25 @@ namespace Avalonia.PropertyStore
public T GetValue<T>(StyledProperty<T> property) public T GetValue<T>(StyledProperty<T> property)
{ {
// <!> Performance critical method
if (_effectiveValues.TryGetValue(property, out var v)) if (_effectiveValues.TryGetValue(property, out var v))
return ((EffectiveValue<T>)v).Value; return CastEffectiveValue<T>(v).Value;
if (property.Inherits && TryGetInheritedValue(property, out v)) if (property.Inherits && TryGetInheritedValue(property, out v))
return ((EffectiveValue<T>)v).Value; return CastEffectiveValue<T>(v).Value;
return property.GetDefaultValue(Owner.GetType()); return property.GetDefaultValue(Owner);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static EffectiveValue<T> CastEffectiveValue<T>(EffectiveValue value)
{
#if DEBUG
return (EffectiveValue<T>)value;
#else
// Avoid casts in release mode for performance since GetValue is a very hot path.
// We control every path:
// it shouldn't be possible to have something else than an EffectiveValue<T> stored for a T property.
return Unsafe.As<EffectiveValue<T>>(value);
#endif
} }
public bool IsAnimating(AvaloniaProperty property) public bool IsAnimating(AvaloniaProperty property)
@ -309,7 +324,7 @@ namespace Avalonia.PropertyStore
public void CoerceDefaultValue<T>(StyledProperty<T> property) public void CoerceDefaultValue<T>(StyledProperty<T> property)
{ {
var metadata = property.GetMetadata(Owner.GetType()); var metadata = property.GetMetadata(Owner);
if (metadata.CoerceValue is null) if (metadata.CoerceValue is null)
return; return;
@ -388,9 +403,9 @@ namespace Avalonia.PropertyStore
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
f._effectiveValues.GetKeyValue(i, out var key, out var value); var value = f._effectiveValues.GetValue(i);
if (key.Inherits) if (value.Property.Inherits)
values.TryAdd(key, new(value)); values.TryAdd(value.Property, new(value));
} }
f = f.InheritanceAncestor; f = f.InheritanceAncestor;
@ -405,19 +420,20 @@ namespace Avalonia.PropertyStore
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
f._effectiveValues.GetKeyValue(i, out var key, out var value); var value = f._effectiveValues.GetValue(i);
var property = value.Property;
if (!key.Inherits) if (!property.Inherits)
continue; continue;
if (values.TryGetValue(key, out var existing)) if (values.TryGetValue(property, out var existing))
{ {
if (existing.NewValue is null) if (existing.NewValue is null)
values[key] = existing.WithNewValue(value); values[property] = existing.WithNewValue(value);
} }
else else
{ {
values.Add(key, new(null, value)); values.Add(property, new(null, value));
} }
} }
@ -431,12 +447,15 @@ namespace Avalonia.PropertyStore
var count = values.Count; var count = values.Count;
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
values.GetKeyValue(i, out var key, out var v); var v = values.GetValue(i);
var oldValue = v.OldValue; var oldValue = v.OldValue;
var newValue = v.NewValue; var newValue = v.NewValue;
if (oldValue != newValue) if (oldValue != newValue)
InheritedValueChanged(key, oldValue, newValue); {
var property = v.OldValue?.Property ?? v.NewValue!.Property;
InheritedValueChanged(property, oldValue, newValue);
}
} }
} }
@ -1077,14 +1096,15 @@ namespace Avalonia.PropertyStore
// Remove all effective values that are still unset. // Remove all effective values that are still unset.
for (var i = _effectiveValues.Count - 1; i >= 0; --i) for (var i = _effectiveValues.Count - 1; i >= 0; --i)
{ {
_effectiveValues.GetKeyValue(i, out var key, out var e); var e = _effectiveValues.GetValue(i);
var property = e.Property;
e.EndReevaluation(this, key); e.EndReevaluation(this, property);
if (e.CanRemove()) if (e.CanRemove())
{ {
RemoveEffectiveValue(key, i); RemoveEffectiveValue(property, i);
e.DisposeAndRaiseUnset(this, key); e.DisposeAndRaiseUnset(this, property);
if (i > _effectiveValues.Count) if (i > _effectiveValues.Count)
break; break;
@ -1134,10 +1154,7 @@ namespace Avalonia.PropertyStore
AvaloniaProperty property, AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? value) [NotNullWhen(true)] out EffectiveValue? value)
{ {
if (_effectiveValues.TryGetValue(property, out value)) return _effectiveValues.TryGetValue(property, out value);
return true;
value = null;
return false;
} }
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property) private EffectiveValue? GetEffectiveValue(AvaloniaProperty property)
@ -1149,7 +1166,7 @@ namespace Avalonia.PropertyStore
private object? GetDefaultValue(AvaloniaProperty property) private object? GetDefaultValue(AvaloniaProperty property)
{ {
return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType()); return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner);
} }
private void DisposeExistingLocalValueBinding(AvaloniaProperty property) private void DisposeExistingLocalValueBinding(AvaloniaProperty property)

74
src/Avalonia.Base/StyledProperty.cs

@ -1,7 +1,7 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -12,6 +12,10 @@ namespace Avalonia
/// </summary> /// </summary>
public class StyledProperty<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor public class StyledProperty<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
{ {
// For performance, cache the default value if there's only one (mostly for AvaloniaObject.GetValue()),
// avoiding a GetMetadata() call which might need to iterate through the control hierarchy.
private Optional<TValue> _singleDefaultValue;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StyledProperty{T}"/> class. /// Initializes a new instance of the <see cref="StyledProperty{T}"/> class.
/// </summary> /// </summary>
@ -41,8 +45,11 @@ namespace Avalonia
if (validate?.Invoke(metadata.DefaultValue) == false) if (validate?.Invoke(metadata.DefaultValue) == false)
{ {
throw new ArgumentException( throw new ArgumentException(
$"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); $"'{metadata.DefaultValue}' is not a valid default value for '{name}'.",
nameof(metadata));
} }
_singleDefaultValue = metadata.DefaultValue;
} }
/// <summary> /// <summary>
@ -68,7 +75,7 @@ namespace Avalonia
public TValue CoerceValue(AvaloniaObject instance, TValue baseValue) public TValue CoerceValue(AvaloniaObject instance, TValue baseValue)
{ {
var metadata = GetMetadata(instance.GetType()); var metadata = GetMetadata(instance);
if (metadata.CoerceValue != null) if (metadata.CoerceValue != null)
{ {
@ -83,22 +90,51 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <returns>The default value.</returns> /// <returns>The default value.</returns>
/// <remarks>
/// For performance, prefer the <see cref="GetDefaultValue(Avalonia.AvaloniaObject)"/> overload when possible.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue GetDefaultValue(Type type) public TValue GetDefaultValue(Type type)
{ {
return GetMetadata(type).DefaultValue; return _singleDefaultValue.HasValue ?
_singleDefaultValue.GetValueOrDefault()! :
GetMetadata(type).DefaultValue;
} }
/// <summary> /// <summary>
/// Gets the property metadata for the specified type. /// Gets the default value for the property on the specified object.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="owner">The object.</param>
/// <returns> /// <returns>The default value.</returns>
/// The property metadata. [MethodImpl(MethodImplOptions.AggressiveInlining)]
/// </returns> public TValue GetDefaultValue(AvaloniaObject owner)
{
return _singleDefaultValue.HasValue ?
_singleDefaultValue.GetValueOrDefault()! :
GetMetadata(owner).DefaultValue;
}
/// <inheritdoc cref="AvaloniaProperty.GetMetadata(System.Type)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new StyledPropertyMetadata<TValue> GetMetadata(Type type) public new StyledPropertyMetadata<TValue> GetMetadata(Type type)
=> CastMetadata(base.GetMetadata(type));
/// <inheritdoc cref="AvaloniaProperty.GetMetadata(Avalonia.AvaloniaObject)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new StyledPropertyMetadata<TValue> GetMetadata(AvaloniaObject owner)
=> CastMetadata(base.GetMetadata(owner));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static StyledPropertyMetadata<TValue> CastMetadata(AvaloniaPropertyMetadata metadata)
{ {
_ = type ?? throw new ArgumentNullException(nameof(type)); #if DEBUG
return (StyledPropertyMetadata<TValue>)base.GetMetadata(type); return (StyledPropertyMetadata<TValue>)metadata;
#else
// Avoid casts in release mode for performance (GetMetadata is a hot path).
// We control every path:
// it shouldn't be possible a metadata type other than a StyledPropertyMetadata<T> stored for a StyledProperty<T>.
return Unsafe.As<StyledPropertyMetadata<TValue>>(metadata);
#endif
} }
/// <summary> /// <summary>
@ -145,6 +181,11 @@ namespace Avalonia
} }
base.OverrideMetadata(type, metadata); base.OverrideMetadata(type, metadata);
if (_singleDefaultValue != metadata.DefaultValue)
{
_singleDefaultValue = default;
}
} }
/// <summary> /// <summary>
@ -156,8 +197,9 @@ namespace Avalonia
return Name; return Name;
} }
/// <inheritdoc/> object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type);
object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
object? IStyledPropertyAccessor.GetDefaultValue(AvaloniaObject owner) => GetDefaultValue(owner);
bool IStyledPropertyAccessor.ValidateValue(object? value) bool IStyledPropertyAccessor.ValidateValue(object? value)
{ {
@ -253,11 +295,5 @@ namespace Avalonia
throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})");
} }
} }
private object? GetDefaultBoxedValue(Type type)
{
_ = type ?? throw new ArgumentNullException(nameof(type));
return GetMetadata(type).DefaultValue;
}
} }
} }

12
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Runtime.CompilerServices;
using Avalonia.Data; using Avalonia.Data;
namespace Avalonia namespace Avalonia
@ -31,7 +32,11 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the default value for the property. /// Gets the default value for the property.
/// </summary> /// </summary>
public TValue DefaultValue => _defaultValue.GetValueOrDefault()!; public TValue DefaultValue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _defaultValue.GetValueOrDefault()!;
}
/// <summary> /// <summary>
/// Gets the value coercion callback, if any. /// Gets the value coercion callback, if any.
@ -62,6 +67,11 @@ namespace Avalonia
/// <inheritdoc /> /// <inheritdoc />
public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata()
{ {
if (IsReadOnly && CoerceValue is null)
{
return this;
}
var copy = new StyledPropertyMetadata<TValue>(DefaultValue, DefaultBindingMode, null, EnableDataValidation ?? false); var copy = new StyledPropertyMetadata<TValue>(DefaultValue, DefaultBindingMode, null, EnableDataValidation ?? false);
copy.Freeze(); copy.Freeze();
return copy; return copy;

208
src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
@ -52,7 +54,7 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <param name="property">The key to get or set.</param> /// <param name="property">The key to get or set.</param>
/// <returns> /// <returns>
/// The value associated with the specified key. If the key is not found, a get operation /// The value associated with the specified key. If the key is not found, a get operation
/// throws a <see cref="KeyNotFoundException"/>, and a set operation creates a /// throws a <see cref="KeyNotFoundException"/>, and a set operation creates a
/// new element for the specified key. /// new element for the specified key.
/// </returns> /// </returns>
@ -63,16 +65,18 @@ namespace Avalonia.Utilities
{ {
get get
{ {
if (!TryGetEntry(property.Id, out var index)) var index = FindEntry(property.Id);
if (index < 0)
ThrowNotFound(); ThrowNotFound();
return _entries[index].Value; return UnsafeGetEntryRef(index).Value;
} }
set set
{ {
if (TryGetEntry(property.Id, out var index)) var index = FindEntry(property.Id);
_entries[index] = new Entry(property, value); if (index >= 0)
UnsafeGetEntryRef(index) = new Entry(property, value);
else else
InsertEntry(new Entry(property, value), index); InsertEntry(new Entry(property, value), ~index);
} }
} }
@ -88,7 +92,7 @@ namespace Avalonia.Utilities
{ {
if (index >= _entryCount) if (index >= _entryCount)
ThrowOutOfRange(); ThrowOutOfRange();
return _entries![index].Value; return UnsafeGetEntryRef(index).Value;
} }
} }
@ -99,11 +103,12 @@ namespace Avalonia.Utilities
/// <param name="value">The value of the element to add.</param> /// <param name="value">The value of the element to add.</param>
public void Add(AvaloniaProperty property, TValue value) public void Add(AvaloniaProperty property, TValue value)
{ {
if (TryGetEntry(property.Id, out var index)) var index = FindEntry(property.Id);
if (index >= 0)
ThrowDuplicate(); ThrowDuplicate();
InsertEntry(new Entry(property, value), index); InsertEntry(new Entry(property, value), ~index);
} }
/// <summary> /// <summary>
/// Removes all keys and values from the collection. /// Removes all keys and values from the collection.
/// </summary> /// </summary>
@ -124,27 +129,19 @@ namespace Avalonia.Utilities
/// Determines whether the collection contains the specified key. /// Determines whether the collection contains the specified key.
/// </summary> /// </summary>
/// <param name="property">The key.</param> /// <param name="property">The key.</param>
public bool ContainsKey(AvaloniaProperty property) => TryGetEntry(property.Id, out _); public bool ContainsKey(AvaloniaProperty property) => FindEntry(property.Id) >= 0;
/// <summary> /// <summary>
/// Gets the key and value at the specified index. /// Gets value at the specified index.
/// </summary> /// </summary>
/// <param name="index"> /// <param name="index">The index of the entry, between 0 and <see cref="Count"/> - 1.</param>
/// The index of the entry, between 0 and <see cref="Count"/> - 1. /// <returns>The value at the specified index.</returns>
/// </param> public TValue GetValue(int index)
/// <param name="key">
/// When this method returns, contains the key at the specified index.
/// </param>
/// <param name="value">
/// When this method returns, contains the value at the specified index.
/// </param>
public void GetKeyValue(int index, out AvaloniaProperty key, out TValue value)
{ {
if (index >= _entryCount) if (index >= _entryCount)
ThrowOutOfRange(); ThrowOutOfRange();
ref var entry = ref _entries![index]; ref var entry = ref UnsafeGetEntryRef(index);
key = entry.Property; return entry.Value;
value = entry.Value;
} }
/// <summary> /// <summary>
@ -157,7 +154,8 @@ namespace Avalonia.Utilities
/// </returns> /// </returns>
public bool Remove(AvaloniaProperty property) public bool Remove(AvaloniaProperty property)
{ {
if (TryGetEntry(property.Id, out var index)) var index = FindEntry(property.Id);
if (index >= 0)
{ {
RemoveAt(index); RemoveAt(index);
return true; return true;
@ -178,9 +176,10 @@ namespace Avalonia.Utilities
/// </returns> /// </returns>
public bool Remove(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) public bool Remove(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{ {
if (TryGetEntry(property.Id, out var index)) var index = FindEntry(property.Id);
if (index >= 0)
{ {
value = _entries[index].Value; value = UnsafeGetEntryRef(index).Value;
RemoveAt(index); RemoveAt(index);
return true; return true;
} }
@ -196,11 +195,11 @@ namespace Avalonia.Utilities
public void RemoveAt(int index) public void RemoveAt(int index)
{ {
if (_entries is null) if (_entries is null)
throw new IndexOutOfRangeException(); ThrowOutOfRange();
Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1); Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1);
_entryCount--; _entryCount--;
_entries[_entryCount] = default; UnsafeGetEntryRef(_entryCount) = default;
} }
/// <summary> /// <summary>
@ -211,9 +210,10 @@ namespace Avalonia.Utilities
/// <returns></returns> /// <returns></returns>
public bool TryAdd(AvaloniaProperty property, TValue value) public bool TryAdd(AvaloniaProperty property, TValue value)
{ {
if (TryGetEntry(property.Id, out var index)) var index = FindEntry(property.Id);
if (index >= 0)
return false; return false;
InsertEntry(new Entry(property, value), index); InsertEntry(new Entry(property, value), ~index);
return true; return true;
} }
@ -228,73 +228,91 @@ namespace Avalonia.Utilities
/// <returns></returns> /// <returns></returns>
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value) public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{ {
if (TryGetEntry(property.Id, out var index)) // <!> Very performance critical code: FindEntry has been manually inlined here.
// This gives a ~20% speedup in micro-benchmarks.
var lo = 0;
var hi = _entryCount - 1;
if (hi >= 0)
{ {
value = _entries[index].Value; var propertyId = property.Id;
return true; ref var entry0 = ref UnsafeGetEntryRef(0);
do
{
// hi and lo are never negative: there's no overflow using unsigned math
var i = (int)(((uint)hi + (uint)lo) >> 1);
#if NET6_0_OR_GREATER
// nuint cast to force zero extend instead of sign extend
ref var entry = ref Unsafe.Add(ref entry0, (nuint)i);
#else
ref var entry = ref Unsafe.Add(ref entry0, i);
#endif
var entryId = entry.Id;
if (entryId == propertyId)
{
value = entry.Value;
return true;
}
if (entryId < propertyId)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
} while (lo <= hi);
} }
value = default; value = default;
return false; return false;
} }
[MemberNotNullWhen(true, nameof(_entries))] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetEntry(int propertyId, out int index) private int FindEntry(int propertyId)
{ {
int checkIndex; var lo = 0;
int iLo = 0; var hi = _entryCount - 1;
int iHi = _entryCount;
if (iHi <= 0) if (hi >= 0)
{ {
index = 0; ref var entry0 = ref UnsafeGetEntryRef(0);
return false;
}
// Do a binary search to find the value do
while (iHi - iLo > 3)
{
int iPv = (iHi + iLo) / 2;
checkIndex = _entries![iPv].Property.Id;
if (propertyId == checkIndex)
{ {
index = iPv; // hi and lo are never negative: there's no overflow using unsigned math
return true; var i = (int)(((uint)hi + (uint)lo) >> 1);
}
#if NET6_0_OR_GREATER
if (propertyId <= checkIndex) // nuint cast to force zero extend instead of sign extend
{ ref var entry = ref Unsafe.Add(ref entry0, (nuint)i);
iHi = iPv; #else
} ref var entry = ref Unsafe.Add(ref entry0, i);
else #endif
{
iLo = iPv + 1; var entryId = entry.Id;
} if (entryId == propertyId)
{
return i;
}
if (entryId < propertyId)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
} while (lo <= hi);
} }
// Now we only have three values to search; switch to a linear search return ~lo;
do
{
checkIndex = _entries![iLo].Property.Id;
if (checkIndex == propertyId)
{
index = iLo;
return true;
}
if (checkIndex > propertyId)
{
// we've gone past the targetIndex - return not found
break;
}
iLo++;
} while (iLo < iHi);
index = iLo;
return false;
} }
[MemberNotNull(nameof(_entries))] [MemberNotNull(nameof(_entries))]
@ -327,18 +345,30 @@ namespace Avalonia.Utilities
entryIndex + 1, entryIndex + 1,
_entryCount - entryIndex); _entryCount - entryIndex);
_entries[entryIndex] = entry; UnsafeGetEntryRef(entryIndex) = entry;
} }
} }
else else
{ {
_entries ??= new Entry[DefaultInitialCapacity]; _entries ??= new Entry[DefaultInitialCapacity];
_entries[0] = entry; UnsafeGetEntryRef(0) = entry;
} }
_entryCount++; _entryCount++;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref Entry UnsafeGetEntryRef(int index)
{
#if NET6_0_OR_GREATER && !DEBUG
// This type is performance critical: in release mode, skip any bound check the JIT compiler couldn't elide.
// The index parameter should always be correct when calling this method: no unchecked user input should get here.
return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_entries!), (uint)index);
#else
return ref _entries![index];
#endif
}
[DoesNotReturn] [DoesNotReturn]
private static void ThrowOutOfRange() => throw new IndexOutOfRangeException(); private static void ThrowOutOfRange() => throw new IndexOutOfRangeException();
@ -351,12 +381,12 @@ namespace Avalonia.Utilities
private readonly struct Entry private readonly struct Entry
{ {
public readonly AvaloniaProperty Property; public readonly int Id;
public readonly TValue Value; public readonly TValue Value;
public Entry(AvaloniaProperty property, TValue value) public Entry(AvaloniaProperty property, TValue value)
{ {
Property = property; Id = property.Id;
Value = value; Value = value;
} }
} }

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@ -63,7 +63,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
AvaloniaObject target, AvaloniaObject target,
object? anchor) object? anchor)
{ {
var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false; var enableDataValidation = targetProperty.GetMetadata(target).EnableDataValidation ?? false;
return InstanceCore(target, targetProperty, anchor, enableDataValidation); return InstanceCore(target, targetProperty, anchor, enableDataValidation);
} }

2
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -77,7 +77,7 @@ namespace Avalonia.Data
AvaloniaObject target, AvaloniaObject target,
object? anchor) object? anchor)
{ {
var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false; var enableDataValidation = targetProperty.GetMetadata(target).EnableDataValidation ?? false;
return InstanceCore(targetProperty, target, anchor, enableDataValidation); return InstanceCore(targetProperty, target, anchor, enableDataValidation);
} }

2
src/Markup/Avalonia.Markup/Data/BindingBase.cs

@ -109,7 +109,7 @@ namespace Avalonia.Data
if (mode == BindingMode.Default) if (mode == BindingMode.Default)
{ {
if (targetProperty?.GetMetadata(target.GetType()) is { } metadata) if (targetProperty?.GetMetadata(target) is { } metadata)
mode = metadata.DefaultBindingMode; mode = metadata.DefaultBindingMode;
else else
mode = BindingMode.OneWay; mode = BindingMode.OneWay;

2
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -75,7 +75,7 @@ namespace Avalonia.Data
{ {
var input = InstanceCore(target, targetProperty); var input = InstanceCore(target, targetProperty);
var mode = Mode == BindingMode.Default ? var mode = Mode == BindingMode.Default ?
targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; targetProperty?.GetMetadata(target).DefaultBindingMode : Mode;
switch (mode) switch (mode)
{ {

2
src/Shared/StringCompatibilityExtensions.cs

@ -7,7 +7,7 @@ internal static class StringCompatibilityExtensions
{ {
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Contains(this string str, char search) => public static bool Contains(this string str, char search) =>
str.Contains(search.ToString()); str.IndexOf(search) >= 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EndsWith(this string str, char search) => public static bool EndsWith(this string str, char search) =>

9
tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs

@ -161,7 +161,7 @@ namespace Avalonia.Base.UnitTests.Utilities
[Theory] [Theory]
[MemberData(nameof(Counts))] [MemberData(nameof(Counts))]
public void GetKeyValue_Finds_Value(int count) public void GetValue_Finds_Value(int count)
{ {
if (count == 0) if (count == 0)
return; return;
@ -169,20 +169,19 @@ namespace Avalonia.Base.UnitTests.Utilities
var target = CreateTarget(count); var target = CreateTarget(count);
var index = count / 2; var index = count / 2;
target.GetKeyValue(index, out var property, out var value); var value = target.GetValue(index);
Assert.NotNull(property);
Assert.NotNull(value); Assert.NotNull(value);
} }
[Theory] [Theory]
[MemberData(nameof(Counts))] [MemberData(nameof(Counts))]
public void GetKeyValue_Throws_If_Index_Out_Of_Range(int count) public void GetValue_Throws_If_Index_Out_Of_Range(int count)
{ {
var target = CreateTarget(count); var target = CreateTarget(count);
var index = count; var index = count;
Assert.Throws<IndexOutOfRangeException>(() => target.GetKeyValue(index, out var _, out var _)); Assert.Throws<IndexOutOfRangeException>(() => target.GetValue(index));
} }
[Theory] [Theory]

2
tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs

@ -14,7 +14,7 @@ namespace Avalonia.Benchmarks
for (int j = 0; j < innerCount; ++j) for (int j = 0; j < innerCount; ++j)
{ {
var child = new Button(); var child = new Button() { Width = 100, Height = 50 };
parent.Children.Add(child); parent.Children.Add(child);

Loading…
Cancel
Save