committed by
GitHub
11 changed files with 360 additions and 337 deletions
@ -1,6 +1,6 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="SkiaSharp" Version="1.68.0" /> |
<PackageReference Include="SkiaSharp" Version="1.68.0" /> |
||||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.1" /> |
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -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; |
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
using System.Collections.Generic; |
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
namespace Avalonia.Utilities |
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>
|
/// <summary>
|
||||
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
|
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
|
||||
/// Used to fix #855.
|
/// Used to fix #855.
|
||||
/// </summary>
|
/// </summary>
|
||||
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
|
/// <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) |
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value) |
||||
{ |
{ |
||||
this.status = status; |
var old = backing; |
||||
status.Notifying = true; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
backing = value; |
||||
{ |
|
||||
status.Notifying = false; |
source.RaisePropertyChanged(property, old, value); |
||||
} |
|
||||
} |
} |
||||
|
|
||||
/// <summary>
|
public bool SetAndNotify( |
||||
/// Information on current setting/notification status of a property.
|
AvaloniaObject source, |
||||
/// </summary>
|
AvaloniaProperty<TSetRecord> property, |
||||
private class SettingStatus |
ref TSetRecord backing, |
||||
|
TSetRecord value) |
||||
{ |
{ |
||||
public bool Notifying { get; set; } |
if (!_isNotifying) |
||||
|
|
||||
private SingleOrQueue<TSetRecord> pendingValues; |
|
||||
|
|
||||
public SingleOrQueue<TSetRecord> PendingValues |
|
||||
{ |
{ |
||||
get |
using (new NotifyDisposable(this)) |
||||
{ |
{ |
||||
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>()); |
SetAndRaisePropertyChanged(source, property, ref backing, value); |
||||
} |
} |
||||
} |
|
||||
} |
|
||||
|
|
||||
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords; |
if (!_pendingValues.Empty) |
||||
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords |
{ |
||||
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>()); |
using (new NotifyDisposable(this)) |
||||
|
{ |
||||
|
while (!_pendingValues.Empty) |
||||
|
{ |
||||
|
SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
private SettingStatus GetOrCreateStatus(AvaloniaProperty property) |
return true; |
||||
{ |
|
||||
if (!SetRecords.TryGetValue(property, out var status)) |
|
||||
{ |
|
||||
status = new SettingStatus(); |
|
||||
SetRecords.Add(property, status); |
|
||||
} |
} |
||||
|
|
||||
return status; |
_pendingValues.Enqueue(value); |
||||
|
|
||||
|
return false; |
||||
} |
} |
||||
|
|
||||
/// <summary>
|
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value) |
||||
/// Mark the property as currently notifying.
|
where TValue : TSetRecord |
||||
/// </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) |
|
||||
{ |
{ |
||||
Contract.Requires<InvalidOperationException>(!IsNotifying(property)); |
if (!_isNotifying) |
||||
|
{ |
||||
SettingStatus status = GetOrCreateStatus(property); |
using (new NotifyDisposable(this)) |
||||
|
{ |
||||
return new NotifyDisposable(status); |
setAndNotifyCallback(property, ref backing, value); |
||||
} |
} |
||||
|
|
||||
/// <summary>
|
if (!_pendingValues.Empty) |
||||
/// Check if the property is currently notifying listeners.
|
{ |
||||
/// </summary>
|
using (new NotifyDisposable(this)) |
||||
/// <param name="property">The property.</param>
|
{ |
||||
/// <returns>If the property is currently notifying listeners.</returns>
|
while (!_pendingValues.Empty) |
||||
private bool IsNotifying(AvaloniaProperty property) |
{ |
||||
=> SetRecords.TryGetValue(property, out var value) && value.Notifying; |
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
/// <summary>
|
return true; |
||||
/// 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)); |
|
||||
|
|
||||
GetOrCreateStatus(property).PendingValues.Enqueue(value); |
_pendingValues.Enqueue(value); |
||||
} |
|
||||
|
|
||||
/// <summary>
|
return false; |
||||
/// 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; |
|
||||
} |
} |
||||
|
|
||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||
/// <param name="property">The property to check.</param>
|
private readonly struct NotifyDisposable : IDisposable |
||||
/// <returns>The first pending assignment for the property.</returns>
|
|
||||
private TSetRecord GetFirstPendingSet(AvaloniaProperty property) |
|
||||
{ |
{ |
||||
return GetOrCreateStatus(property).PendingValues.Dequeue(); |
private readonly DeferredSetter<TSetRecord> _setter; |
||||
} |
|
||||
|
|
||||
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback); |
|
||||
|
|
||||
/// <summary>
|
internal NotifyDisposable(DeferredSetter<TSetRecord> setter) |
||||
/// 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)) |
|
||||
{ |
{ |
||||
bool updated = false; |
_setter = setter; |
||||
if (!object.Equals(value, backing)) |
_setter._isNotifying = true; |
||||
{ |
|
||||
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; |
|
||||
} |
} |
||||
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