tldr:
counting without cumulative timing errors
Sometimes I want just a small counter, incrementing an integer each second
running somewhere in a terminal. Maybe it is because my wristwatch is in the
bathroom or because I want to do more rewarding things than counting seconds
manually. Maybe I want not only to know how long something takes but also for
how long it already ran in the middle of its execution? There are many reason
why I would want some script that does nothing else than simply counting upward
or downward with some specific frequency.
Some bonuses:
- the period should be possible to give as a floating point number and
especially periods of a fraction of a second would be nice
- it should be able to execute an arbitrary program after each period
- it should not matter how long the execution of this program takes for the
overall counting
Now this can not be hard, right? One would probably write this line and be done
with it:
while sleep 1; do echo $i; i=$((i+1)); done
or to count for a certain number of steps:
for i in seq 1 100 ; do echo $i; sleep 1; done
This would roughly do the job but in each iteration some small offset would be
added and though small, this offset would quickly accumulate.
Sure that cumulative error is tiny but given that this task seems to be so damn
trivial I couldn't bear anymore with running any of the above but started
looking into a solution.
Sure I could just quickly hack a small C script that would check
gettimeofday(2) at each iteration and would adjust the time to usleep(3)
accordinly but there HAD to be people before me with the same problem who
already came up with a solution.
And there was! The solution is the sleepenh(1) program which, when given the
timestamp of its last invocation and the sleep time in floating point seconds,
will sleep for just the right amount to keep the overall frequency stable.
The author suggests, that sleepenh is to be used in shell scripts that need to
repeat an action in a regular time interval and that is just what I did.
The result is trivial and simple but does just what I want:
- the interval will stay the same on average and the counter will not "fall behind"
- count upward or downward
- specify interval length as a floating point number of seconds including fractions of one second
- begin to count at given integer and count for a specific number of times or until infinity
- execute a program at every step, optionally by forking it from the script for programs possibly running longer than the given interval
You can check it out and read how to use and what to do with it on github:
https://github.com/josch/periodic
Now lets compare the periodic script with the second example from above:
$ time sh -c 'for i in seq 1 1000 ; do echo $i; sleep 1; done'
0.08s user 0.12s system 0% cpu 16:41.55 total
So after only 1000 iterations, the counter is already off by 1.55 seconds. This
means that instead of having run with a frequency of 1.0 Hz, the actual
frequency was 1.00155 Hz. Is it too much to not want this 0.155% of error?
$ time ./periodic -c 1000
0.32s user 0.00s system 0% cpu 16:40.00 total
1000 iterations took exactly 1000 seconds. Cool.