Skip to content

Commit

Permalink
Implement batching for AvaloniaObject property values.
Browse files Browse the repository at this point in the history
Cuts down the amount of notifications raised when controls' stying is attached/detached.

Part of fixing #5027.
  • Loading branch information
grokys committed Nov 18, 2020
1 parent 662b1e0 commit 0f23811
Show file tree
Hide file tree
Showing 16 changed files with 828 additions and 80 deletions.
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;
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;

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

0 comments on commit 0f23811

Please sign in to comment.