Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve AvaloniaObject.GetValue performance #15342

Merged
merged 4 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 28 additions & 28 deletions src/Avalonia.Base/AvaloniaObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.Utilities;

namespace Avalonia
{
Expand Down Expand Up @@ -126,7 +126,7 @@ public IBinding this[IndexerDescriptor binding]
/// <param name="property">The property.</param>
public void ClearValue(AvaloniaProperty property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
_values.ClearValue(property);
}
Expand All @@ -137,7 +137,7 @@ public void ClearValue(AvaloniaProperty property)
/// <param name="property">The property.</param>
public void ClearValue<T>(AvaloniaProperty<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();

switch (property)
Expand All @@ -159,7 +159,7 @@ public void ClearValue<T>(AvaloniaProperty<T> property)
/// <param name="property">The property.</param>
public void ClearValue<T>(StyledProperty<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();

_values.ClearValue(property);
Expand All @@ -171,11 +171,11 @@ public void ClearValue<T>(StyledProperty<T> property)
/// <param name="property">The property.</param>
public void ClearValue<T>(DirectPropertyBase<T> 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));
}

/// <summary>
Expand Down Expand Up @@ -216,7 +216,7 @@ public void ClearValue<T>(DirectPropertyBase<T> property)
/// <returns>The value.</returns>
public object? GetValue(AvaloniaProperty property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));

if (property.IsDirect)
return property.RouteGetValue(this);
Expand All @@ -232,7 +232,7 @@ public void ClearValue<T>(DirectPropertyBase<T> property)
/// <returns>The value.</returns>
public T GetValue<T>(StyledProperty<T> property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
return _values.GetValue(property);
}
Expand All @@ -245,7 +245,7 @@ public T GetValue<T>(StyledProperty<T> property)
/// <returns>The value.</returns>
public T GetValue<T>(DirectPropertyBase<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();

var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
Expand All @@ -262,7 +262,7 @@ public T GetValue<T>(DirectPropertyBase<T> property)
/// </remarks>
public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();
return _values.GetBaseValue(property);
}
Expand All @@ -274,7 +274,7 @@ public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
/// <returns>True if the property is animating, otherwise false.</returns>
public bool IsAnimating(AvaloniaProperty property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));

VerifyAccess();

Expand All @@ -292,7 +292,7 @@ public bool IsAnimating(AvaloniaProperty property)
/// </remarks>
public bool IsSet(AvaloniaProperty property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));

VerifyAccess();

Expand All @@ -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);
}
Expand All @@ -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);

Expand All @@ -357,7 +357,7 @@ public bool IsSet(AvaloniaProperty property)
/// <param name="value">The value.</param>
public void SetValue<T>(DirectPropertyBase<T> property, T value)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();

property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
Expand Down Expand Up @@ -401,7 +401,7 @@ public void SetCurrentValue(AvaloniaProperty property, object? value) =>
/// </remarks>
public void SetCurrentValue<T>(StyledProperty<T> property, T value)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();

LogPropertySet(property, value, BindingPriority.LocalValue);
Expand Down Expand Up @@ -458,8 +458,8 @@ public IDisposable Bind<T>(
IObservable<object?> 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);

Expand Down Expand Up @@ -495,8 +495,8 @@ public IDisposable Bind<T>(
IObservable<T> 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);

Expand All @@ -518,8 +518,8 @@ public IDisposable Bind<T>(
IObservable<BindingValue<T>> 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);

Expand All @@ -539,7 +539,7 @@ public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<object?> source)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();

property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
Expand Down Expand Up @@ -574,7 +574,7 @@ public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<T> source)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();

property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
Expand All @@ -600,7 +600,7 @@ public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
property = property ?? throw new ArgumentNullException(nameof(property));
ThrowHelper.ThrowIfNull(property, nameof(property));
VerifyAccess();

property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
Expand Down Expand Up @@ -796,7 +796,7 @@ internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value
{
if (value is UnsetValueType)
{
property.InvokeSetter(this, property.GetUnsetValue(GetType()));
property.InvokeSetter(this, property.GetUnsetValue(this));
}
else if (!(value is DoNothingType))
{
Expand All @@ -815,7 +815,7 @@ internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> 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:
Expand All @@ -828,7 +828,7 @@ internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, Binding
break;
}

var metadata = property.GetMetadata(GetType());
var metadata = property.GetMetadata(this);

if (metadata.EnableDataValidation == true)
{
Expand Down
92 changes: 71 additions & 21 deletions src/Avalonia.Base/AvaloniaProperty.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -28,10 +29,11 @@ public abstract class AvaloniaProperty : IEquatable<AvaloniaProperty>, IProperty
/// <summary>
/// Provides a fast path when the property has no metadata overrides.
/// </summary>
private KeyValuePair<Type, AvaloniaPropertyMetadata>? _singleMetadata;
private Type? _singleHostType;
private AvaloniaPropertyMetadata? _singleMetadata;

private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadata;
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadataCache = new Dictionary<Type, AvaloniaPropertyMetadata>();
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadata = new(ReferenceEqualityComparer.Instance);
private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadataCache = new(ReferenceEqualityComparer.Instance);

/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
Expand Down Expand Up @@ -60,8 +62,6 @@ private protected AvaloniaProperty(
throw new ArgumentException("'name' may not contain periods.");
}

_metadata = new Dictionary<Type, AvaloniaPropertyMetadata>();

Name = name;
PropertyType = valueType;
OwnerType = ownerType;
Expand All @@ -71,7 +71,8 @@ private protected AvaloniaProperty(
metadata.Freeze();
_metadata.Add(hostType, metadata);
_defaultMetadata = metadata.GenerateTypeSafeMetadata();
_singleMetadata = new(hostType, metadata);
_singleHostType = hostType;
_singleMetadata = metadata;
}

/// <summary>
Expand All @@ -85,8 +86,6 @@ private protected AvaloniaProperty(
Type ownerType,
AvaloniaPropertyMetadata? metadata)
{
_metadata = new Dictionary<Type, AvaloniaPropertyMetadata>();

Name = source?.Name ?? throw new ArgumentNullException(nameof(source));
PropertyType = source.PropertyType;
OwnerType = ownerType ?? throw new ArgumentNullException(nameof(ownerType));
Expand Down Expand Up @@ -474,11 +473,39 @@ public override int GetHashCode()
/// Gets the <see cref="AvaloniaPropertyMetadata"/> which applies to this property when it is used with the specified type.
/// </summary>
/// <typeparam name="T">The type for which to retrieve metadata.</typeparam>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AvaloniaPropertyMetadata GetMetadata<T>() where T : AvaloniaObject => GetMetadata(typeof(T));

/// <inheritdoc cref="GetMetadata{T}"/>
/// <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>
/// Checks whether the <paramref name="value"/> is valid for the property.
Expand Down Expand Up @@ -593,42 +620,65 @@ private protected void OverrideMetadata(Type type, AvaloniaPropertyMetadata meta
_metadataCache.Clear();

_singleMetadata = null;
_singleHostType = null;
}

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;

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;
Expand Down
Loading
Loading