Skip to content

Commit

Permalink
Merge buffering with generic Debouncer
Browse files Browse the repository at this point in the history
  • Loading branch information
amvandenberg committed Apr 15, 2024
1 parent 034a307 commit 9942c7f
Show file tree
Hide file tree
Showing 15 changed files with 401 additions and 589 deletions.
27 changes: 0 additions & 27 deletions Debounce/BufferedEventArgs.cs

This file was deleted.

124 changes: 0 additions & 124 deletions Debounce/Bufferer.cs

This file was deleted.

26 changes: 19 additions & 7 deletions Debounce/DebouncedEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,37 @@
namespace Dorssel.Utilities;

/// <summary>
/// Provides data for the <see cref="Debouncer.Debounced"/> event.
/// Provides data for the <see cref="Debouncer{TData}.Debounced"/> event.
/// </summary>
public class DebouncedEventArgs : EventArgs
public class DebouncedEventArgs<TData> : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="DebouncedEventArgs"/> class.
/// Initializes a new instance of the <see cref="DebouncedEventArgs{TData}"/> class.
/// </summary>
/// <param name="count">The number of triggers accumulated since the previous event was sent.
/// <para>Must be greater than 0.</para>
/// </param>
/// <param name="triggerData">
/// Accumulated data from each individual trigger, or empty when buffering is disabled in the <see cref="Debouncer{TData}"/>
/// </param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="count"/> is not greater than 0.</exception>
public DebouncedEventArgs(long count)
: this(count, true)
public DebouncedEventArgs(long count, IReadOnlyList<TData> triggerData)
: this(count, true, triggerData)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DebouncedEventArgs"/> class.
/// Initializes a new instance of the <see cref="DebouncedEventArgs{TData}"/> class.
/// </summary>
/// <param name="count">The number of triggers accumulated since the previous event was sent.
/// <para>Must be greater than 0 if <paramref name="boundsCheck"/> is <c>true</c>.</para>
/// </param>
/// <param name="boundsCheck">If <c>true</c>, <paramref name="count"/> is checked to be within its valid range.</param>
/// <param name="triggerData">
/// Accumulated data from each individual trigger, or empty when buffering is disabled in the <see cref="Debouncer{TData}"/>
/// </param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="boundsCheck"/> is <c>true</c> and <paramref name="count"/> is not greater than 0.</exception>
protected DebouncedEventArgs(long count, bool boundsCheck)
protected DebouncedEventArgs(long count, bool boundsCheck, IReadOnlyList<TData> triggerData)
{
if (boundsCheck)
{
Expand All @@ -40,6 +46,7 @@ protected DebouncedEventArgs(long count, bool boundsCheck)
}

Count = count;
TriggerData = triggerData;
}

/// <summary>
Expand All @@ -49,4 +56,9 @@ protected DebouncedEventArgs(long count, bool boundsCheck)
/// <para>The value will always greater than 0.</para>
/// </remarks>
public long Count { get; }

/// <summary>
/// List of data accumulated in this buffered event.
/// </summary>
public IReadOnlyList<TData> TriggerData { get; }
}
88 changes: 73 additions & 15 deletions Debounce/Debouncer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,49 @@
//
// SPDX-License-Identifier: MIT

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Dorssel.Utilities;

/// <summary>
/// This class implements the <see cref="IDebouncer"/> interface.
/// Object which debounces events, i.e., accumulating multiple incoming events into one.
/// </summary>
public sealed class Debouncer
: IDebouncer
, IDisposable
public sealed class Debouncer : Debouncer<Void>, IDebouncer
{
/// <summary>
/// Initializes a new instance of the <see cref="Debouncer"/> class.
/// </summary>
public Debouncer()
: base(false)
{
}
}

/// <summary>
/// Object which debounces events, i.e., accumulating multiple incoming events into one with the possibility of
/// keeping track of the incoming trigger data.
/// </summary>
public class Debouncer<TData> : IDebouncer<TData>
, IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="Debouncer{TData}"/> class with buffering enabled.
/// </summary>
public Debouncer()
: this(true)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="Debouncer{TData}"/> class with enableBuffering option.
/// </summary>
/// <param name="enableBuffering">Whether to buffer trigger data or not</param>
protected Debouncer(bool enableBuffering)
{
Timer = new Timer(OnTimer, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
EnableBuffering = enableBuffering;
}

long InterlockedCountMinusOne = -1;
Expand Down Expand Up @@ -53,6 +78,9 @@ internal BenchmarkCounters Benchmark
readonly Timer Timer;
bool TimerActive;
bool SendingEvent;
bool EnableBuffering;
List<TData> TriggerData = new();
object TriggerDataLock = new();

internal static long AddWithClamp(long left, long right)
{
Expand Down Expand Up @@ -137,6 +165,15 @@ void LockedReschedule()
{
// Sending event now, so accumulate all coalesced triggers.
var count = AddWithClamp(Count, Interlocked.Exchange(ref InterlockedCountMinusOne, -1) + 1);
IReadOnlyList<TData> triggerData = [];
if (EnableBuffering)
{
lock (TriggerDataLock)
{
triggerData = new ReadOnlyCollection<TData>(TriggerData);
TriggerData = new();
}
}

FirstTrigger.Reset();
LastTrigger.Reset();
Expand All @@ -146,7 +183,7 @@ void LockedReschedule()
// Must call handler asynchronously and outside the lock.
Task.Run(() =>
{
Debounced?.Invoke(this, new DebouncedEventArgs((long)count));
Debounced?.Invoke(this, new DebouncedEventArgs<TData>((long)count, triggerData));
lock (LockObject)
{
// Handler has finished.
Expand Down Expand Up @@ -213,14 +250,18 @@ void LockedReschedule()

#region IDebounce Support
/// <inheritdoc/>
public event EventHandler<DebouncedEventArgs>? Debounced;

/// <inheritdoc/>
public void Trigger(Void data) => Trigger();
public event EventHandler<DebouncedEventArgs<TData>>? Debounced;

/// <inheritdoc/>
public void Trigger()
public void Trigger(TData data = default!)
{
if (EnableBuffering)
{
lock (TriggerDataLock)
{
TriggerData.Add(data);
}
}
var newCountMinusOne = Interlocked.Increment(ref InterlockedCountMinusOne);
if (newCountMinusOne > 0)
{
Expand Down Expand Up @@ -250,6 +291,10 @@ public long Reset()
{
lock (LockObject)
{
lock (TriggerDataLock)
{
TriggerData = new();
}
if (!IsDisposed)
{
Count = AddWithClamp(Count, Interlocked.Exchange(ref InterlockedCountMinusOne, -1) + 1);
Expand Down Expand Up @@ -356,13 +401,26 @@ void ThrowIfDisposed()
/// </summary>
public void Dispose()
{
lock (LockObject)
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Perform object cleanup.
/// </summary>
/// <param name="disposing">Indicates whether the method call comes from a Dispose method (true) or from a finalizer (false)</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (!IsDisposed)
lock (LockObject)
{
Count = AddWithClamp(Count, Interlocked.Exchange(ref InterlockedCountMinusOne, long.MinValue) + 1);
Timer.Dispose();
IsDisposed = true;
if (!IsDisposed)
{
Count = AddWithClamp(Count, Interlocked.Exchange(ref InterlockedCountMinusOne, long.MinValue) + 1);
Timer.Dispose();
IsDisposed = true;
}
}
}
}
Expand Down
27 changes: 0 additions & 27 deletions Debounce/IBufferer.cs

This file was deleted.

Loading

0 comments on commit 9942c7f

Please sign in to comment.