C# event debounce

31,199

Solution 1

This isn't a trivial request to code from scratch as there are several nuances. A similar scenario is monitoring a FileSystemWatcher and waiting for things to quiet down after a big copy, before you try to open the modified files.

Reactive Extensions in .NET 4.5 were created to handle exactly these scenarios. You can use them easily to provide such functionality with methods like Throttle, Buffer, Window or Sample. You post the events to a Subject, apply one of the windowing functions to it, for example to get a notification only if there was no activity for X seconds or Y events, then subscribe to the notification.

Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
                          .Subscribe(events=>MySubscriptionMethod(events));

Throttle returns the last event in a sliding window, only if there were no other events in the window. Any event resets the window.

You can find a very good overview of the time-shifted functions here

When your code receives the event, you only need to post it to the Subject with OnNext:

_mySubject.OnNext(MyEventData);

If your hardware event surfaces as a typical .NET Event, you can bypass the Subject and manual posting with Observable.FromEventPattern, as shown here:

var mySequence = Observable.FromEventPattern<MyEventData>(
    h => _myDevice.MyEvent += h,
    h => _myDevice.MyEvent -= h);  
_mySequence.Throttle(TimeSpan.FromSeconds(1))
           .Subscribe(events=>MySubscriptionMethod(events));

You can also create observables from Tasks, combine event sequences with LINQ operators to request eg: pairs of different hardware events with Zip, use another event source to bound Throttle/Buffer etc, add delays and a lot more.

Reactive Extensions is available as a NuGet package, so it's very easy to add them to your project.

Stephen Cleary's book "Concurrency in C# Cookbook" is a very good resource on Reactive Extensions among other things, and explains how you can use it and how it fits with the rest of the concurrent APIs in .NET like Tasks, Events etc.

Introduction to Rx is an excellent series of articles (that's where I copied the samples from), with several examples.

UPDATE

Using your specific example, you could do something like:

IObservable<MachineClass> _myObservable;

private MachineClass connect()
{

    MachineClass rpc = new MachineClass();
   _myObservable=Observable
                 .FromEventPattern<MachineClass>(
                            h=> rpc.RxVARxH += h,
                            h=> rpc.RxVARxH -= h)
                 .Throttle(TimeSpan.FromSeconds(1));
   _myObservable.Subscribe(machine=>eventRxVARxH(machine));
    return rpc;
}

This can be improved vastly of course - both the observable and the subscription need to be disposed at some point. This code assumes that you only control a single device. If you have many devices, you could create the observable inside the class so that each MachineClass exposes and disposes its own observable.

Solution 2

I've used this to debounce events with some success:

public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
{
    var last = 0;
    return arg =>
    {
        var current = Interlocked.Increment(ref last);
        Task.Delay(milliseconds).ContinueWith(task =>
        {
            if (current == last) func(arg);
            task.Dispose();
        });
    };
}

Usage

Action<int> a = (arg) =>
{
    // This was successfully debounced...
    Console.WriteLine(arg);
};
var debouncedWrapper = a.Debounce<int>();

while (true)
{
    var rndVal = rnd.Next(400);
    Thread.Sleep(rndVal);
    debouncedWrapper(rndVal);
}

It may not be a robust as what's in RX but it's easy to understand and use.

Followup 2020-02-03

Revised @collie's solution using cancellation tokens as follows

public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
{
    CancellationTokenSource? cancelTokenSource = null;

    return arg =>
    {
        cancelTokenSource?.Cancel();
        cancelTokenSource = new CancellationTokenSource();

        Task.Delay(milliseconds, cancelTokenSource.Token)
            .ContinueWith(t =>
            {
                if (t.IsCompletedSuccessfully)
                {
                    func(arg);
                }
            }, TaskScheduler.Default);
    };
}

Notes:

  • Calling Cancel is enough to dispose of the CTS
  • A successfully completed CTS is not canceled/disposed until the next call
  • As noted by @collie, tasks get disposed so no need to call Dispose on the task

I've not worked with cancellation tokens before and may not be using them correctly.

Solution 3

I ran into issues with this. I tried each of the answers here, and since I'm in a Xamarin universal app, I seem to be missing certain things that are required in each of these answers, and I didn't want to add any more packages or libraries. My solution works exactly how I'd expect it to, and I haven't run into any issues with it. Hope it helps somebody.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OrderScanner.Models
{
    class Debouncer
    {
        private List<CancellationTokenSource> StepperCancelTokens = new List<CancellationTokenSource>();
        private int MillisecondsToWait;
        private readonly object _lockThis = new object(); // Use a locking object to prevent the debouncer to trigger again while the func is still running

        public Debouncer(int millisecondsToWait = 300)
        {
            this.MillisecondsToWait = millisecondsToWait;
        }

        public void Debouce(Action func)
        {
            CancelAllStepperTokens(); // Cancel all api requests;
            var newTokenSrc = new CancellationTokenSource();
            lock (_lockThis)
            {
                StepperCancelTokens.Add(newTokenSrc);
            }
            Task.Delay(MillisecondsToWait, newTokenSrc.Token).ContinueWith(task => // Create new request
            {
                if (!newTokenSrc.IsCancellationRequested) // if it hasn't been cancelled
                {
                    CancelAllStepperTokens(); // Cancel any that remain (there shouldn't be any)
                    StepperCancelTokens = new List<CancellationTokenSource>(); // set to new list
                    lock (_lockThis)
                    {
                        func(); // run
                    }
                }
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

        private void CancelAllStepperTokens()
        {
            foreach (var token in StepperCancelTokens)
            {
                if (!token.IsCancellationRequested)
                {
                    token.Cancel();
                }
            }
        }
    }
}

It's called like so...

private Debouncer StepperDeboucer = new Debouncer(1000); // one second

StepperDeboucer.Debouce(() => { WhateverMethod(args) });

I wouldn't recommend this for anything where the machine could be sending in hundreds of requests a second, but for user input, it works excellently. I'm using it on a stepper in an android/IOS app that calls to an api on step.

Solution 4

Recently I was doing some maintenance on an application that was targeting an older version of the .NET framework (v3.5).

I couldn't use Reactive Extensions nor Task Parallel Library, but I needed a nice, clean, consistent way of debouncing events. Here's what I came up with:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace MyApplication
{
    public class Debouncer : IDisposable
    {
        readonly TimeSpan _ts;
        readonly Action _action;
        readonly HashSet<ManualResetEvent> _resets = new HashSet<ManualResetEvent>();
        readonly object _mutex = new object();

        public Debouncer(TimeSpan timespan, Action action)
        {
            _ts = timespan;
            _action = action;
        }

        public void Invoke()
        {
            var thisReset = new ManualResetEvent(false);

            lock (_mutex)
            {
                while (_resets.Count > 0)
                {
                    var otherReset = _resets.First();
                    _resets.Remove(otherReset);
                    otherReset.Set();
                }

                _resets.Add(thisReset);
            }

            ThreadPool.QueueUserWorkItem(_ =>
            {
                try
                {
                    if (!thisReset.WaitOne(_ts))
                    {
                        _action();
                    }
                }
                finally
                {
                    lock (_mutex)
                    {
                        using (thisReset)
                            _resets.Remove(thisReset);
                    }
                }
            });
        }

        public void Dispose()
        {
            lock (_mutex)
            {
                while (_resets.Count > 0)
                {
                    var reset = _resets.First();
                    _resets.Remove(reset);
                    reset.Set();
                }
            }
        }
    }
}

Here's an example of using it in a windows form that has a search text box:

public partial class Example : Form 
{
    private readonly Debouncer _searchDebouncer;

    public Example()
    {
        InitializeComponent();
        _searchDebouncer = new Debouncer(TimeSpan.FromSeconds(.75), Search);
        txtSearchText.TextChanged += txtSearchText_TextChanged;
    }

    private void txtSearchText_TextChanged(object sender, EventArgs e)
    {
        _searchDebouncer.Invoke();
    }

    private void Search()
    {
        if (InvokeRequired)
        {
            Invoke((Action)Search);
            return;
        }

        if (!string.IsNullOrEmpty(txtSearchText.Text))
        {
            // Search here
        }
    }
}

Solution 5

RX is probably the easiest choice, especially if you're already using it in your application. But if not, adding it might be a bit of overkill.

For UI based applications (like WPF) I use the following class that use DispatcherTimer:

public class DebounceDispatcher
{
    private DispatcherTimer timer;
    private DateTime timerStarted { get; set; } = DateTime.UtcNow.AddYears(-1);

    public void Debounce(int interval, Action<object> action,
        object param = null,
        DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
        Dispatcher disp = null)
    {
        // kill pending timer and pending ticks
        timer?.Stop();
        timer = null;

        if (disp == null)
            disp = Dispatcher.CurrentDispatcher;

        // timer is recreated for each event and effectively
        // resets the timeout. Action only fires after timeout has fully
        // elapsed without other events firing in between
        timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) =>
        {
            if (timer == null)
                return;

            timer?.Stop();
            timer = null;
            action.Invoke(param);
        }, disp);

        timer.Start();
    }
}

To use it:

private DebounceDispatcher debounceTimer = new DebounceDispatcher();

private void TextSearchText_KeyUp(object sender, KeyEventArgs e)
{
    debounceTimer.Debounce(500, parm =>
    {
        Model.AppModel.Window.ShowStatus("Searching topics...");
        Model.TopicsFilter = TextSearchText.Text;
        Model.AppModel.Window.ShowStatus();
    });
}

Key events are now only processed after keyboard is idle for 200ms - any previous pending events are discarded.

There's also a Throttle method which always fires events after a given interval:

    public void Throttle(int interval, Action<object> action,
        object param = null,
        DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
        Dispatcher disp = null)
    {
        // kill pending timer and pending ticks
        timer?.Stop();
        timer = null;

        if (disp == null)
            disp = Dispatcher.CurrentDispatcher;

        var curTime = DateTime.UtcNow;

        // if timeout is not up yet - adjust timeout to fire 
        // with potentially new Action parameters           
        if (curTime.Subtract(timerStarted).TotalMilliseconds < interval)
            interval = (int) curTime.Subtract(timerStarted).TotalMilliseconds;

        timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) =>
        {
            if (timer == null)
                return;

            timer?.Stop();
            timer = null;
            action.Invoke(param);
        }, disp);

        timer.Start();
        timerStarted = curTime;            
    }
Share:
31,199
Tobia
Author by

Tobia

Updated on December 23, 2021

Comments

  • Tobia
    Tobia over 2 years

    I'm listening to a hardware event message, but I need to debounce it to avoid too many queries.

    This is an hardware event that sends the machine status and I have to store it in a database for statistical purposes, and it sometimes happens that its status changes very often (flickering?). In this case I would like to store only a "stable" status and I want to implement it by simply waiting for 1-2s before storing the status to the database.

    This is my code:

    private MachineClass connect()
    {
        try
        {
            MachineClass rpc = new MachineClass();
            rpc.RxVARxH += eventRxVARxH;
            return rpc;
        }
        catch (Exception e1)
        {
            log.Error(e1.Message);
            return null;
        }
    }
    
    private void eventRxVARxH(MachineClass Machine)
    {
        log.Debug("Event fired");
    }
    

    I call this behaviour "debounce": wait a few times to really do its job: if the same event is fired again during the debounce time, I have to dismiss the first request and start to wait the debounce time to complete the second event.

    What is the best choice to manage it? Simply a one-shot timer?

    To explain the "debounce" function please see this javascript implementation for key events: http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/

  • Tobia
    Tobia over 9 years
    I want the opposite: I want to consider the last last one not the first.
  • Tobia
    Tobia over 9 years
    I maybe do not what is the last, but I can wait to do this event with a timer, and if nothing happends for 1-2s I do not abort the timer and let the callback do its job. Otherwise, I another event is fired, I have to reset the timer and start waiting again for 1-2s.
  • Panagiotis Kanavos
    Panagiotis Kanavos over 9 years
    Without a timer, this can only work if you get another event XYZ ms after the last
  • Tobia
    Tobia over 9 years
    This seems to be the answer! Thank you... but It is not so easy to implement, and I can not understand how to apply your example to my code.
  • Tobia
    Tobia over 9 years
    Thanks, I get what I missed before!
  • Léon Pelletier
    Léon Pelletier over 7 years
    How did you use it?
  • astrowalker
    astrowalker almost 6 years
    Slick, it took me some time to notice how do you cancel already "running" action :-). However this approach has a problem, you don't have a feedback/control over your debouncer, so you don't control when all the actions finish. This is troublesome, when for example you dispose your main object, and you don't realize the debounced action will be executed after disposal.
  • jjnguy
    jjnguy over 5 years
    I don't understand how this is intended to work. It seems like if Debounce get's called more often than MillisecondsToWait milis, the code will never execute. Am I missing something?
  • Nieminen
    Nieminen over 5 years
    Have you tried it? Works perfectly in my implementations. Just set your debounce time to something like 2000, and debug to see how it works.
  • jjnguy
    jjnguy over 5 years
    When running a tight loop, I end up with no output. while (true) { deBounce.Debounce(() => Console.WriteLine(DateTime.UtcNow.ToString("o")));}
  • Nieminen
    Nieminen over 5 years
    Oh, well of course you don't get an output, it's a debounce, not a throttle. Debounce waits until input events stop for a defined amount of time before running the function. If you want a throttle (run it so many times per defined amount of time) this is not the solution you want.
  • Nieminen
    Nieminen over 5 years
    In practice, say you've got an expensive or slow call that gets made on some user input, (like getting price when you increase a quantity or something like that). You don't want to run the call every click, you only want to when the user has stopped clicking for at least a second. (click, click, click, stop) -> 1 second -> debounce runs.
  • jjnguy
    jjnguy over 5 years
    Ok, that makes more sense. Thanks for clarifying. I completely misunderstood the goal of the code.
  • Professor of programming
    Professor of programming over 4 years
    I tried this as I liked how clean the code was, but this results in performance issues when shutting down the app, any idea why that might be? Changed nothing else, other than adding this code.
  • Professor of programming
    Professor of programming over 4 years
    A timer is a bit overkill for what is needed, the other examples only execute code as and when required, where as this continues to execute code regardless of any events taking place.
  • Anthony Wieser
    Anthony Wieser over 4 years
    If there's a delay at shutdown of the time requested, say 3 seconds, you could pass the same cancellation token to all of the calls to Task.Delay, and set the cancellation token when you shutdown. Note that cancelling will also cause an exception you'll have to deal with around Task.Delay
  • HDL_CinC_Dragon
    HDL_CinC_Dragon over 4 years
    This doesn't continuously execute code. When TextChanged is called, it schedules the saveOrWhatever method to be called 3 seconds later. If TextChanged is called again before saveOrWhatever is called, it resets the timer so it will only be called after 3 seconds has gone by since the last TextChanged call. The second parameter of Change on the timer object is the re-trigger rate. When set to infinite, it will not retrigger after the first code execution.
  • Professor of programming
    Professor of programming over 4 years
    It continuously executes a timer.
  • Collie
    Collie over 4 years
    See stackoverflow.com/a/59296962/545233 for a version that uses cancellation tokens to clean up the unused tasks.
  • Mike Ward
    Mike Ward over 4 years
    Neat idea. Does it need a fence around the Cancel/Dispose/new/assign part? Also, good reference about the task dispose. I always fret about zombie tasks. Guess I can fret a little less now. Thanks.
  • Collie
    Collie over 4 years
    Hmm. Examining the CancellationTokenSource docs, apparently it's thread-safe except for Dispose() "which must only be used when all other operations on the CancellationTokenSource object have completed." . I believe it's safe to Dispose() after Cancel() in this use case. However, they do recommend that Dispose() used in this context be wrapped in a Try-Catch. I'll add that.
  • PEK
    PEK over 4 years
    IsCompletedSuccessfully is only available in .NET Core. You could might use !t.IsCanceled instead to make the code also work in .NET Framework.
  • Arlo
    Arlo almost 4 years
    If you are using this to interact with events coming off the UI thread (as you usually would be), it's worth adding TaskScheduler.FromCurrentSynchronizationContext() to the second parameter of the .ContinueWith call. Otherwise you'll have to make another call to get threading back to the UI, which ruins performance. See: stackoverflow.com/questions/4331262/…
  • Alex from Jitbit
    Alex from Jitbit over 3 years
    This is awesome. I wrote another answer based on your solution, that uses a unique string-key for throttling (rather than actual Action) useful for webapps stackoverflow.com/a/64867741/56621
  • lonix
    lonix over 3 years
    I wrote one that is async without continuations - suitable for use in aspnet
  • TheAtomicOption
    TheAtomicOption over 2 years
    Is there a good reason you're throwing inside a try block if cancelled? Usually if(token.IsCancellationRequested) {return;} is better to avoid the exceptions as control flow antipattern.
  • Douglas Riddle
    Douglas Riddle over 2 years
    @TheAtomicOption Not necessarily. I had not heard that was an anti-pattern. Just to confirm, you feel debounceToken.Token.ThrowIfCancellationRequested(); should be replaced with if (debounceToken.Token.IsCancellationRequested) { return; }
  • TheAtomicOption
    TheAtomicOption over 2 years
    Yes you understood correctly. However on testing it myself, Task.Delay(milliseconds, debounceToken.Token) throws when cancellation is requested, so you'd also have to avoid passing the token and let the Delay task finish normally. I guess if it's designed to throw, then it's designed to throw.
  • hlovdal
    hlovdal about 2 years
    I updated the nuget link since the old Rx-Main package is unlisted and superseeded. But some of the documentation links also are in need of an update.