C# event debounce

后端 未结 14 1025
南旧
南旧 2020-11-28 06:22

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 t

相关标签:
14条回答
  • 2020-11-28 06:33

    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.

    0 讨论(0)
  • 2020-11-28 06:34

    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;            
        }
    
    0 讨论(0)
  • 2020-11-28 06:34

    Simply remember the latest 'hit:

    DateTime latestHit = DatetIme.MinValue;
    
    private void eventRxVARxH(MachineClass Machine)
    {
        log.Debug("Event fired");
        if(latestHit - DateTime.Now < TimeSpan.FromXYZ() // too fast
        {
            // ignore second hit, too fast
            return;
        }
        latestHit = DateTime.Now;
        // it was slow enough, do processing
        ...
    }
    

    This will allow a second event if there was enough time after the last event.

    Please note: it is not possible (in a simple way) to handle the last event in a series of fast events, because you never know which one is the last...

    ...unless you are prepared to handle the last event of a burst which is a long time ago. Then you have to remember the last event and log it if the next event is slow enough:

    DateTime latestHit = DatetIme.MinValue;
    Machine historicEvent;
    
    private void eventRxVARxH(MachineClass Machine)
    {
        log.Debug("Event fired");
    
        if(latestHit - DateTime.Now < TimeSpan.FromXYZ() // too fast
        {
            // ignore second hit, too fast
            historicEvent = Machine; // or some property
            return;
        }
        latestHit = DateTime.Now;
        // it was slow enough, do processing
        ...
        // process historicEvent
        ...
        historicEvent = Machine; 
    }
    
    0 讨论(0)
  • 2020-11-28 06:36

    Panagiotis's answer is certainly correct, however I wanted to give a simpler example, as it took me a while to sort through how to get it working. My scenario is that a user types in a search box, and as the user types we want to make api calls to return search suggestions, so we want to debounce the api calls so they don't make one every time they type a character.

    I'm using Xamarin.Android, however this should apply to any C# scenario...

    private Subject<string> typingSubject = new Subject<string> ();
    private IDisposable typingEventSequence;
    
    private void Init () {
                var searchText = layoutView.FindViewById<EditText> (Resource.Id.search_text);
                searchText.TextChanged += SearchTextChanged;
                typingEventSequence = typingSubject.Throttle (TimeSpan.FromSeconds (1))
                    .Subscribe (query => suggestionsAdapter.Get (query));
    }
    
    private void SearchTextChanged (object sender, TextChangedEventArgs e) {
                var searchText = layoutView.FindViewById<EditText> (Resource.Id.search_text);
                typingSubject.OnNext (searchText.Text.Trim ());
            }
    
    public override void OnDestroy () {
                if (typingEventSequence != null)
                    typingEventSequence.Dispose ();
                base.OnDestroy ();
            }
    

    When you first initialize the screen / class, you create your event to listen to the user typing (SearchTextChanged), and then also set up a throttling subscription, which is tied to the "typingSubject".

    Next, in your SearchTextChanged event, you can call typingSubject.OnNext and pass in the search box's text. After the debounce period (1 second), it will call the subscribed event (suggestionsAdapter.Get in our case.)

    Lastly, when the screen is closed, make sure to dispose of the subscription!

    0 讨论(0)
  • 2020-11-28 06:36

    This little gem is inspired by Mike Wards diabolically ingenious extension attempt. However, this one cleans up after itself quite nicely.

    public static Action Debounce(this Action action, int milliseconds = 300)
    {
        CancellationTokenSource lastCToken = null;
    
        return () =>
        {
            //Cancel/dispose previous
            lastCToken?.Cancel();
            try { 
                lastCToken?.Dispose(); 
            } catch {}          
    
            var tokenSrc = lastCToken = new CancellationTokenSource();
    
            Task.Delay(milliseconds).ContinueWith(task => { action(); }, tokenSrc.Token);
        };
    }
    

    Note: there's no need to dispose of the task in this case. See here for the evidence.

    Usage

    Action DebounceToConsole;
    int count = 0;
    
    void Main()
    {
        //Assign
        DebounceToConsole = ((Action)ToConsole).Debounce(50);
    
        var random = new Random();
        for (int i = 0; i < 50; i++)
        {
            DebounceToConsole();
            Thread.Sleep(random.Next(100));
        }
    }
    
    public void ToConsole()
    {
        Console.WriteLine($"I ran for the {++count} time.");
    }
    
    0 讨论(0)
  • 2020-11-28 06:37

    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.

    0 讨论(0)
提交回复
热议问题