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

Batching for AvaloniaObject property values. #5070

Merged
merged 19 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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
40 changes: 39 additions & 1 deletion src/Avalonia.Base/AvaloniaObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyProp
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private List<IAvaloniaObject> _inheritanceChildren;
private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
private bool _batchUpdate;

/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
Expand Down Expand Up @@ -117,6 +117,22 @@ public IBinding this[IndexerDescriptor binding]
set { this.Bind(binding.Property, value); }
}

private ValueStore Values
{
get
{
if (_values is null)
{
_values = new ValueStore(this);

if (_batchUpdate)
_values.BeginBatchUpdate();
}

return _values;
}
}

public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();

public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
Expand Down Expand Up @@ -434,6 +450,28 @@ public void CoerceValue<T>(StyledPropertyBase<T> property)
_values?.CoerceValue(property);
}

public void BeginBatchUpdate()
{
if (_batchUpdate)
{
throw new InvalidOperationException("Batch update already in progress.");
}

_batchUpdate = true;
_values?.BeginBatchUpdate();
}

public void EndBatchUpdate()
{
if (!_batchUpdate)
{
throw new InvalidOperationException("No batch update in progress.");
}

_batchUpdate = false;
_values?.EndBatchUpdate();
}

/// <inheritdoc/>
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{
Expand Down
44 changes: 40 additions & 4 deletions src/Avalonia.Base/PropertyStore/BindingEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore
/// <summary>
/// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
/// </summary>
internal interface IBindingEntry : IPriorityValueEntry, IDisposable
internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
{
void Start(bool ignoreBatchUpdate);
}

/// <summary>
Expand All @@ -22,6 +23,8 @@ internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserve
private readonly IAvaloniaObject _owner;
private IValueSink _sink;
private IDisposable? _subscription;
private bool _isSubscribed;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth packing these into an flags enum to save 4 bytes per value stored.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If only life would be that simple 😄 Depending on the type of the entry alignment of this class will change, for example Sharplab reducing size will do nothing since there is no free space and it will get aligned.

private bool _batchUpdate;
private Optional<T> _value;

public BindingEntry(
Expand All @@ -43,6 +46,16 @@ public BindingEntry(
public IObservable<BindingValue<T>> Source { get; }
Optional<object> IValue.GetValue() => _value.ToObject();

public void BeginBatchUpdate() => _batchUpdate = true;

public void EndBatchUpdate()
{
_batchUpdate = false;

if (_sink is ValueStore)
Start();
}

public Optional<T> GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
Expand All @@ -52,6 +65,7 @@ public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_isSubscribed = false;
_sink.Completed(Property, this, _value);
}

Expand Down Expand Up @@ -79,13 +93,35 @@ public void OnNext(BindingValue<T> value)
}
}

public void Start()
public void Start() => Start(false);

public void Start(bool ignoreBatchUpdate)
{
_subscription = Source.Subscribe(this);
// We can't use _subscription to check whether we're subscribed because it won't be set
// until Subscribe has finished, which will be too late to prevent reentrancy.
if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate))
{
_isSubscribed = true;
_subscription = Source.Subscribe(this);
}
}

public void Reparent(IValueSink sink) => _sink = sink;


public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
_value,
Priority));
}

private void UpdateValue(BindingValue<T> value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
Expand Down
15 changes: 15 additions & 0 deletions src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,20 @@ public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animat

public void Dispose() => _sink.Completed(Property, this, _value);
public void Reparent(IValueSink sink) => _sink = sink;
public void Start() { }

public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
_value,
Priority));
}
}
}
8 changes: 8 additions & 0 deletions src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Avalonia.PropertyStore
{
internal interface IBatchUpdate
{
void BeginBatchUpdate();
void EndBatchUpdate();
}
}
8 changes: 7 additions & 1 deletion src/Avalonia.Base/PropertyStore/IValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ namespace Avalonia.PropertyStore
/// </summary>
internal interface IValue
{
Optional<object> GetValue();
BindingPriority Priority { get; }
Optional<object> GetValue();
void Start();
void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue);
}

/// <summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,20 @@ public Optional<T> GetValue(BindingPriority maxPriority)
}

public void SetValue(T value) => _value = value;
public void Start() { }

public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
_value,
BindingPriority.LocalValue));
}
}
}
109 changes: 83 additions & 26 deletions src/Avalonia.Base/PropertyStore/PriorityValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ namespace Avalonia.PropertyStore
/// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
/// they were added) plus a local value.
/// </remarks>
internal class PriorityValue<T> : IValue<T>, IValueSink
internal class PriorityValue<T> : IValue<T>, IValueSink, IBatchUpdate
{
private readonly IAvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue;
private Optional<T> _value;
private bool _isCalculatingValue;
private bool _batchUpdate;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth packing these into an flags enum to save 4 bytes per value stored.


public PriorityValue(
IAvaloniaObject owner,
Expand Down Expand Up @@ -53,6 +55,18 @@ public PriorityValue(
existing.Reparent(this);
_entries.Add(existing);

if (existing is IBindingEntry binding &&
existing.Priority == BindingPriority.LocalValue)
{
// Bit of a special case here: if we have a local value binding that is being
// promoted to a priority value we need to make sure the binding is subscribed
// even if we've got a batch operation in progress because otherwise we don't know
// whether the binding or a subsequent SetValue with local priority will win. A
// notification won't be sent during batch update anyway because it will be
// caught and stored for later by the ValueStore.
binding.Start(ignoreBatchUpdate: true);
}

var v = existing.GetValue();

if (v.HasValue)
Expand All @@ -78,6 +92,28 @@ public PriorityValue(
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
Optional<object> IValue.GetValue() => _value.ToObject();

public void BeginBatchUpdate()
{
_batchUpdate = true;

foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.BeginBatchUpdate();
}
}

public void EndBatchUpdate()
{
_batchUpdate = false;

foreach (var entry in _entries)
{
(entry as IBatchUpdate)?.EndBatchUpdate();
}

UpdateEffectiveValue(null);
}

public void ClearLocalValue()
{
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
Expand Down Expand Up @@ -137,7 +173,22 @@ public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPr
return binding;
}

public void CoerceValue() => UpdateEffectiveValue(null);
public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
public void Start() => UpdateEffectiveValue(null);

public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
_value,
Priority));
}

void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
{
Expand All @@ -146,7 +197,7 @@ void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> ch
_localValue = default;
}

if (change is AvaloniaPropertyChangedEventArgs<T> c)
if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
{
UpdateEffectiveValue(c);
}
Expand Down Expand Up @@ -188,41 +239,47 @@ private int FindInsertPoint(BindingPriority priority)

public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
{
var reachedLocalValues = false;
_isCalculatingValue = true;

for (var i = _entries.Count - 1; i >= 0; --i)
try
{
var entry = _entries[i];

if (entry.Priority < maxPriority)
for (var i = _entries.Count - 1; i >= 0; --i)
{
continue;
var entry = _entries[i];

if (entry.Priority < maxPriority)
{
continue;
}

entry.Start();

if (entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}

var entryValue = entry.GetValue();

if (entryValue.HasValue)
{
return (entryValue, entry.Priority);
}
}

if (!reachedLocalValues &&
entry.Priority >= BindingPriority.LocalValue &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}

var entryValue = entry.GetValue();

if (entryValue.HasValue)
{
return (entryValue, entry.Priority);
}
return (default, BindingPriority.Unset);
}

if (!reachedLocalValues &&
maxPriority <= BindingPriority.LocalValue &&
_localValue.HasValue)
finally
{
return (_localValue, BindingPriority.LocalValue);
_isCalculatingValue = false;
}

return (default, BindingPriority.Unset);
}

private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)
Expand Down
Loading