The problem#
For a compile-on-save workflow where some computation is done in response to every change to a file, if there may sometimes be many changes close together, it may be wasteful to respond to all of them. This is often handled by debouncing the events: instead of responding to every change, ignore changes that occur too close together in time.
The solution#
watch_todo_debounced.sh
is a modification
of the watch_todo.sh
script from my recent
post on converting todo.txt files to HTML. It uses a
script I found called debounce.sh to wait until there have been no
updates to the todo.txt file for 5 seconds before generating the HTML
file. The core logic looks like this:
while true
do
inotifywait -e close_write "$1" >/dev/null
echo "TODO file updated."
done | debounce.sh read $delay "./do-something.sh \"$1\""
$delay
is the delay in seconds to wait before taking an action. $1
is file to watch for changes on and ./do-something.sh
is the script to
run on it when it changes.
The details#
When to run the action#
Given a quick sequence of events, there's multiple choices of when to run the action which have different trade-offs between computation and latency:
-
To minimize the runs of the action, wait until it has been
$delay
seconds since the last event and then run the action, which maximizes the latency to the first execution of the action. This is what debounce.sh does. -
Run the action every
$delay
seconds as long as the events keep coming. oalders/debounce implements this logic. -
(2) but wait
$delay
seconds before running the action for the first time. If the quick sequence of events is shorter than$delay
seconds, this will only run the action once but have lower latency than (1).watch_todo.sh
or similar can be easily modified to implement this logic simply by addingsleep $delay
after theinotifywait
call and before running the action.
Specifying the event and action#
Passing a command to be run by another shell command is awkward due to the possibility of having arbitrarily many arguments and escaping can get complicated. But it's further complicated in this scenario because we want to pass two commands into our script: the "wait for event" command and the "perform action" command.
debounce.sh's solution is to just not try that hard. The event command
does not support arguments. The action command is passed to bash
-c
, so it's effectively interpreted as a string of
a Bash script, so it can include calling a program with arguments if
you're careful with escaping.
As a workaround, instead of passing the "wait for event" script to
debounce.sh, the code above just tells it to run read
and runs the
code to wait for events outside of debounce.sh. It echo
es a line
to the pipe to debounce.sh for each event, which results in read
returning.
Other debounce implementations#
In addition to debounce.sh, I found a few other shell scripts implementing similar ideas:
-
oalders/debounce defines a command
debounce
which takes a delay and a command and checks its log of the last time that command was run. If it was run more recently than the specified delay, then it does nothing. Otherwise, it runs the command and logs the it was run at. This means the first timedebounce
is run with a command, it executes it immediately. This is effectively variation (2) described above. -
throttle-and-debounce.sh defines Bash functions
@throttle
and@debounce
which implement variations (2) and (3) respectively. -
This Reddit post (referencing this blog post) also implements the logic for variation (3) described above.
Comments
Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.
There are no comments yet.