3
\$\begingroup\$

I have the following problem. I want to write a class that handles events and passes it through but reduce its number if there recently was an event. Basically it is debounce. However there is a slight change that if we don't receive events for a while and the latest event was skipped we want to emit this event.

I wrote the following

public class Throttle
{
    private string _latest;
    private DateTime _date = DateTime.MinValue;
    private static TimeSpan _timeout = TimeSpan.FromSeconds(2);
    private readonly Timer _timer;

    public Throttle()
    {
        _timer = new Timer(OnTimer);
    }

    public void Handle(string e)
    {
        lock (_timer)
        {
            var now = DateTime.Now;
            if (now - _date < _timeout)
            {
                _latest = e;
                _timer.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);
            }
            else
            {
                _date = now;
                _latest = null;
                _timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
                HandleNext(e);
            }
        }
    }
    
    private void OnTimer(object state)
    {
        lock (_timer)
        {
            if (_latest == null)
            {
                return;
            }
            var e = _latest;
            _latest = null;
            _date = DateTime.Now;
            HandleNext(e);
        }
    }

    private void HandleNext(string s)
    {
        Console.WriteLine(s);
    }
}

It behaves as I want. However when the number of Throttle classes gets high, it noticeably consumes CPU in comparison to version without the timer. However there is no way I can skip the the latest event or postpone it for a long time.

What can you suggest to make it more efficient? And what exactly is the problem?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Have you done some kind of profiling, for example via CodeTrack? \$\endgroup\$ Oct 4, 2021 at 7:30
  • \$\begingroup\$ For future experience take a look at Rx.NET. Reactive extensions does such jobs out-of-the-box. \$\endgroup\$
    – aepot
    Oct 18, 2021 at 9:38

1 Answer 1

5
\$\begingroup\$

Using the following function to test, 62% of your CPU usage is used by DateTime.Now and 15% by Timer.Change

static void Main()
{
    var throttle = new Throttle();
    for (int index=0; index < int.MaxValue; index++)
    {
        throttle.Handle("index = " + index);
    }
}

enter image description here Total runtime: 690.76 seconds

DateTime.Now looks deceptively simple but it's quite expensive, see this for more details. A better approach would be to use Environment.TickCount or System.Diagnostics.Stopwatch which handles it for you:

public class Throttle
{
    private const double _interval = 2000; // timeout in miliseconds
    private string _latest = null;
    private Stopwatch _stopwatch = new Stopwatch();
    private Timer _timer = new Timer(_interval);

    public Throttle()
    {
        _timer.Elapsed += OnTimer;
    }

    public void Handle(string e)
    {
        if (_stopwatch.IsRunning == false)
        {
            HandleNext(e);
            _stopwatch.Start();
        }
        else
        {
            if (_stopwatch.ElapsedMilliseconds > _interval)
            {
                _timer.Stop();
                HandleNext(e);
                _stopwatch.Restart();
                _timer.Start();
            }
            else
            {
                _latest = e;
            }
        }
    }

    public void OnTimer(object sender, ElapsedEventArgs e)
    {
        if (_latest != null)
        {
            if (_stopwatch.ElapsedMilliseconds > _interval)
            {
                HandleNext(_latest);
                _latest = null;
                _stopwatch.Restart();
            }
        }
    }

    private void HandleNext(string s)
    {
        Console.WriteLine(s);
    }
}

enter image description here Total runtime: 115.74 seconds

This is looking better but a lot of time is still wasted by polling ElapsedMilliseconds. We can refactor the class to simply start and stop the timer as needed:

public class Throttle
{
    private string _latest = null;
    private System.Timers.Timer _timer = null;

    public Throttle()
    {
        _timer = new Timer(2000);
        _timer.Elapsed += OnTimer;
    }

    public void Handle(string e)
    {
        if (_timer.Enabled == false)
        {
             HandleNext(e);
            _timer.Start();
        }
        else
        {
            _latest = e;
        }
    }

    public void OnTimer(object sender, ElapsedEventArgs e)
    {
        if (_latest != null)
        {
            HandleNext(_latest);
            _latest = null;
        }
        else
        {
            _timer.Stop();
        }
    }

    private void HandleNext(string s)
    {
        Console.WriteLine(s);
    }
}

enter image description here Total runtime: 67.04 seconds

Finally, lots of CPU time is still wasted on formatting the progress string which is rarely used, we can refactor the code to only format it when needed:

public class Throttle
{
    private int _target;
    private int _latest = -1;
    private System.Timers.Timer _timer = null;

    public Throttle(int target, double interval = 200)
    {
        _target = target;
        _timer = new Timer(interval);
        _timer.Elapsed += OnTimer;
    }

    public void Handle(int e)
    {
        if (_timer.Enabled == false)
        {
             HandleNext(e);
            _timer.Start();
        }
        else
        {
            _latest = e;
        }
    }

    public void OnTimer(object sender, ElapsedEventArgs e)
    {
        if (_latest != -1)
        {
            HandleNext(_latest);
            _latest = -1;
        }
        else
        {
            _timer.Stop();
        }
    }

    private void HandleNext(int s)
    {
        Console.WriteLine("index = " + s + " / " + _target);
    }
}

class Program
{
    static void Main()
    {
        var throttle = new Throttle(int.MaxValue);
        for (int index = 0; index < int.MaxValue; index++)
        {
            throttle.Handle(index);
        }
    }
}

enter image description here Total runtime: 1.07 seconds

Note that Timer is a disposable object and generally speaking you should always dispose of disposable resources. This can also be a good place to ensure that the final event is emitted.

public class Throttle : IDisposable
{
    public void Dispose()
    {
        _timer.Dispose();
        if (_latest != -1)
            HandleNext(_latest);
    }

}

class Program
{
    static void Main()
    {
        using (var throttle = new Throttle(int.MaxValue))
        {
            for (int index = 0; index < int.MaxValue; index++)
            {
                throttle.Handle(index);
            }
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.