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;
public sealed class DebounceProperty<T> : TimingPropertyBase<T>
public sealed class DebounceProperty<T> : DeclarativePropertyBase<T>
{
private CancellationTokenSource? _debounceCts;
private bool _isActive;
private DateTime _startTime;
private readonly object _lock = new();
private readonly Func<TimeSpan> _interval;
private DateTime _startTime = DateTime.MinValue;
private T? _nextValue;
private CancellationToken _nextCancellationToken;
private bool _isThrottleTaskRunning;
public bool ResetTimer { get; init; }
public TimeSpan WaitInterval { get; init; } = TimeSpan.FromMilliseconds(1);
public TimeSpan WaitInterval { get; init; } = TimeSpan.FromMilliseconds(10);
public DebounceProperty(
IDeclarativeProperty<T> from,
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();
var newTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_debounceCts = newTokenSource;
var newToken = newTokenSource.Token;
if (!_isActive || ResetTimer)
lock (_lock)
{
_nextValue = next;
_nextCancellationToken = cancellationToken;
if (_isThrottleTaskRunning)
{
if (ResetTimer)
{
_isActive = true;
_startTime = DateTime.Now;
}
Task.Run(async () =>
{
try
{
while (DateTime.Now - _startTime < Interval())
{
await Task.Delay(WaitInterval, newToken);
return Task.CompletedTask;
}
WithLock(() => { _isActive = false; });
await FireAsync(next, cancellationToken);
_startTime = DateTime.Now;
_isThrottleTaskRunning = true;
Task.Run(async () => await StartDebounceTask());
}
catch (TaskCanceledException ex)
{
}
});
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 async Task SetValue(T newValue, CancellationToken cancellationToken = default)
=> 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

@@ -84,6 +84,13 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
});
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))
{
@@ -102,8 +109,6 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
}
_triggerDisposables.Clear();
if(cancellationToken.IsCancellationRequested) return;
}
_value = newValue;
@@ -112,18 +117,16 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
{
foreach (var subscribeTrigger in _subscribeTriggers)
{
if(cancellationToken.IsCancellationRequested) return;
var disposable = subscribeTrigger(this, _value);
if (disposable != null) _triggerDisposables.Add(disposable);
}
}
}
if (cancellationToken.IsCancellationRequested) return;
OnPropertyChanged(nameof(Value));
}
await NotifySubscribersAsync(newValue, cancellationToken);
}
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)
=> 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)
=> new DistinctUntilChangedProperty<T>(from);

View File

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

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);
}