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?

Sara J
4,20112 silver badges37 bronze badges
asked Oct 3, 2021 at 12:11
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Have you done some kind of profiling, for example via CodeTrack? \$\endgroup\$ Commented 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\$ Commented 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);
 }
 }
 }
}
answered Oct 3, 2021 at 14:36
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.