From 9171a3de5421b75b94af0341cb29c41119f09f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Thu, 27 Jul 2023 13:55:44 +0200 Subject: [PATCH] Performance upgrade for Debounce/Throttle --- .../DeclarativeProperty/DebounceProperty.cs | 78 ++++++++++------- .../DeclarativeProperty.cs | 17 +++- .../DeclarativePropertyBase.cs | 17 ++-- .../DeclarativePropertyExtensions.cs | 6 ++ .../DeclarativeProperty/ThrottleProperty.cs | 60 ++++--------- .../DeclarativeProperty/TimingPropertyBase.cs | 85 ------------------- 6 files changed, 97 insertions(+), 166 deletions(-) delete mode 100644 src/Library/DeclarativeProperty/TimingPropertyBase.cs diff --git a/src/Library/DeclarativeProperty/DebounceProperty.cs b/src/Library/DeclarativeProperty/DebounceProperty.cs index 2e21b20..830df88 100644 --- a/src/Library/DeclarativeProperty/DebounceProperty.cs +++ b/src/Library/DeclarativeProperty/DebounceProperty.cs @@ -1,52 +1,68 @@ namespace DeclarativeProperty; -public sealed class DebounceProperty : TimingPropertyBase +public sealed class DebounceProperty : DeclarativePropertyBase { - private CancellationTokenSource? _debounceCts; - private bool _isActive; - private DateTime _startTime; + private readonly object _lock = new(); + private readonly Func _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 from, Func interval, - Action? setValueHook = null) : base(from, interval, setValueHook) + Action? 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 + ); + } } \ No newline at end of file diff --git a/src/Library/DeclarativeProperty/DeclarativeProperty.cs b/src/Library/DeclarativeProperty/DeclarativeProperty.cs index 5ea1f20..8eab7ba 100644 --- a/src/Library/DeclarativeProperty/DeclarativeProperty.cs +++ b/src/Library/DeclarativeProperty/DeclarativeProperty.cs @@ -18,9 +18,24 @@ public sealed class DeclarativeProperty : DeclarativePropertyBase public DeclarativeProperty(T initialValue, Action? 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 + { + } + }); + } } \ No newline at end of file diff --git a/src/Library/DeclarativeProperty/DeclarativePropertyBase.cs b/src/Library/DeclarativeProperty/DeclarativePropertyBase.cs index f848777..e00e2b9 100644 --- a/src/Library/DeclarativeProperty/DeclarativePropertyBase.cs +++ b/src/Library/DeclarativeProperty/DeclarativePropertyBase.cs @@ -52,7 +52,7 @@ public abstract class DeclarativePropertyBase : IDeclarativeProperty { _subscribers.Add(onChange); onChange(_value, default); - + return new Unsubscriber(this, onChange); } @@ -84,6 +84,13 @@ public abstract class DeclarativePropertyBase : IDeclarativeProperty }); 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 : IDeclarativeProperty } _triggerDisposables.Clear(); - - if(cancellationToken.IsCancellationRequested) return; } _value = newValue; @@ -112,18 +117,16 @@ public abstract class DeclarativePropertyBase : IDeclarativeProperty { 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() diff --git a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs index c280152..2dd08e2 100644 --- a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs +++ b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs @@ -12,6 +12,12 @@ public static class DeclarativePropertyExtensions public static IDeclarativeProperty Debounce(this IDeclarativeProperty from, Func interval, bool resetTimer = false) => new DebounceProperty(from, interval) {ResetTimer = resetTimer}; + public static IDeclarativeProperty Throttle(this IDeclarativeProperty from, TimeSpan interval) + => new ThrottleProperty(from, () => interval); + + public static IDeclarativeProperty Throttle(this IDeclarativeProperty from, Func interval) + => new ThrottleProperty(from, interval); + public static IDeclarativeProperty DistinctUntilChanged(this IDeclarativeProperty from) => new DistinctUntilChangedProperty(from); diff --git a/src/Library/DeclarativeProperty/ThrottleProperty.cs b/src/Library/DeclarativeProperty/ThrottleProperty.cs index a494967..9fdea22 100644 --- a/src/Library/DeclarativeProperty/ThrottleProperty.cs +++ b/src/Library/DeclarativeProperty/ThrottleProperty.cs @@ -1,59 +1,35 @@ namespace DeclarativeProperty; -public class ThrottleProperty : TimingPropertyBase +public class ThrottleProperty : DeclarativePropertyBase { - private CancellationTokenSource? _debounceCts; - private DateTime _lastFired; + private readonly object _lock = new(); + private DateTime _lastFired = DateTime.MinValue; + private readonly Func _interval; public ThrottleProperty( IDeclarativeProperty from, Func interval, - Action? setValueHook = null) : base(from, interval, setValueHook) + Action? 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 + ); } } \ No newline at end of file diff --git a/src/Library/DeclarativeProperty/TimingPropertyBase.cs b/src/Library/DeclarativeProperty/TimingPropertyBase.cs deleted file mode 100644 index ddaaa89..0000000 --- a/src/Library/DeclarativeProperty/TimingPropertyBase.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace DeclarativeProperty; - -public abstract class TimingPropertyBase : DeclarativePropertyBase -{ - private readonly SemaphoreSlim _semaphore = new(1, 1); - protected Func Interval { get; } - - protected TimingPropertyBase( - IDeclarativeProperty from, - Func interval, - Action? 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(Func func) - { - try - { - _semaphore.Wait(); - return func(); - } - finally - { - _semaphore.Release(); - } - } - - protected async Task WithLockAsync(Func 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); -} \ No newline at end of file