diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 28fa4d5d0c9..37457ad7c34 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -9,8 +9,8 @@ using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.PropertyStore; -using Avalonia.Reactive; using Avalonia.Threading; +using Avalonia.Utilities; namespace Avalonia { @@ -126,7 +126,7 @@ public IBinding this[IndexerDescriptor binding] /// The property. public void ClearValue(AvaloniaProperty property) { - _ = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); _values.ClearValue(property); } @@ -137,7 +137,7 @@ public void ClearValue(AvaloniaProperty property) /// The property. public void ClearValue(AvaloniaProperty property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); switch (property) @@ -159,7 +159,7 @@ public void ClearValue(AvaloniaProperty property) /// The property. public void ClearValue(StyledProperty property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); _values.ClearValue(property); @@ -171,11 +171,11 @@ public void ClearValue(StyledProperty property) /// The property. public void ClearValue(DirectPropertyBase property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); - p.InvokeSetter(this, p.GetUnsetValue(GetType())); + p.InvokeSetter(this, p.GetUnsetValue(this)); } /// @@ -216,7 +216,7 @@ public void ClearValue(DirectPropertyBase property) /// The value. public object? GetValue(AvaloniaProperty property) { - _ = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); if (property.IsDirect) return property.RouteGetValue(this); @@ -232,7 +232,7 @@ public void ClearValue(DirectPropertyBase property) /// The value. public T GetValue(StyledProperty property) { - _ = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); return _values.GetValue(property); } @@ -245,7 +245,7 @@ public T GetValue(StyledProperty property) /// The value. public T GetValue(DirectPropertyBase property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); @@ -262,7 +262,7 @@ public T GetValue(DirectPropertyBase property) /// public Optional GetBaseValue(StyledProperty property) { - _ = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); return _values.GetBaseValue(property); } @@ -274,7 +274,7 @@ public Optional GetBaseValue(StyledProperty property) /// True if the property is animating, otherwise false. public bool IsAnimating(AvaloniaProperty property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); @@ -292,7 +292,7 @@ public bool IsAnimating(AvaloniaProperty property) /// public bool IsSet(AvaloniaProperty property) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); @@ -310,7 +310,7 @@ public bool IsSet(AvaloniaProperty property) object? value, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); return property.RouteSetValue(this, value, priority); } @@ -330,7 +330,7 @@ public bool IsSet(AvaloniaProperty property) T value, BindingPriority priority = BindingPriority.LocalValue) { - _ = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); ValidatePriority(priority); @@ -357,7 +357,7 @@ public bool IsSet(AvaloniaProperty property) /// The value. public void SetValue(DirectPropertyBase property, T value) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); @@ -401,7 +401,7 @@ public void SetCurrentValue(AvaloniaProperty property, object? value) => /// public void SetCurrentValue(StyledProperty property, T value) { - _ = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); LogPropertySet(property, value, BindingPriority.LocalValue); @@ -458,8 +458,8 @@ public IDisposable Bind( IObservable source, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); + ThrowHelper.ThrowIfNull(property, nameof(property)); + ThrowHelper.ThrowIfNull(source, nameof(source)); VerifyAccess(); ValidatePriority(priority); @@ -495,8 +495,8 @@ public IDisposable Bind( IObservable source, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); + ThrowHelper.ThrowIfNull(property, nameof(property)); + ThrowHelper.ThrowIfNull(source, nameof(source)); VerifyAccess(); ValidatePriority(priority); @@ -518,8 +518,8 @@ public IDisposable Bind( IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); + ThrowHelper.ThrowIfNull(property, nameof(property)); + ThrowHelper.ThrowIfNull(source, nameof(source)); VerifyAccess(); ValidatePriority(priority); @@ -539,7 +539,7 @@ public IDisposable Bind( DirectPropertyBase property, IObservable source) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); @@ -574,7 +574,7 @@ public IDisposable Bind( DirectPropertyBase property, IObservable source) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); @@ -600,7 +600,7 @@ public IDisposable Bind( DirectPropertyBase property, IObservable> source) { - property = property ?? throw new ArgumentNullException(nameof(property)); + ThrowHelper.ThrowIfNull(property, nameof(property)); VerifyAccess(); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); @@ -796,7 +796,7 @@ internal void SetDirectValueUnchecked(DirectPropertyBase property, T value { if (value is UnsetValueType) { - property.InvokeSetter(this, property.GetUnsetValue(GetType())); + property.InvokeSetter(this, property.GetUnsetValue(this)); } else if (!(value is DoNothingType)) { @@ -815,7 +815,7 @@ internal void SetDirectValueUnchecked(DirectPropertyBase property, Binding { case BindingValueType.UnsetValue: 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); break; case BindingValueType.DataValidationError: @@ -828,7 +828,7 @@ internal void SetDirectValueUnchecked(DirectPropertyBase property, Binding break; } - var metadata = property.GetMetadata(GetType()); + var metadata = property.GetMetadata(this); if (metadata.EnableDataValidation == true) { diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index f6d6e352c9f..3af338d740f 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.PropertyStore; @@ -28,10 +29,11 @@ public abstract class AvaloniaProperty : IEquatable, IProperty /// /// Provides a fast path when the property has no metadata overrides. /// - private KeyValuePair? _singleMetadata; + private Type? _singleHostType; + private AvaloniaPropertyMetadata? _singleMetadata; - private readonly Dictionary _metadata; - private readonly Dictionary _metadataCache = new Dictionary(); + private readonly Dictionary _metadata = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _metadataCache = new(ReferenceEqualityComparer.Instance); /// /// Initializes a new instance of the class. @@ -60,8 +62,6 @@ private protected AvaloniaProperty( throw new ArgumentException("'name' may not contain periods."); } - _metadata = new Dictionary(); - Name = name; PropertyType = valueType; OwnerType = ownerType; @@ -71,7 +71,8 @@ private protected AvaloniaProperty( metadata.Freeze(); _metadata.Add(hostType, metadata); _defaultMetadata = metadata.GenerateTypeSafeMetadata(); - _singleMetadata = new(hostType, metadata); + _singleHostType = hostType; + _singleMetadata = metadata; } /// @@ -85,8 +86,6 @@ private protected AvaloniaProperty( Type ownerType, AvaloniaPropertyMetadata? metadata) { - _metadata = new Dictionary(); - Name = source?.Name ?? throw new ArgumentNullException(nameof(source)); PropertyType = source.PropertyType; OwnerType = ownerType ?? throw new ArgumentNullException(nameof(ownerType)); @@ -474,11 +473,39 @@ public override int GetHashCode() /// Gets the which applies to this property when it is used with the specified type. /// /// The type for which to retrieve metadata. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public AvaloniaPropertyMetadata GetMetadata() where T : AvaloniaObject => GetMetadata(typeof(T)); /// /// The type for which to retrieve metadata. - public AvaloniaPropertyMetadata GetMetadata(Type type) => GetMetadataWithOverrides(type); + /// + /// For performance, prefer the overload when possible. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public AvaloniaPropertyMetadata GetMetadata(Type type) + { + if (_singleMetadata == _defaultMetadata) + { + return _defaultMetadata; + } + + return GetMetadataFromCache(type); + } + + /// + /// Gets the which applies to this property when it is used with the specified object. + /// + /// The object for which to retrieve metadata. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public AvaloniaPropertyMetadata GetMetadata(AvaloniaObject owner) + { + if (_singleMetadata == _defaultMetadata) + { + return _defaultMetadata; + } + + return GetMetadataFromCache(owner); + } /// /// Checks whether the is valid for the property. @@ -593,42 +620,65 @@ private protected void OverrideMetadata(Type type, AvaloniaPropertyMetadata meta _metadataCache.Clear(); _singleMetadata = null; + _singleHostType = null; } private protected abstract IObservable 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; - 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; } currentType = currentType.BaseType; } - return _metadataCache[type] = _defaultMetadata; + return _defaultMetadata; } bool IPropertyInfo.CanGet => true; diff --git a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs index 2aec72aaf70..b28a93d7ee4 100644 --- a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs +++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs @@ -8,7 +8,6 @@ namespace Avalonia /// public abstract class AvaloniaPropertyMetadata { - private bool _isReadOnly; private BindingMode _defaultBindingMode; /// @@ -47,6 +46,11 @@ public BindingMode DefaultBindingMode /// public bool? EnableDataValidation { get; private set; } + /// + /// Gets whether this instance is read-only and can't be modified. + /// + public bool IsReadOnly { get; private set; } + /// /// Merges the metadata with the base metadata. /// @@ -56,7 +60,7 @@ public virtual void Merge( AvaloniaPropertyMetadata baseMetadata, AvaloniaProperty property) { - if (_isReadOnly) + if (IsReadOnly) { throw new InvalidOperationException("The metadata is read-only."); } @@ -74,7 +78,7 @@ public virtual void Merge( /// No further modifications are allowed after this call. /// public void Freeze() - => _isReadOnly = true; + => IsReadOnly = true; /// /// Gets a copy of this object configured for use with any owner type. diff --git a/src/Avalonia.Base/Compatibility/ReferenceEqualityComparer.cs b/src/Avalonia.Base/Compatibility/ReferenceEqualityComparer.cs new file mode 100644 index 00000000000..72a4f887a56 --- /dev/null +++ b/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, 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 diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 23a3f532aca..9170fbfaa06 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -34,7 +34,7 @@ public static IDisposable Apply( if (mode == BindingMode.Default) { - mode = property.GetMetadata(target.GetType()).DefaultBindingMode; + mode = property.GetMetadata(target).DefaultBindingMode; } switch (mode) diff --git a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs index 20686c3546d..9b6e260f957 100644 --- a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs +++ b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs @@ -532,9 +532,9 @@ protected bool TryGetTarget([NotNullWhen(true)] out AvaloniaObject? target) if (TargetProperty is not null && _target?.TryGetTarget(out var target) == true) { if (TargetProperty.IsDirect) - _defaultValue = ((IDirectPropertyAccessor)TargetProperty).GetUnsetValue(target.GetType()); + _defaultValue = ((IDirectPropertyAccessor)TargetProperty).GetUnsetValue(target); else - _defaultValue = ((IStyledPropertyAccessor)TargetProperty).GetDefaultValue(target.GetType()); + _defaultValue = ((IStyledPropertyAccessor)TargetProperty).GetDefaultValue(target); _isDefaultValueInitialized = true; return _defaultValue; diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index 513222d4272..2f989a5ed97 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Avalonia.Data { @@ -67,7 +68,7 @@ public Optional(T value) /// Gets the value if present, otherwise the default value. /// /// The value. - public T? GetValueOrDefault() => HasValue ? _value : default; + public T? GetValueOrDefault() => _value; /// /// Gets the value if present, otherwise a default value. diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 720b21c4844..3efe003710a 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -97,7 +97,7 @@ public DirectProperty AddOwner( metadata.Freeze(); var result = new DirectProperty( - (DirectPropertyBase)this, + this, getter, setter, metadata); @@ -144,9 +144,9 @@ void IDirectPropertyAccessor.SetValue(AvaloniaObject instance, object? value) } object? IDirectPropertyAccessor.GetUnsetValue(Type type) - { - var metadata = GetMetadata(type); - return metadata.UnsetValue; - } + => GetMetadata(type).UnsetValue; + + object? IDirectPropertyAccessor.GetUnsetValue(AvaloniaObject owner) + => GetMetadata(owner).UnsetValue; } } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 64eb4d2615f..b6b99671779 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.PropertyStore; @@ -70,21 +71,37 @@ private protected DirectPropertyBase( /// The type. /// The unset value. public TValue GetUnsetValue(Type type) - { - type = type ?? throw new ArgumentNullException(nameof(type)); - return GetMetadata(type).UnsetValue; - } + => GetMetadata(type).UnsetValue; /// - /// Gets the property metadata for the specified type. + /// Gets the unset value for the property on the specified object. /// - /// The type. - /// - /// The property metadata. - /// + /// The object. + /// The unset value. + public TValue GetUnsetValue(AvaloniaObject owner) + => GetMetadata(owner).UnsetValue; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new DirectPropertyMetadata GetMetadata(Type type) + => CastMetadata(base.GetMetadata(type)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new DirectPropertyMetadata GetMetadata(AvaloniaObject owner) + => CastMetadata(base.GetMetadata(owner)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static DirectPropertyMetadata CastMetadata(AvaloniaPropertyMetadata metadata) { - return (DirectPropertyMetadata)base.GetMetadata(type); +#if DEBUG + return (DirectPropertyMetadata)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 stored for a DirectPropertyBase. + return Unsafe.As>(metadata); +#endif } /// diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 2c653f7481b..dd0ecb84c5b 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -49,6 +49,11 @@ public override void Merge(AvaloniaPropertyMetadata baseMetadata, AvaloniaProper /// public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() { + if (IsReadOnly) + { + return this; + } + var copy = new DirectPropertyMetadata(UnsetValue, DefaultBindingMode, EnableDataValidation); copy.Freeze(); return copy; diff --git a/src/Avalonia.Base/IDirectPropertyAccessor.cs b/src/Avalonia.Base/IDirectPropertyAccessor.cs index f92ca1fbf8f..5eeffb42044 100644 --- a/src/Avalonia.Base/IDirectPropertyAccessor.cs +++ b/src/Avalonia.Base/IDirectPropertyAccessor.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Metadata; namespace Avalonia { @@ -38,5 +37,11 @@ internal interface IDirectPropertyAccessor /// /// The type. object? GetUnsetValue(Type type); + + /// + /// Gets the unset value of the property for the specified object. + /// + /// The object. + object? GetUnsetValue(AvaloniaObject owner); } } diff --git a/src/Avalonia.Base/IStyledPropertyAccessor.cs b/src/Avalonia.Base/IStyledPropertyAccessor.cs index 4cbfd7b759b..821e19dcd00 100644 --- a/src/Avalonia.Base/IStyledPropertyAccessor.cs +++ b/src/Avalonia.Base/IStyledPropertyAccessor.cs @@ -16,6 +16,15 @@ internal interface IStyledPropertyAccessor /// object? GetDefaultValue(Type type); + /// + /// Gets the default value for the property for the specified object. + /// + /// The object. + /// + /// The default value. + /// + object? GetDefaultValue(AvaloniaObject owner); + /// /// Validates the specified property value. /// diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index dc2f19d3895..294313a15e4 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -45,7 +45,7 @@ private BindingEntryBase( Frame = frame; Property = property; Source = source; - if (property.GetMetadata(target.GetType()).EnableDataValidation == true) + if (property.GetMetadata(target).EnableDataValidation == true) _uncommon = new() { _hasDataValidation = true }; } @@ -112,7 +112,7 @@ public virtual void Unsubscribe() protected abstract BindingValue ConvertAndValidate(TSource value); protected abstract BindingValue ConvertAndValidate(BindingValue value); - protected abstract TValue GetDefaultValue(Type ownerType); + protected abstract TValue GetDefaultValue(AvaloniaObject owner); protected virtual void Start(bool produceValue) { @@ -186,7 +186,7 @@ private TValue GetCachedDefaultValue() if (_uncommon?._isDefaultValueInitialized != true) { _uncommon ??= new(); - _uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); + _uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner); _uncommon._isDefaultValueInitialized = true; } diff --git a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs index 4bf98e3f7b9..fdab9668051 100644 --- a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs @@ -15,7 +15,7 @@ internal class DirectBindingObserver : IObserver, public DirectBindingObserver(ValueStore owner, DirectPropertyBase property) { _owner = owner; - _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false; + _hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false; Property = property; } diff --git a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs index 54dffa3a95a..e1db0b19aeb 100644 --- a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs @@ -16,7 +16,7 @@ internal class DirectUntypedBindingObserver : IObserver, public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase property) { _owner = owner; - _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false; + _hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false; Property = property; } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 7b7094686d1..7421b183d48 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -11,6 +11,11 @@ namespace Avalonia.PropertyStore /// internal abstract class EffectiveValue { + /// + /// Gets the property targeted by this value. + /// + public AvaloniaProperty Property { get; protected init; } + /// /// Gets the current effective value as a boxed value. /// @@ -53,6 +58,13 @@ internal abstract class EffectiveValue /// public bool IsCoercedDefaultValue { get; set; } + /// + /// Initializes a new instance of . + /// + /// The property targeted by this value. + protected EffectiveValue(AvaloniaProperty property) + => Property = property; + /// /// Begins a reevaluation pass on the effective value. /// diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 3216273b89d..b50935692e3 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -23,10 +23,11 @@ public EffectiveValue( AvaloniaObject owner, StyledProperty property, EffectiveValue? inherited) + : base(property) { Priority = BindingPriority.Unset; BasePriority = BindingPriority.Unset; - _metadata = property.GetMetadata(owner.GetType()); + _metadata = property.GetMetadata(owner); var value = inherited is null ? _metadata.DefaultValue : inherited.Value; diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs index 084ac7011f0..29cc891bb9f 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs @@ -18,7 +18,7 @@ protected LocalValueBindingObserverBase(ValueStore owner, StyledProperty prop { _owner = owner; Property = property; - _hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false; + _hasDataValidation = property.GetMetadata(owner.Owner).EnableDataValidation ?? false; } public StyledProperty Property { get;} @@ -121,7 +121,7 @@ private T GetCachedDefaultValue() { if (!_isDefaultValueInitialized) { - _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _defaultValue = Property.GetDefaultValue(_owner.Owner); _isDefaultValueInitialized = true; } diff --git a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs index 99c6a3ee9de..ca7e483b359 100644 --- a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs @@ -33,6 +33,6 @@ protected override BindingValue ConvertAndValidate(BindingValue Property.GetDefaultValue(ownerType); + protected override TTarget GetDefaultValue(AvaloniaObject owner) => Property.GetDefaultValue(owner); } } diff --git a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs index c209138605d..a55202c9f2a 100644 --- a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs @@ -51,6 +51,6 @@ protected override BindingValue ConvertAndValidate(BindingValue value) return value; } - protected override T GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); + protected override T GetDefaultValue(AvaloniaObject owner) => Property.GetDefaultValue(owner); } } diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index d241819fa78..35406993c71 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Diagnostics; @@ -283,11 +284,25 @@ public void SetLocalValue(StyledProperty property, T value) public T GetValue(StyledProperty property) { + // Performance critical method if (_effectiveValues.TryGetValue(property, out var v)) - return ((EffectiveValue)v).Value; + return CastEffectiveValue(v).Value; if (property.Inherits && TryGetInheritedValue(property, out v)) - return ((EffectiveValue)v).Value; - return property.GetDefaultValue(Owner.GetType()); + return CastEffectiveValue(v).Value; + return property.GetDefaultValue(Owner); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static EffectiveValue CastEffectiveValue(EffectiveValue value) + { +#if DEBUG + return (EffectiveValue)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 stored for a T property. + return Unsafe.As>(value); +#endif } public bool IsAnimating(AvaloniaProperty property) @@ -309,7 +324,7 @@ public void CoerceValue(AvaloniaProperty property) public void CoerceDefaultValue(StyledProperty property) { - var metadata = property.GetMetadata(Owner.GetType()); + var metadata = property.GetMetadata(Owner); if (metadata.CoerceValue is null) return; @@ -388,9 +403,9 @@ public void SetInheritanceParent(AvaloniaObject? newParent) for (var i = 0; i < count; ++i) { - f._effectiveValues.GetKeyValue(i, out var key, out var value); - if (key.Inherits) - values.TryAdd(key, new(value)); + var value = f._effectiveValues.GetValue(i); + if (value.Property.Inherits) + values.TryAdd(value.Property, new(value)); } f = f.InheritanceAncestor; @@ -405,19 +420,20 @@ public void SetInheritanceParent(AvaloniaObject? newParent) 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; - if (values.TryGetValue(key, out var existing)) + if (values.TryGetValue(property, out var existing)) { if (existing.NewValue is null) - values[key] = existing.WithNewValue(value); + values[property] = existing.WithNewValue(value); } else { - values.Add(key, new(null, value)); + values.Add(property, new(null, value)); } } @@ -431,12 +447,15 @@ public void SetInheritanceParent(AvaloniaObject? newParent) var count = values.Count; 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 newValue = v.NewValue; if (oldValue != newValue) - InheritedValueChanged(key, oldValue, newValue); + { + var property = v.OldValue?.Property ?? v.NewValue!.Property; + InheritedValueChanged(property, oldValue, newValue); + } } } @@ -1077,14 +1096,15 @@ private void ReevaluateEffectiveValues(IValueEntry? changedValueEntry = null) // Remove all effective values that are still unset. 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()) { - RemoveEffectiveValue(key, i); - e.DisposeAndRaiseUnset(this, key); + RemoveEffectiveValue(property, i); + e.DisposeAndRaiseUnset(this, property); if (i > _effectiveValues.Count) break; @@ -1134,10 +1154,7 @@ private bool TryGetEffectiveValue( AvaloniaProperty property, [NotNullWhen(true)] out EffectiveValue? value) { - if (_effectiveValues.TryGetValue(property, out value)) - return true; - value = null; - return false; + return _effectiveValues.TryGetValue(property, out value); } private EffectiveValue? GetEffectiveValue(AvaloniaProperty property) @@ -1149,7 +1166,7 @@ private bool TryGetEffectiveValue( private object? GetDefaultValue(AvaloniaProperty property) { - return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType()); + return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner); } private void DisposeExistingLocalValueBinding(AvaloniaProperty property) diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index df12b7099dc..a008ba4092f 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Avalonia.Data; -using Avalonia.Data.Core; using Avalonia.PropertyStore; using Avalonia.Utilities; @@ -12,6 +12,10 @@ namespace Avalonia /// public class StyledProperty : AvaloniaProperty, 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 _singleDefaultValue; + /// /// Initializes a new instance of the class. /// @@ -41,8 +45,11 @@ internal StyledProperty( if (validate?.Invoke(metadata.DefaultValue) == false) { 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; } /// @@ -68,7 +75,7 @@ public StyledProperty AddOwner(StyledPropertyMetadata? m public TValue CoerceValue(AvaloniaObject instance, TValue baseValue) { - var metadata = GetMetadata(instance.GetType()); + var metadata = GetMetadata(instance); if (metadata.CoerceValue != null) { @@ -83,22 +90,51 @@ public TValue CoerceValue(AvaloniaObject instance, TValue baseValue) /// /// The type. /// The default value. + /// + /// For performance, prefer the overload when possible. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue GetDefaultValue(Type type) { - return GetMetadata(type).DefaultValue; + return _singleDefaultValue.HasValue ? + _singleDefaultValue.GetValueOrDefault()! : + GetMetadata(type).DefaultValue; } /// - /// Gets the property metadata for the specified type. + /// Gets the default value for the property on the specified object. /// - /// The type. - /// - /// The property metadata. - /// + /// The object. + /// The default value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue GetDefaultValue(AvaloniaObject owner) + { + return _singleDefaultValue.HasValue ? + _singleDefaultValue.GetValueOrDefault()! : + GetMetadata(owner).DefaultValue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new StyledPropertyMetadata GetMetadata(Type type) + => CastMetadata(base.GetMetadata(type)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new StyledPropertyMetadata GetMetadata(AvaloniaObject owner) + => CastMetadata(base.GetMetadata(owner)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static StyledPropertyMetadata CastMetadata(AvaloniaPropertyMetadata metadata) { - _ = type ?? throw new ArgumentNullException(nameof(type)); - return (StyledPropertyMetadata)base.GetMetadata(type); +#if DEBUG + return (StyledPropertyMetadata)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 stored for a StyledProperty. + return Unsafe.As>(metadata); +#endif } /// @@ -145,6 +181,11 @@ public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) } base.OverrideMetadata(type, metadata); + + if (_singleDefaultValue != metadata.DefaultValue) + { + _singleDefaultValue = default; + } } /// @@ -156,8 +197,9 @@ public override string ToString() return Name; } - /// - object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type); + + object? IStyledPropertyAccessor.GetDefaultValue(AvaloniaObject owner) => GetDefaultValue(owner); bool IStyledPropertyAccessor.ValidateValue(object? value) { @@ -253,11 +295,5 @@ private bool ShouldSetValue(AvaloniaObject target, object? value, [NotNullWhen(t 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; - } } } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 57207e46a09..6f0892e5430 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using Avalonia.Data; namespace Avalonia @@ -31,7 +32,11 @@ public StyledPropertyMetadata( /// /// Gets the default value for the property. /// - public TValue DefaultValue => _defaultValue.GetValueOrDefault()!; + public TValue DefaultValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _defaultValue.GetValueOrDefault()!; + } /// /// Gets the value coercion callback, if any. @@ -62,6 +67,11 @@ public override void Merge(AvaloniaPropertyMetadata baseMetadata, AvaloniaProper /// public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() { + if (IsReadOnly && CoerceValue is null) + { + return this; + } + var copy = new StyledPropertyMetadata(DefaultValue, DefaultBindingMode, null, EnableDataValidation ?? false); copy.Freeze(); return copy; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs index 0589abb2dd0..ab34e852208 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Avalonia.Utilities { @@ -52,7 +54,7 @@ public AvaloniaPropertyDictionary(int capactity) /// /// The key to get or set. /// - /// 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 , and a set operation creates a /// new element for the specified key. /// @@ -63,16 +65,18 @@ public TValue this[AvaloniaProperty property] { get { - if (!TryGetEntry(property.Id, out var index)) + var index = FindEntry(property.Id); + if (index < 0) ThrowNotFound(); - return _entries[index].Value; + return UnsafeGetEntryRef(index).Value; } set { - if (TryGetEntry(property.Id, out var index)) - _entries[index] = new Entry(property, value); + var index = FindEntry(property.Id); + if (index >= 0) + UnsafeGetEntryRef(index) = new Entry(property, value); else - InsertEntry(new Entry(property, value), index); + InsertEntry(new Entry(property, value), ~index); } } @@ -88,7 +92,7 @@ public TValue this[int index] { if (index >= _entryCount) ThrowOutOfRange(); - return _entries![index].Value; + return UnsafeGetEntryRef(index).Value; } } @@ -99,11 +103,12 @@ public TValue this[int index] /// The value of the element to add. public void Add(AvaloniaProperty property, TValue value) { - if (TryGetEntry(property.Id, out var index)) + var index = FindEntry(property.Id); + if (index >= 0) ThrowDuplicate(); - InsertEntry(new Entry(property, value), index); + InsertEntry(new Entry(property, value), ~index); } - + /// /// Removes all keys and values from the collection. /// @@ -124,27 +129,19 @@ public void Clear() /// Determines whether the collection contains the specified key. /// /// The key. - public bool ContainsKey(AvaloniaProperty property) => TryGetEntry(property.Id, out _); + public bool ContainsKey(AvaloniaProperty property) => FindEntry(property.Id) >= 0; /// - /// Gets the key and value at the specified index. + /// Gets value at the specified index. /// - /// - /// The index of the entry, between 0 and - 1. - /// - /// - /// When this method returns, contains the key at the specified index. - /// - /// - /// When this method returns, contains the value at the specified index. - /// - public void GetKeyValue(int index, out AvaloniaProperty key, out TValue value) + /// The index of the entry, between 0 and - 1. + /// The value at the specified index. + public TValue GetValue(int index) { if (index >= _entryCount) ThrowOutOfRange(); - ref var entry = ref _entries![index]; - key = entry.Property; - value = entry.Value; + ref var entry = ref UnsafeGetEntryRef(index); + return entry.Value; } /// @@ -157,7 +154,8 @@ public void GetKeyValue(int index, out AvaloniaProperty key, out TValue value) /// public bool Remove(AvaloniaProperty property) { - if (TryGetEntry(property.Id, out var index)) + var index = FindEntry(property.Id); + if (index >= 0) { RemoveAt(index); return true; @@ -178,9 +176,10 @@ public bool Remove(AvaloniaProperty property) /// 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); return true; } @@ -196,11 +195,11 @@ public bool Remove(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue public void RemoveAt(int index) { if (_entries is null) - throw new IndexOutOfRangeException(); + ThrowOutOfRange(); Array.Copy(_entries, index + 1, _entries, index, _entryCount - index - 1); _entryCount--; - _entries[_entryCount] = default; + UnsafeGetEntryRef(_entryCount) = default; } /// @@ -211,9 +210,10 @@ public void RemoveAt(int index) /// public bool TryAdd(AvaloniaProperty property, TValue value) { - if (TryGetEntry(property.Id, out var index)) + var index = FindEntry(property.Id); + if (index >= 0) return false; - InsertEntry(new Entry(property, value), index); + InsertEntry(new Entry(property, value), ~index); return true; } @@ -228,73 +228,91 @@ public bool TryAdd(AvaloniaProperty property, 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; - return true; + var propertyId = property.Id; + 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; return false; } - [MemberNotNullWhen(true, nameof(_entries))] - private bool TryGetEntry(int propertyId, out int index) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindEntry(int propertyId) { - int checkIndex; - int iLo = 0; - int iHi = _entryCount; + var lo = 0; + var hi = _entryCount - 1; - if (iHi <= 0) + if (hi >= 0) { - index = 0; - return false; - } + ref var entry0 = ref UnsafeGetEntryRef(0); - // Do a binary search to find the value - while (iHi - iLo > 3) - { - int iPv = (iHi + iLo) / 2; - checkIndex = _entries![iPv].Property.Id; - - if (propertyId == checkIndex) + do { - index = iPv; - return true; - } - - if (propertyId <= checkIndex) - { - iHi = iPv; - } - else - { - iLo = iPv + 1; - } + // 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) + { + 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 - 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; + return ~lo; } [MemberNotNull(nameof(_entries))] @@ -327,18 +345,30 @@ private void InsertEntry(Entry entry, int entryIndex) entryIndex + 1, _entryCount - entryIndex); - _entries[entryIndex] = entry; + UnsafeGetEntryRef(entryIndex) = entry; } } else { _entries ??= new Entry[DefaultInitialCapacity]; - _entries[0] = entry; + UnsafeGetEntryRef(0) = entry; } _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] private static void ThrowOutOfRange() => throw new IndexOutOfRangeException(); @@ -351,12 +381,12 @@ private static void ThrowDuplicate() => private readonly struct Entry { - public readonly AvaloniaProperty Property; + public readonly int Id; public readonly TValue Value; public Entry(AvaloniaProperty property, TValue value) { - Property = property; + Id = property.Id; Value = value; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index cc6e44f4de1..357c949d117 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -63,7 +63,7 @@ private protected override BindingExpressionBase Instance( AvaloniaObject target, object? anchor) { - var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false; + var enableDataValidation = targetProperty.GetMetadata(target).EnableDataValidation ?? false; return InstanceCore(target, targetProperty, anchor, enableDataValidation); } diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index cdfc25d5549..e089da64912 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -77,7 +77,7 @@ private protected override BindingExpressionBase Instance( AvaloniaObject target, object? anchor) { - var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false; + var enableDataValidation = targetProperty.GetMetadata(target).EnableDataValidation ?? false; return InstanceCore(targetProperty, target, anchor, enableDataValidation); } diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 21cb9a942e6..be106ef5b49 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -109,7 +109,7 @@ private protected (BindingMode, UpdateSourceTrigger) ResolveDefaultsFromMetadata if (mode == BindingMode.Default) { - if (targetProperty?.GetMetadata(target.GetType()) is { } metadata) + if (targetProperty?.GetMetadata(target) is { } metadata) mode = metadata.DefaultBindingMode; else mode = BindingMode.OneWay; diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 044dfb4a4df..5128232dc78 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -75,7 +75,7 @@ public MultiBinding() { var input = InstanceCore(target, targetProperty); var mode = Mode == BindingMode.Default ? - targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; + targetProperty?.GetMetadata(target).DefaultBindingMode : Mode; switch (mode) { diff --git a/src/Shared/StringCompatibilityExtensions.cs b/src/Shared/StringCompatibilityExtensions.cs index dc694e6af59..8e3956f820b 100644 --- a/src/Shared/StringCompatibilityExtensions.cs +++ b/src/Shared/StringCompatibilityExtensions.cs @@ -7,7 +7,7 @@ internal static class StringCompatibilityExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Contains(this string str, char search) => - str.Contains(search.ToString()); + str.IndexOf(search) >= 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EndsWith(this string str, char search) => diff --git a/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs index 4b4aeb4344c..3467f3f4032 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/AvaloniaPropertyDictionaryTests.cs @@ -161,7 +161,7 @@ public void ContainsKey_Returns_False_If_Value_Does_Not_Exist(int count) [Theory] [MemberData(nameof(Counts))] - public void GetKeyValue_Finds_Value(int count) + public void GetValue_Finds_Value(int count) { if (count == 0) return; @@ -169,20 +169,19 @@ public void GetKeyValue_Finds_Value(int count) var target = CreateTarget(count); var index = count / 2; - target.GetKeyValue(index, out var property, out var value); + var value = target.GetValue(index); - Assert.NotNull(property); Assert.NotNull(value); } [Theory] [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 index = count; - Assert.Throws(() => target.GetKeyValue(index, out var _, out var _)); + Assert.Throws(() => target.GetValue(index)); } [Theory] diff --git a/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs b/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs index a2fc14e7944..7ee25bc1b99 100644 --- a/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs +++ b/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs @@ -14,7 +14,7 @@ public static List CreateChildren(List controls, Panel parent, for (int j = 0; j < innerCount; ++j) { - var child = new Button(); + var child = new Button() { Width = 100, Height = 50 }; parent.Children.Add(child);