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 17 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
57 changes: 50 additions & 7 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 @@ -39,10 +42,20 @@ public BindingEntry(
}

public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; }
public BindingPriority Priority { get; private set; }
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,10 +65,17 @@ public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_sink.Completed(Property, this, _value);
_isSubscribed = false;
OnCompleted();
}

public void OnCompleted() => _sink.Completed(Property, this, _value);
public void OnCompleted()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_sink.Completed(Property, this, oldValue);
}

public void OnError(Exception error)
{
Expand All @@ -79,13 +99,36 @@ 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,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}

private void UpdateValue(BindingValue<T> value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
Expand Down
30 changes: 27 additions & 3 deletions src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;

#nullable enable
Expand All @@ -17,7 +18,7 @@ internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable

public ConstantValueEntry(
StyledPropertyBase<T> property,
T value,
[AllowNull] T value,
BindingPriority priority,
IValueSink sink)
{
Expand All @@ -28,15 +29,38 @@ public ConstantValueEntry(
}

public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; }
public BindingPriority Priority { get; private set; }
Optional<object> IValue.GetValue() => _value.ToObject();

public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
{
return Priority >= maxPriority ? _value : Optional<T>.Empty;
}

public void Dispose() => _sink.Completed(Property, this, _value);
public void Dispose()
{
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_sink.Completed(Property, this, oldValue);
}

public void Reparent(IValueSink sink) => _sink = sink;
public void Start() { }

public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
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();
}
}
9 changes: 8 additions & 1 deletion src/Avalonia.Base/PropertyStore/IValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ 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,
Optional<object> newValue);
}

/// <summary>
Expand Down
16 changes: 16 additions & 0 deletions src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,21 @@ 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,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
BindingPriority.LocalValue));
}
}
}
Loading