For some code I\'m writing I could use a nice general implementation of debounce
in Java.
public interface Callback {
public void call(Object
My implementation, very easy to use, 2 util methods for debounce and throttle, pass your runnable into it to get the debounce/throttle runnable
package basic.thread.utils;
public class ThreadUtils {
/** Make a runnable become debounce
*
* usage: to reduce the real processing for some task
*
* example: the stock price sometimes probably changes 1000 times in 1 second,
* but you just want redraw the candlestick of k-line chart after last change+"delay ms"
*
* @param realRunner Runnable that has something real to do
* @param delay milliseconds that realRunner should wait since last call
* @return
*/
public static Runnable debounce (Runnable realRunner, long delay) {
Runnable debounceRunner = new Runnable() {
// whether is waiting to run
private boolean _isWaiting = false;
// target time to run realRunner
private long _timeToRun;
// specified delay time to wait
private long _delay = delay;
// Runnable that has the real task to run
private Runnable _realRunner = realRunner;
@Override
public void run() {
// current time
long now;
synchronized (this) {
now = System.currentTimeMillis();
// update time to run each time
_timeToRun = now+_delay;
// another thread is waiting, skip
if (_isWaiting) return;
// set waiting status
_isWaiting = true;
}
try {
// wait until target time
while (now < _timeToRun) {
Thread.sleep(_timeToRun-now);
now = System.currentTimeMillis();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// clear waiting status before run
_isWaiting = false;
// do the real task
_realRunner.run();
}
}};
return debounceRunner;
}
/** Make a runnable become throttle
*
* usage: to smoothly reduce running times of some task
*
* example: assume the price of a stock often updated 1000 times per second
* but you want to redraw the candlestick of k-line at most once per 300ms
*
* @param realRunner
* @param delay
* @return
*/
public static Runnable throttle (Runnable realRunner, long delay) {
Runnable throttleRunner = new Runnable() {
// whether is waiting to run
private boolean _isWaiting = false;
// target time to run realRunner
private long _timeToRun;
// specified delay time to wait
private long _delay = delay;
// Runnable that has the real task to run
private Runnable _realRunner = realRunner;
@Override
public void run() {
// current time
long now;
synchronized (this) {
// another thread is waiting, skip
if (_isWaiting) return;
now = System.currentTimeMillis();
// update time to run
// do not update it each time since
// you do not want to postpone it unlimited
_timeToRun = now+_delay;
// set waiting status
_isWaiting = true;
}
try {
Thread.sleep(_timeToRun-now);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// clear waiting status before run
_isWaiting = false;
// do the real task
_realRunner.run();
}
}};
return throttleRunner;
}
}
Here is my working implementation:
Execution Callback:
public interface cbDebounce {
void execute();
}
Debouncer:
public class Debouncer {
private Timer timer;
private ConcurrentHashMap<String, TimerTask> delayedTaskMap;
public Debouncer() {
this.timer = new Timer(true); //run as daemon
this.delayedTaskMap = new ConcurrentHashMap<>();
}
public void debounce(final String key, final cbDebounce debounceCallback, final long delay) {
if (key == null || key.isEmpty() || key.trim().length() < 1 || delay < 0) return;
cancelPreviousTasks(); //if any
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
debounceCallback.execute();
cancelPreviousTasks();
delayedTaskMap.clear();
if (timer != null) timer.cancel();
}
};
scheduleNewTask(key, timerTask, delay);
}
private void cancelPreviousTasks() {
if (delayedTaskMap == null) return;
if (!delayedTaskMap.isEmpty()) delayedTaskMap
.forEachEntry(1000, entry -> entry.getValue().cancel());
delayedTaskMap.clear();
}
private void scheduleNewTask(String key, TimerTask timerTask, long delay) {
if (key == null || key.isEmpty() || key.trim().length() < 1 || timerTask == null || delay < 0) return;
if (delayedTaskMap.containsKey(key)) return;
timer.schedule(timerTask, delay);
delayedTaskMap.put(key, timerTask);
}
}
Main (to test)
public class Main {
private static Debouncer debouncer;
public static void main(String[] args) throws IOException, InterruptedException {
debouncer = new Debouncer();
search("H");
search("HE");
search("HEL");
System.out.println("Waiting for user to finish typing");
Thread.sleep(2000);
search("HELL");
search("HELLO");
}
private static void search(String searchPhrase) {
System.out.println("Search for: " + searchPhrase);
cbDebounce debounceCallback = () -> System.out.println("Now Executing search for: "+searchPhrase);
debouncer.debounce(searchPhrase, debounceCallback, 4000); //wait 4 seconds after user's last keystroke
}
}
Output
I've updated @Eyal's answer to be able to configure debouncing time in each call, and use runnable code block instead of callback:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Debouncer<T> {
private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>();
public Debouncer() {
}
public void call(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
TimerTask task = new TimerTask(key, runnable, interval, timeUnit);
TimerTask prev;
do {
prev = delayedMap.putIfAbsent(key, task);
if (prev == null)
sched.schedule(task, interval, timeUnit);
} while (prev != null && !prev.extend());
}
public void terminate() {
sched.shutdownNow();
}
private class TimerTask implements Runnable {
private final T key;
private final Runnable runnable;
private final int interval;
private final TimeUnit timeUnit;
private long dueTime;
private final Object lock = new Object();
public TimerTask(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
this.key = key;
this.runnable = runnable;
this.interval = interval;
this.timeUnit = timeUnit;
extend();
}
public boolean extend() {
synchronized (lock) {
if (dueTime < 0)
return false;
dueTime = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(interval, timeUnit);
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 {
runnable.run();
} finally {
delayedMap.remove(key);
}
}
}
}
}
}
I don't know if it exists but it should be simple to implement.
class Debouncer implements Callback {
private CallBack c;
private volatile long lastCalled;
private int interval;
public Debouncer(Callback c, int interval) {
//init fields
}
public void call(Object arg) {
if( lastCalled + interval < System.currentTimeMillis() ) {
lastCalled = System.currentTimeMillis();
c.call( arg );
}
}
}
Of course this example oversimplifies it a bit, but this is more or less all you need. If you want to keep separate timeouts for different arguments, you'll need a Map<Object,long>
instead of just a long
to keep track of the last execution time.
This looks like it could work:
class Debouncer implements Callback {
private Callback callback;
private Map<Integer, Timer> scheduled = new HashMap<Integer, Timer>();
private int delay;
public Debouncer(Callback c, int delay) {
this.callback = c;
this.delay = delay;
}
public void call(final Object arg) {
final int h = arg.hashCode();
Timer task = scheduled.remove(h);
if (task != null) { task.cancel(); }
task = new Timer();
scheduled.put(h, task);
task.schedule(new TimerTask() {
@Override
public void run() {
callback.call(arg);
scheduled.remove(h);
}
}, this.delay);
}
}
The following implementation works on Handler based threads (e.g. the main UI thread, or in an IntentService). It expects only to be called from the thread on which it is created, and it will also run it's action on this thread.
public class Debouncer
{
private CountDownTimer debounceTimer;
private Runnable pendingRunnable;
public Debouncer() {
}
public void debounce(Runnable runnable, long delayMs) {
pendingRunnable = runnable;
cancelTimer();
startTimer(delayMs);
}
public void cancel() {
cancelTimer();
pendingRunnable = null;
}
private void startTimer(final long updateIntervalMs) {
if (updateIntervalMs > 0) {
// Debounce timer
debounceTimer = new CountDownTimer(updateIntervalMs, updateIntervalMs) {
@Override
public void onTick(long millisUntilFinished) {
// Do nothing
}
@Override
public void onFinish() {
execute();
}
};
debounceTimer.start();
}
else {
// Do immediately
execute();
}
}
private void cancelTimer() {
if (debounceTimer != null) {
debounceTimer.cancel();
debounceTimer = null;
}
}
private void execute() {
if (pendingRunnable != null) {
pendingRunnable.run();
pendingRunnable = null;
}
}
}