For some code I'm writing I could use a nice general implementation of debounce
in Java.
public interface Callback { public void call(Object arg); } class Debouncer implements Callback { public Debouncer(Callback c, int interval) { ... } public void call(Object arg) { // should forward calls with the same arguments to the callback c // but batch multiple calls inside `interval` to a single one } }
When call()
is called multiple times in interval
milliseconds with the same argument the callback function should be called exactly once.
A visualization:
Debouncer#call xxx x xxxxxxx xxxxxxxxxxxxxxx Callback#call x x x (interval is 2)
The debounce() function forces a function to wait a certain amount of time before running again. The function is built to limit the number of times a function is called.
Bouncing is the tendency of any two metal contacts in an electronic device to generate multiple signals as the contacts close or open; debouncing is any kind of hardware device or software that ensures that only a single signal will be acted upon for a single opening or closing of a contact.
util. debounce. Wraps a function to allow it to be called, at most, once for each sequence of calls fired repeatedly so long as they are fired less than a specified interval apart (in milliseconds). This can be used to reduce the number of invocations of an expensive function while ensuring it eventually runs.
Please consider the following thread safe solution. Note that the lock granularity is on the key level, so that only calls on the same key block each other. It also handles the case of an expiration on key K which occurs while call(K) is called.
public class Debouncer <T> { private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1); private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>(); private final Callback<T> callback; private final int interval; public Debouncer(Callback<T> c, int interval) { this.callback = c; this.interval = interval; } public void call(T key) { TimerTask task = new TimerTask(key); TimerTask prev; do { prev = delayedMap.putIfAbsent(key, task); if (prev == null) sched.schedule(task, interval, TimeUnit.MILLISECONDS); } while (prev != null && !prev.extend()); // Exit only if new task was added to map, or existing task was extended successfully } public void terminate() { sched.shutdownNow(); } // The task that wakes up when the wait time elapses private class TimerTask implements Runnable { private final T key; private long dueTime; private final Object lock = new Object(); public TimerTask(T key) { this.key = key; extend(); } public boolean extend() { synchronized (lock) { if (dueTime < 0) // Task has been shutdown return false; dueTime = System.currentTimeMillis() + interval; return true; } } public void run() { synchronized (lock) { long remaining = dueTime - System.currentTimeMillis(); if (remaining > 0) { // Re-schedule task sched.schedule(this, remaining, TimeUnit.MILLISECONDS); } else { // Mark as terminated and invoke callback dueTime = -1; try { callback.call(key); } finally { delayedMap.remove(key); } } } } }
and callback interface:
public interface Callback<T> { public void call(T t); }
Here's my implementation:
public class Debouncer { private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private final ConcurrentHashMap<Object, Future<?>> delayedMap = new ConcurrentHashMap<>(); /** * Debounces {@code callable} by {@code delay}, i.e., schedules it to be executed after {@code delay}, * or cancels its execution if the method is called with the same key within the {@code delay} again. */ public void debounce(final Object key, final Runnable runnable, long delay, TimeUnit unit) { final Future<?> prev = delayedMap.put(key, scheduler.schedule(new Runnable() { @Override public void run() { try { runnable.run(); } finally { delayedMap.remove(key); } } }, delay, unit)); if (prev != null) { prev.cancel(true); } } public void shutdown() { scheduler.shutdownNow(); } }
Example usage:
final Debouncer debouncer = new Debouncer(); debouncer.debounce(Void.class, new Runnable() { @Override public void run() { // ... } }, 300, TimeUnit.MILLISECONDS);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With