Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# event debounce

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/

like image 286
Tobia Avatar asked Feb 12 '15 08:02

Tobia


2 Answers

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.

like image 178
Mike Ward Avatar answered Oct 11 '22 14:10

Mike Ward


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.

like image 42
Panagiotis Kanavos Avatar answered Oct 11 '22 15:10

Panagiotis Kanavos