Performance upgrade for Debounce/Throttle
This commit is contained in:
@@ -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;
|
_nextValue = next;
|
||||||
|
_nextCancellationToken = cancellationToken;
|
||||||
var newToken = newTokenSource.Token;
|
|
||||||
|
if (_isThrottleTaskRunning)
|
||||||
if (!_isActive || ResetTimer)
|
{
|
||||||
|
if (ResetTimer)
|
||||||
{
|
{
|
||||||
_isActive = true;
|
|
||||||
_startTime = DateTime.Now;
|
_startTime = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (DateTime.Now - _startTime < Interval())
|
|
||||||
{
|
|
||||||
await Task.Delay(WaitInterval, newToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WithLock(() => { _isActive = false; });
|
_startTime = DateTime.Now;
|
||||||
|
_isThrottleTaskRunning = true;
|
||||||
await FireAsync(next, cancellationToken);
|
Task.Run(async () => await StartDebounceTask());
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException ex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
_lastFired = DateTime.Now;
|
||||||
_debounceCts = new();
|
}
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
await SetNewValueAsync(
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(interval, _debounceCts.Token);
|
|
||||||
await FireIfNeededAsync(
|
|
||||||
next,
|
next,
|
||||||
() => { _lastFired = DateTime.Now; },
|
cancellationToken
|
||||||
_debounceCts.Token, 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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