Performance upgrade for Debounce/Throttle
This commit is contained in:
@@ -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)
|
||||
{
|
||||
_isActive = true;
|
||||
_startTime = DateTime.Now;
|
||||
}
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
_nextValue = next;
|
||||
_nextCancellationToken = cancellationToken;
|
||||
|
||||
if (_isThrottleTaskRunning)
|
||||
{
|
||||
while (DateTime.Now - _startTime < Interval())
|
||||
if (ResetTimer)
|
||||
{
|
||||
await Task.Delay(WaitInterval, newToken);
|
||||
_startTime = DateTime.Now;
|
||||
}
|
||||
|
||||
WithLock(() => { _isActive = false; });
|
||||
|
||||
await FireAsync(next, cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
_startTime = DateTime.Now;
|
||||
_isThrottleTaskRunning = true;
|
||||
Task.Run(async () => await StartDebounceTask());
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
|
||||
{
|
||||
_subscribers.Add(onChange);
|
||||
onChange(_value, default);
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
else
|
||||
{
|
||||
_debounceCts = new();
|
||||
Task.Run(async () =>
|
||||
if (DateTime.Now - _lastFired < _interval())
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(interval, _debounceCts.Token);
|
||||
await FireIfNeededAsync(
|
||||
next,
|
||||
() => { _lastFired = DateTime.Now; },
|
||||
_debounceCts.Token, cancellationToken
|
||||
);
|
||||
/*var shouldFire = WithLock(() =>
|
||||
{
|
||||
if (_debounceCts.Token.IsCancellationRequested)
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
_lastFired = DateTime.Now;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!shouldFire) return;
|
||||
|
||||
await Fire(next, cancellationToken);*/
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
}
|
||||
});
|
||||
_lastFired = DateTime.Now;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
||||
await SetNewValueAsync(
|
||||
next,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user