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?
-
1\$\begingroup\$ Have you done some kind of profiling, for example via CodeTrack? \$\endgroup\$Peter Csala– Peter Csala2021年10月04日 07:30:06 +00:00Commented 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– aepot2021年10月18日 09:38:25 +00:00Commented Oct 18, 2021 at 9:38
1 Answer 1
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);
}
}
}
}