Performance upgrade for Debounce/Throttle

This commit is contained in:
2023-07-27 13:55:44 +02:00
parent d26401948a
commit 9171a3de54
6 changed files with 97 additions and 166 deletions

View File

@@ -1,52 +1,68 @@
namespace DeclarativeProperty; namespace DeclarativeProperty;
public sealed class DebounceProperty<T> : TimingPropertyBase<T> public sealed class DebounceProperty<T> : DeclarativePropertyBase<T>
{ {
private CancellationTokenSource? _debounceCts; private readonly object _lock = new();
private bool _isActive; private readonly Func<TimeSpan> _interval;
private DateTime _startTime; private DateTime _startTime = DateTime.MinValue;
private T? _nextValue;
private CancellationToken _nextCancellationToken;
private bool _isThrottleTaskRunning;
public bool ResetTimer { get; init; } public bool ResetTimer { get; init; }
public TimeSpan WaitInterval { get; init; } = TimeSpan.FromMilliseconds(1); public TimeSpan WaitInterval { get; init; } = TimeSpan.FromMilliseconds(10);
public DebounceProperty( public DebounceProperty(
IDeclarativeProperty<T> from, IDeclarativeProperty<T> from,
Func<TimeSpan> interval, Func<TimeSpan> interval,
Action<T?>? setValueHook = null) : base(from, interval, setValueHook) Action<T?>? setValueHook = null) : base(from.Value, setValueHook)
{ {
_interval = interval;
AddDisposable(from.Subscribe(SetValue));
} }
protected override Task SetValue(T? next, CancellationToken cancellationToken = default) private Task SetValue(T? next, CancellationToken cancellationToken = default)
{ {
_debounceCts?.Cancel(); lock (_lock)
var newTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_debounceCts = newTokenSource;
var newToken = newTokenSource.Token;
if (!_isActive || ResetTimer)
{ {
_isActive = true; _nextValue = next;
_startTime = DateTime.Now; _nextCancellationToken = cancellationToken;
}
if (_isThrottleTaskRunning)
Task.Run(async () =>
{
try
{ {
while (DateTime.Now - _startTime < Interval()) if (ResetTimer)
{ {
await Task.Delay(WaitInterval, newToken); _startTime = DateTime.Now;
} }
return Task.CompletedTask;
WithLock(() => { _isActive = false; });
await FireAsync(next, cancellationToken);
} }
catch (TaskCanceledException ex)
{ _startTime = DateTime.Now;
} _isThrottleTaskRunning = true;
}); Task.Run(async () => await StartDebounceTask());
}
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task StartDebounceTask()
{
while (DateTime.Now - _startTime < _interval())
{
await Task.Delay(WaitInterval);
}
T? next;
CancellationToken cancellationToken;
lock (_lock)
{
_isThrottleTaskRunning = false;
next = _nextValue;
cancellationToken = _nextCancellationToken;
}
await SetNewValueAsync(
next,
cancellationToken
);
}
} }

View File

@@ -18,9 +18,24 @@ public sealed class DeclarativeProperty<T> : DeclarativePropertyBase<T>
public DeclarativeProperty(T initialValue, Action<T?>? setValueHook = null) : base(initialValue, setValueHook) public DeclarativeProperty(T initialValue, Action<T?>? setValueHook = null) : base(initialValue, setValueHook)
{ {
} }
public async Task SetValue(T newValue, CancellationToken cancellationToken = default) public async Task SetValue(T newValue, CancellationToken cancellationToken = default)
=> await SetNewValueAsync(newValue, cancellationToken); => await SetNewValueAsync(newValue, cancellationToken);
public void SetValueSafe(T newValue, CancellationToken cancellationToken = default)
{
SetNewValueSync(newValue, cancellationToken);
if (cancellationToken.IsCancellationRequested) return;
Task.Run(async () =>
{
try
{
await NotifySubscribersAsync(newValue, cancellationToken);
}
catch
{
}
});
}
} }

View File

@@ -52,7 +52,7 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
{ {
_subscribers.Add(onChange); _subscribers.Add(onChange);
onChange(_value, default); onChange(_value, default);
return new Unsubscriber<T>(this, onChange); return new Unsubscriber<T>(this, onChange);
} }
@@ -84,6 +84,13 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
}); });
protected async Task SetNewValueAsync(T? newValue, CancellationToken cancellationToken = default) protected async Task SetNewValueAsync(T? newValue, CancellationToken cancellationToken = default)
{
SetNewValueSync(newValue, cancellationToken);
if (cancellationToken.IsCancellationRequested) return;
await NotifySubscribersAsync(newValue, cancellationToken);
}
protected void SetNewValueSync(T? newValue, CancellationToken cancellationToken = default)
{ {
if (!(Value?.Equals(newValue) ?? false)) if (!(Value?.Equals(newValue) ?? false))
{ {
@@ -102,8 +109,6 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
} }
_triggerDisposables.Clear(); _triggerDisposables.Clear();
if(cancellationToken.IsCancellationRequested) return;
} }
_value = newValue; _value = newValue;
@@ -112,18 +117,16 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
{ {
foreach (var subscribeTrigger in _subscribeTriggers) foreach (var subscribeTrigger in _subscribeTriggers)
{ {
if(cancellationToken.IsCancellationRequested) return;
var disposable = subscribeTrigger(this, _value); var disposable = subscribeTrigger(this, _value);
if (disposable != null) _triggerDisposables.Add(disposable); if (disposable != null) _triggerDisposables.Add(disposable);
} }
} }
} }
if (cancellationToken.IsCancellationRequested) return;
OnPropertyChanged(nameof(Value)); OnPropertyChanged(nameof(Value));
} }
await NotifySubscribersAsync(newValue, cancellationToken);
} }
public async Task ReFireAsync() public async Task ReFireAsync()

View File

@@ -12,6 +12,12 @@ public static class DeclarativePropertyExtensions
public static IDeclarativeProperty<T> Debounce<T>(this IDeclarativeProperty<T> from, Func<TimeSpan> interval, bool resetTimer = false) public static IDeclarativeProperty<T> Debounce<T>(this IDeclarativeProperty<T> from, Func<TimeSpan> interval, bool resetTimer = false)
=> new DebounceProperty<T>(from, interval) {ResetTimer = resetTimer}; => new DebounceProperty<T>(from, interval) {ResetTimer = resetTimer};
public static IDeclarativeProperty<T> Throttle<T>(this IDeclarativeProperty<T> from, TimeSpan interval)
=> new ThrottleProperty<T>(from, () => interval);
public static IDeclarativeProperty<T> Throttle<T>(this IDeclarativeProperty<T> from, Func<TimeSpan> interval)
=> new ThrottleProperty<T>(from, interval);
public static IDeclarativeProperty<T> DistinctUntilChanged<T>(this IDeclarativeProperty<T> from) public static IDeclarativeProperty<T> DistinctUntilChanged<T>(this IDeclarativeProperty<T> from)
=> new DistinctUntilChangedProperty<T>(from); => new DistinctUntilChangedProperty<T>(from);

View File

@@ -1,59 +1,35 @@
namespace DeclarativeProperty; namespace DeclarativeProperty;
public class ThrottleProperty<T> : TimingPropertyBase<T> public class ThrottleProperty<T> : DeclarativePropertyBase<T>
{ {
private CancellationTokenSource? _debounceCts; private readonly object _lock = new();
private DateTime _lastFired; private DateTime _lastFired = DateTime.MinValue;
private readonly Func<TimeSpan> _interval;
public ThrottleProperty( public ThrottleProperty(
IDeclarativeProperty<T> from, IDeclarativeProperty<T> from,
Func<TimeSpan> interval, Func<TimeSpan> interval,
Action<T?>? setValueHook = null) : base(from, interval, setValueHook) Action<T?>? setValueHook = null) : base(from.Value, setValueHook)
{ {
_interval = interval;
AddDisposable(from.Subscribe(SetValue));
} }
protected override Task SetValue(T? next, CancellationToken cancellationToken = default) private async Task SetValue(T? next, CancellationToken cancellationToken = default)
{ {
_debounceCts?.Cancel(); lock (_lock)
var interval = Interval();
if (DateTime.Now - _lastFired > interval)
{ {
_lastFired = DateTime.Now; if (DateTime.Now - _lastFired < _interval())
// Note: Recursive chains can happen. Awaiting this can cause a deadlock.
Task.Run(async () => await FireAsync(next, cancellationToken));
}
else
{
_debounceCts = new();
Task.Run(async () =>
{ {
try return;
{ }
await Task.Delay(interval, _debounceCts.Token);
await FireIfNeededAsync(
next,
() => { _lastFired = DateTime.Now; },
_debounceCts.Token, cancellationToken
);
/*var shouldFire = WithLock(() =>
{
if (_debounceCts.Token.IsCancellationRequested)
return false;
_lastFired = DateTime.Now; _lastFired = DateTime.Now;
return true;
});
if (!shouldFire) return;
await Fire(next, cancellationToken);*/
}
catch (TaskCanceledException ex)
{
}
});
} }
return Task.CompletedTask; await SetNewValueAsync(
next,
cancellationToken
);
} }
} }

View File

@@ -1,85 +0,0 @@
namespace DeclarativeProperty;
public abstract class TimingPropertyBase<T> : DeclarativePropertyBase<T>
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
protected Func<TimeSpan> Interval { get; }
protected TimingPropertyBase(
IDeclarativeProperty<T> from,
Func<TimeSpan> interval,
Action<T?>? setValueHook = null) : base(from.Value, setValueHook)
{
Interval = interval;
AddDisposable(from.Subscribe(SetValueInternal));
}
private async Task SetValueInternal(T? next, CancellationToken cancellationToken = default)
=> await WithLockAsync(async () => await SetValue(next, cancellationToken), cancellationToken);
protected void WithLock(Action action)
{
try
{
_semaphore.Wait();
action();
}
finally
{
_semaphore.Release();
}
}
protected TResult WithLock<TResult>(Func<TResult> func)
{
try
{
_semaphore.Wait();
return func();
}
finally
{
_semaphore.Release();
}
}
protected async Task WithLockAsync(Func<Task> action, CancellationToken cancellationToken = default)
{
try
{
await _semaphore.WaitAsync(cancellationToken);
await action();
}
finally
{
_semaphore.Release();
}
}
protected abstract Task SetValue(T? next, CancellationToken cancellationToken = default);
protected async Task FireIfNeededAsync(
T? next,
Action cleanup,
CancellationToken timingCancellationToken = default,
CancellationToken cancellationToken = default)
{
await Task.Delay(Interval(), timingCancellationToken);
var shouldFire = WithLock(() =>
{
if (timingCancellationToken.IsCancellationRequested)
return false;
cleanup();
return true;
});
if (!shouldFire) return;
await FireAsync(next, cancellationToken);
}
protected async Task FireAsync(T? next, CancellationToken cancellationToken = default)
=> await SetNewValueAsync(next, cancellationToken);
}