committed by
GitHub
10 changed files with 359 additions and 336 deletions
@ -0,0 +1,150 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Stores values with <see cref="AvaloniaProperty"/> as key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Stored value type.</typeparam>
|
|||
internal sealed class AvaloniaPropertyValueStore<TValue> |
|||
{ |
|||
private Entry[] _entries; |
|||
|
|||
public AvaloniaPropertyValueStore() |
|||
{ |
|||
// The last item in the list is always int.MaxValue
|
|||
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; |
|||
} |
|||
|
|||
private (int, bool) TryFindEntry(int propertyId) |
|||
{ |
|||
if (_entries.Length <= 12) |
|||
{ |
|||
// For small lists, we use an optimized linear search. Since the last item in the list
|
|||
// is always int.MaxValue, we can skip a conditional branch in each iteration.
|
|||
// By unrolling the loop, we can skip another unconditional branch in each iteration.
|
|||
|
|||
if (_entries[0].PropertyId >= propertyId) |
|||
return (0, _entries[0].PropertyId == propertyId); |
|||
if (_entries[1].PropertyId >= propertyId) |
|||
return (1, _entries[1].PropertyId == propertyId); |
|||
if (_entries[2].PropertyId >= propertyId) |
|||
return (2, _entries[2].PropertyId == propertyId); |
|||
if (_entries[3].PropertyId >= propertyId) |
|||
return (3, _entries[3].PropertyId == propertyId); |
|||
if (_entries[4].PropertyId >= propertyId) |
|||
return (4, _entries[4].PropertyId == propertyId); |
|||
if (_entries[5].PropertyId >= propertyId) |
|||
return (5, _entries[5].PropertyId == propertyId); |
|||
if (_entries[6].PropertyId >= propertyId) |
|||
return (6, _entries[6].PropertyId == propertyId); |
|||
if (_entries[7].PropertyId >= propertyId) |
|||
return (7, _entries[7].PropertyId == propertyId); |
|||
if (_entries[8].PropertyId >= propertyId) |
|||
return (8, _entries[8].PropertyId == propertyId); |
|||
if (_entries[9].PropertyId >= propertyId) |
|||
return (9, _entries[9].PropertyId == propertyId); |
|||
if (_entries[10].PropertyId >= propertyId) |
|||
return (10, _entries[10].PropertyId == propertyId); |
|||
} |
|||
else |
|||
{ |
|||
int low = 0; |
|||
int high = _entries.Length; |
|||
int id; |
|||
|
|||
while (high - low > 3) |
|||
{ |
|||
int pivot = (high + low) / 2; |
|||
id = _entries[pivot].PropertyId; |
|||
|
|||
if (propertyId == id) |
|||
return (pivot, true); |
|||
|
|||
if (propertyId <= id) |
|||
high = pivot; |
|||
else |
|||
low = pivot + 1; |
|||
} |
|||
|
|||
do |
|||
{ |
|||
id = _entries[low].PropertyId; |
|||
|
|||
if (id == propertyId) |
|||
return (low, true); |
|||
|
|||
if (id > propertyId) |
|||
break; |
|||
|
|||
++low; |
|||
} |
|||
while (low < high); |
|||
} |
|||
|
|||
return (0, false); |
|||
} |
|||
|
|||
public bool TryGetValue(AvaloniaProperty property, out TValue value) |
|||
{ |
|||
(int index, bool found) = TryFindEntry(property.Id); |
|||
if (!found) |
|||
{ |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
value = _entries[index].Value; |
|||
return true; |
|||
} |
|||
|
|||
public void AddValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
Entry[] entries = new Entry[_entries.Length + 1]; |
|||
|
|||
for (int i = 0; i < _entries.Length; ++i) |
|||
{ |
|||
if (_entries[i].PropertyId > property.Id) |
|||
{ |
|||
if (i > 0) |
|||
{ |
|||
Array.Copy(_entries, 0, entries, 0, i); |
|||
} |
|||
|
|||
entries[i] = new Entry { PropertyId = property.Id, Value = value }; |
|||
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
_entries = entries; |
|||
} |
|||
|
|||
public void SetValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
_entries[TryFindEntry(property.Id).Item1].Value = value; |
|||
} |
|||
|
|||
public Dictionary<AvaloniaProperty, TValue> ToDictionary() |
|||
{ |
|||
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1); |
|||
|
|||
for (int i = 0; i < _entries.Length - 1; ++i) |
|||
{ |
|||
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); |
|||
} |
|||
|
|||
return dict; |
|||
} |
|||
|
|||
private struct Entry |
|||
{ |
|||
internal int PropertyId; |
|||
internal TValue Value; |
|||
} |
|||
} |
|||
} |
|||
@ -1,168 +1,122 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Callback invoked when deferred setter wants to set a value.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Value type.</typeparam>
|
|||
/// <param name="property">Property being set.</param>
|
|||
/// <param name="backing">Backing field reference.</param>
|
|||
/// <param name="value">New value.</param>
|
|||
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value); |
|||
|
|||
/// <summary>
|
|||
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
|
|||
/// Used to fix #855.
|
|||
/// </summary>
|
|||
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
|
|||
class DeferredSetter<TSetRecord> |
|||
internal sealed class DeferredSetter<TSetRecord> |
|||
{ |
|||
private struct NotifyDisposable : IDisposable |
|||
private readonly SingleOrQueue<TSetRecord> _pendingValues; |
|||
private bool _isNotifying; |
|||
|
|||
public DeferredSetter() |
|||
{ |
|||
private readonly SettingStatus status; |
|||
_pendingValues = new SingleOrQueue<TSetRecord>(); |
|||
} |
|||
|
|||
internal NotifyDisposable(SettingStatus status) |
|||
{ |
|||
this.status = status; |
|||
status.Notifying = true; |
|||
} |
|||
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value) |
|||
{ |
|||
var old = backing; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
status.Notifying = false; |
|||
} |
|||
backing = value; |
|||
|
|||
source.RaisePropertyChanged(property, old, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Information on current setting/notification status of a property.
|
|||
/// </summary>
|
|||
private class SettingStatus |
|||
public bool SetAndNotify( |
|||
AvaloniaObject source, |
|||
AvaloniaProperty<TSetRecord> property, |
|||
ref TSetRecord backing, |
|||
TSetRecord value) |
|||
{ |
|||
public bool Notifying { get; set; } |
|||
|
|||
private SingleOrQueue<TSetRecord> pendingValues; |
|||
|
|||
public SingleOrQueue<TSetRecord> PendingValues |
|||
if (!_isNotifying) |
|||
{ |
|||
get |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>()); |
|||
SetAndRaisePropertyChanged(source, property, ref backing, value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords; |
|||
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords |
|||
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>()); |
|||
if (!_pendingValues.Empty) |
|||
{ |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
while (!_pendingValues.Empty) |
|||
{ |
|||
SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private SettingStatus GetOrCreateStatus(AvaloniaProperty property) |
|||
{ |
|||
if (!SetRecords.TryGetValue(property, out var status)) |
|||
{ |
|||
status = new SettingStatus(); |
|||
SetRecords.Add(property, status); |
|||
return true; |
|||
} |
|||
|
|||
return status; |
|||
_pendingValues.Enqueue(value); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Mark the property as currently notifying.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to mark as notifying.</param>
|
|||
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
|
|||
private NotifyDisposable MarkNotifying(AvaloniaProperty property) |
|||
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value) |
|||
where TValue : TSetRecord |
|||
{ |
|||
Contract.Requires<InvalidOperationException>(!IsNotifying(property)); |
|||
|
|||
SettingStatus status = GetOrCreateStatus(property); |
|||
|
|||
return new NotifyDisposable(status); |
|||
} |
|||
if (!_isNotifying) |
|||
{ |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
setAndNotifyCallback(property, ref backing, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Check if the property is currently notifying listeners.
|
|||
/// </summary>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <returns>If the property is currently notifying listeners.</returns>
|
|||
private bool IsNotifying(AvaloniaProperty property) |
|||
=> SetRecords.TryGetValue(property, out var value) && value.Notifying; |
|||
if (!_pendingValues.Empty) |
|||
{ |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
while (!_pendingValues.Empty) |
|||
{ |
|||
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add a pending assignment for the property.
|
|||
/// </summary>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="value">The value to assign.</param>
|
|||
private void AddPendingSet(AvaloniaProperty property, TSetRecord value) |
|||
{ |
|||
Contract.Requires<InvalidOperationException>(IsNotifying(property)); |
|||
return true; |
|||
} |
|||
|
|||
GetOrCreateStatus(property).PendingValues.Enqueue(value); |
|||
} |
|||
_pendingValues.Enqueue(value); |
|||
|
|||
/// <summary>
|
|||
/// Checks if there are any pending assignments for the property.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to check.</param>
|
|||
/// <returns>If the property has any pending assignments.</returns>
|
|||
private bool HasPendingSet(AvaloniaProperty property) |
|||
{ |
|||
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the first pending assignment for the property.
|
|||
/// Disposable that marks the property as currently notifying.
|
|||
/// When disposed, marks the property as done notifying.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to check.</param>
|
|||
/// <returns>The first pending assignment for the property.</returns>
|
|||
private TSetRecord GetFirstPendingSet(AvaloniaProperty property) |
|||
private readonly struct NotifyDisposable : IDisposable |
|||
{ |
|||
return GetOrCreateStatus(property).PendingValues.Dequeue(); |
|||
} |
|||
|
|||
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback); |
|||
private readonly DeferredSetter<TSetRecord> _setter; |
|||
|
|||
/// <summary>
|
|||
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
|
|||
/// </summary>
|
|||
/// <param name="property">The property to set.</param>
|
|||
/// <param name="backing">The backing field for the property</param>
|
|||
/// <param name="setterCallback">
|
|||
/// A callback that actually sets the property.
|
|||
/// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
|
|||
/// </param>
|
|||
/// <param name="value">The value to try to set.</param>
|
|||
public bool SetAndNotify<TValue>( |
|||
AvaloniaProperty property, |
|||
ref TValue backing, |
|||
SetterDelegate<TValue> setterCallback, |
|||
TSetRecord value) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(setterCallback != null); |
|||
if (!IsNotifying(property)) |
|||
internal NotifyDisposable(DeferredSetter<TSetRecord> setter) |
|||
{ |
|||
bool updated = false; |
|||
if (!object.Equals(value, backing)) |
|||
{ |
|||
updated = setterCallback(value, ref backing, notification => |
|||
{ |
|||
using (MarkNotifying(property)) |
|||
{ |
|||
notification(); |
|||
} |
|||
}); |
|||
} |
|||
while (HasPendingSet(property)) |
|||
{ |
|||
updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification => |
|||
{ |
|||
using (MarkNotifying(property)) |
|||
{ |
|||
notification(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
return updated; |
|||
_setter = setter; |
|||
_setter._isNotifying = true; |
|||
} |
|||
else if(!object.Equals(value, backing)) |
|||
|
|||
public void Dispose() |
|||
{ |
|||
AddPendingSet(property, value); |
|||
_setter._isNotifying = false; |
|||
} |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,65 @@ |
|||
using BenchmarkDotNet.Attributes; |
|||
|
|||
namespace Avalonia.Benchmarks.Base |
|||
{ |
|||
[MemoryDiagnoser] |
|||
public class DirectPropertyBenchmark |
|||
{ |
|||
[Benchmark(Baseline = true)] |
|||
public void SetAndRaiseOriginal() |
|||
{ |
|||
var obj = new DirectClass(); |
|||
|
|||
for (var i = 0; i < 100; ++i) |
|||
{ |
|||
obj.IntValue += 1; |
|||
} |
|||
} |
|||
|
|||
[Benchmark] |
|||
public void SetAndRaiseSimple() |
|||
{ |
|||
var obj = new DirectClass(); |
|||
|
|||
for (var i = 0; i < 100; ++i) |
|||
{ |
|||
obj.IntValueSimple += 1; |
|||
} |
|||
} |
|||
|
|||
class DirectClass : AvaloniaObject |
|||
{ |
|||
private int _intValue; |
|||
|
|||
public static readonly DirectProperty<DirectClass, int> IntValueProperty = |
|||
AvaloniaProperty.RegisterDirect<DirectClass, int>(nameof(IntValue), |
|||
o => o.IntValue, |
|||
(o, v) => o.IntValue = v); |
|||
|
|||
public int IntValue |
|||
{ |
|||
get => _intValue; |
|||
set => SetAndRaise(IntValueProperty, ref _intValue, value); |
|||
} |
|||
|
|||
public int IntValueSimple |
|||
{ |
|||
get => _intValue; |
|||
set |
|||
{ |
|||
VerifyAccess(); |
|||
|
|||
if (_intValue == value) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var old = _intValue; |
|||
_intValue = value; |
|||
|
|||
RaisePropertyChanged(IntValueProperty, old, _intValue); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue